Skip to content

11.2 · Auth & headers reference

บทนี้รวบรวม header, apiKey, JWT, และ auth flow ของทั้ง 3 services เพื่อให้เวลาเขียน client หรือ debug สามารถเช็คได้ในที่เดียว


ภาพรวม — แต่ละ service ใช้ auth อะ่างไร

ServiceapiKey headerAuthorization headerAuth mechanism
vtrc-apiapiKey: <uuid>Authorization: Bearer <jwt>JWT (HS256) + apiKey (6 APP_TYPE)
cu-central-apiapiKey: <secret>Authorization: Bearer <jwt>JWT (HS256) + apiKey (2 levels) + LDAP bind
centralize-apiไม่ใช้Authorization: Bearer <jwt> (nominal)JWT guard ที่ถูก @Public() bypass

1. vtrc-api auth

Headers ที่ต้องส่ง

Authorization: Bearer <jwt>
apiKey: <device-key-uuid>
Content-Type: application/json

GraphQL multipart (upload) เพิ่ม Content-Type: multipart/form-data

apiKey — Device key mapping

apiKey เป็นตัวบอกว่า request มาจากแอปไหน — ฝังใน config.js:41-149

APP_TYPEAPP_NAME (prod)ใช้โดย
WEBVTRC-WEBvtrc-web frontend
MOBILEVTRC-MOBILEmobile app
WEB_ADMINVTRC-BACKOFFICEvtrc-rc-backoffice frontend
INTEGRATE_APPINFOMA / CONICLEบุคคลที่สาม (integration)
TWOWAYVTRC-TWOWAYTwo-way feedback sub-app
MEETINGVTRC-MEETINGMeeting sub-app

ที่มา: vtrc-api/api/src/config.js:42-84 (prod), 86-148 (UAT — มี LOAD_TEST_* เพิ่ม)

apiKey validation

js
// ใน apollo context (index.js:33-38)
const apiKeyHeader = req.headers.apikey
const mapKey = allowedKeys.filter(value => value.key === apiKeyHeader)
if (mapKey.length === 0) {
  throw responError('ACCESS_KEY_INVALID')
}
// ถ้าผ่าน → context.device = mapKey[0].APP_TYPE

JWT — issue + verify

Issue (login)

js
// lib/repositories/auth/auth.js:96-102
jwt.sign(
  { id: sessionId, uid: userId },
  JWT_SECRET,
  { expiresIn: JWT_EXP, issuer: JWT_ISSUER, jwtid }
)
// default: JWT_EXP='15m', JWT_ISSUER='redcross.or.th:vtrc'

Verify (ใน @auth directive)

js
// directives/auth.js:23-31
const decoded = jwt.decode(token)
const verify = verifyToken(token)
// ถ้า TokenExpiredError → responError('TOKEN_EXPIRED')
// ถ้าอื่น ๆ → responError('TOKEN_INVALID')

@auth directive — field-level

graphql
# typeDefs/root.js:4
directive @auth(accessRole: [String], accessMenu: [String]) on FIELD_DEFINITION
  • field ที่ไม่มี @auth เป็น public — ใครก็เรียกได้ (เช่น login, refreshToken)
  • field ที่มี @auth ต้อง JWT ที่ยังไม่หมดอายุ + session ใน DB + (ถ้าระบุ) role/menu ตรง

ตัวอย่าง

graphql
# public — ไม่ต้อง auth
refreshToken(token: String, refreshToken: String): AccessTokens

# ต้องเป็น USER
fetchProfile: Profile @auth(accessRole: ["USER"])

# ต้องเป็น ADMIN และมีเมนู hospitalHr
createWDHospital(...): ResposeStatus
  @auth(accessRole: ["SUPER_ADMIN","ADMIN","STAFF"], accessMenu: ["hospitalHr"])

Roles

js
// config.js:150-151
ACCESS_BACK_OFFICE = ['SUPER_ADMIN', 'ADMIN', 'STAFF']
ACCESS_USER = ['USER']

รายการ menu key อยู่ใน config.js:159-370 — ตรงกับ MENUS_BACK_OFFICE — ใช้เฉพาะใน wdHospital.js (4 fields) ในปัจจุบัน

Session

นอกจาก JWT แล้ว vtrc-api ยังเก็บ session ใน DB ตาราง Session — ทำให้สามารถ revoke token ได้กลางวัน

js
// lib/repositories/session/session.js:20-48
const checkSession = async (userId, accessRole, jti, device) => {
  // ตรวจ Session table: userId + jwtId + client(device)
  // ถ้าไม่พบ → responError('TOKEN_INVALID')
  // ถ้า expireDate <= now → responError('SESSION_EXPIRED')
}

การ refresh token

graphql
# typeDefs/authentication.js
refreshToken(token: String, refreshToken: String): AccessTokens

flow:

  1. client ส่ง token เก่า + refreshToken
  2. server ตรวจ refreshToken ใน DB
  3. issue JWT + refreshToken ใหม่
  4. client เก็บ token ใหม่

2. cu-central-api auth

Headers ที่ต้องส่ง

Authorization: Bearer <jwt>
apiKey: <secret-string>

apiKey ของ cu-central-api ไม่ใช่ UUID เหมือน vtrc-api — เป็น secret string ที่ยาวกว่า

apiKey — 2 levels

js
// lib/auth/auth.controller.js:253-274
const allowedApiKeys = [
  { apiKey: 'OGJlYTEzOTBjMzk3...', appId: 'acwfa3t-wafbAS-...', appName: 'VTRC_APP',        level: 1 },
  { apiKey: '562B132B1E87695A...', appId: '7d962c5b-68d1-...', appName: 'VTRC_ADMIN_APP',   level: 4 }
]
appNamelevelใช้โดย
VTRC_APP1vtrc-api (ฝั่ง user)
VTRC_ADMIN_APP4vtrc-api (ฝั่ง admin)

ข้อระวัง: apiKey เหล่านี้ ฝังใน source code — เป็น secret ที่ commit ลง git

LDAP bind flow

cu-central-api ใช้ LDAP เป็น auth หลัก ไม่ใช่แค่ JWT:

1. POST /auth/signin { username, password, org, apiKey }

2. checkApiKey(apiKey) → ตรวจ level

3. getEmployeeByEmpCode(username) → หา profile ใน MSSQL

4. internalUsername = sha256(idCard)
   adUsername = crc64(idCard:HASH_KEY)

5. bindAdWithUserAndPassword(adUsername, password)
   → service account bind ก่อน (AD_USERNAME/AD_PASSWORD)
   → แล้ว user bind

6. ถ้าสำเร็จ → generateSignInToken (JWT) + เก็บ session ใน Redis

7. Response { accessToken, refreshToken, userData: { uid, profileKey, sourceDb } }

ที่มา: cu-central-api/main-api/src/lib/auth/auth.js:11-98

JWT — issue + verify

js
// lib/jwtToken.js:32-55
jwt.sign(
  payload,
  JWT_SECRET,
  { expiresIn: JWT_EXPIRE_IN, audience: appId + ':' + hash(uuid) }
)
// default: JWT_EXPIRE_IN='20m'

@auth directive

graphql
# typeDefs/root.js:4
directive @auth(requires: [String], accessLevel: Int, ignoreAuth: Boolean) on FIELD_DEFINITION
  • accessLevel — เช็ค clientAppInfo.level >= accessLevel
  • ignoreAuth: true — ข้าม JWT check ใช้สำหรับ admin-only field (เช่น searchUser)

Session ใน Redis

cu-central-api เก็บ session ใน Redis แทน DB:

key: sess:<sid>
TTL: 1 day
value: { sid, refreshToken, identityCard, userId, appName, audience }

ที่มา: cu-central-api/main-api/src/lib/session/session.js:32-51


3. centralize-api auth

Headers ที่ต้องส่ง (nominal)

Authorization: Bearer <jwt>

JWT guard — ลักษณะเฉพาะ

centralize-api ใช้ passport-jwt + @nestjs/passport:

ts
// src/main.ts:27
app.useGlobalGuards(new JwtAuthGuard(reflector))
ts
// libs/jwt/jwt-auth.guard.ts:22-35
canActivate(context: ExecutionContext) {
  const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
    context.getHandler(),
    context.getClass(),
  ])
  if (isPublic) {
    return true  // ข้าม JWT
  }
  return super.canActivate(context)
}

@Public() — bypassed ทุก route

ทุก endpoint ของ centralize-api มี @Public():

ts
// src/modules/employee/employee.controller.ts:38-42
@Public()
@Get()
getEmployees(@Query() query: GetEmployeeQueryDto) {
  return this.employeeService.getEmployees(query)
}

ผล: JWT guard ไม่ทำงานจริง — ทุก request ผ่านเข้าได้โดยไม่ต้อง auth

ทำจึงเป็นเช่นนั้น

  • ทีมอาจจะวางแผนจะใส่ auth ในอนาคต แต่ยังไม่ได้ใส่จริง
  • หรือ centralize-api อยู่ใน intranet เท่านั้น จึงไม่ต้อง auth (แต่ก็เสี่ยงถ้าใครเข้า intranet ได้)

ดูรายละเอียดใน Volume 5 บท 5.5


Auth flow — end-to-end จาก frontend

[1] User เปิด vtrc-web / vtrc-rc-backoffice

[2] POST /api/graphql { login(...) } (ผ่าน vtrc-api)

[3] vtrc-api ส่งต่อไป cu-central-api:
     POST /auth/signin { username, password, org, apiKey: VTRC_APP }

[4] cu-central-api:
     - checkApiKey
     - หา employee ใน MSSQL
     - bindAdWithUserAndPassword (LDAP)
     - generateSignInToken (JWT cu-central)
     - เก็บ session ใน Redis

[5] cu-central-api ส่ง { accessToken, refreshToken, userData } กลับ vtrc-api

[6] vtrc-api:
     - สร้าง User row (ถ้ายังไม่มี)
     - generateToken (JWT vtrc-api) + เก็บ Session row ใน MariaDB

[7] vtrc-api ส่ง { token, refreshToken, user } กลับ frontend

[8] Frontend เก็บ token ใน localStorage

[9] ทุก request ถัดไป ส่ง Authorization: Bearer + apiKey

JWT 2 ตัว

  • JWT vtrc-api — frontend ถือ ใช้ยิง vtrc-api
  • JWT cu-central-api — vtrc-api ถือ (เก็บใน Session table) ใช้ยิง cu-central-api

frontend ไม่เคยเห็น JWT ของ cu-central-api โดยตรง


Public routes — ไม่ต้อง auth

vtrc-api

GraphQL fields ที่ไม่มี @auth:

DomainFields ตัวอย่าง
Authenticationlogin, loginBackoffice, refreshToken, generateCaptcha, verifyEmpCodeLogin, logout
UserforgetPassword, checkSMSforgetPassword, searchUser, createUser, resetPassword
NewsfetchListPost, createPostRead
HRMIfetchLeaveDocumentList, fetchLeaveDocumentDetail, fetchLeaveDocumentHistory

REST ที่ไม่ต้อง auth:

  • GET /file/:id/download
  • GET /download/excel/:table
  • POST /leave/attachment/draft
  • GET /wdHospital/* (export, report)
  • GET /conicle/*
  • ทุก /fund/*

cu-central-api

GraphQL fields ที่ไม่มี @auth:

DomainFields ตัวอย่าง
HRMIทั้งหมด 26 operations (leave, medical, approver) — public ทั้งหมด
RegistrationcreateAccountByVerifyCode, requestOtp, getVerifyCodeByEmail
User verificationprofileVerification
Master/Reference dataทั้งหมด
FundfetchEmployeeProvidentFund, fetchEmployeeSalary (@auth ถูก comment ออก)

REST:

  • /auth/signin, /auth/signinbypass, /auth/token, /auth/signout — ต้องมี apiKey แต่ไม่ต้องมี JWT
  • /conicle/longtermไม่ต้อง auth เลย (gated ด้วย magic param runLong=20k)

centralize-api

  • ทุก endpoint public เพราะ @Public()

เปรียบเทียบ apiKey 3 แบบ

ServiceapiKey รูปแบบจำนวน keyเก็บที่ไหน
vtrc-apiUUID (36 ตัวอักษร)6 APP_TYPE × 2 env (prod + UAT)config.js (inline)
cu-central-apisecret string (ยาว)2 levelslib/auth/auth.controller.js (inline)
centralize-apiไม่ใช้

ข้อระวังเรื่อง auth

1. apiKey ไม่ใช่ secret จริง ๆ

  • ทุก apiKey ฝังใน frontend bundle — ผู้ใช้สามารถดูได้
  • apiKey เป็นเหมือน client ID — บอกว่า "เป็นแอปไหน" ไม่ใช่ "เป็นใคร"
  • ตัวตนจริงพิสูจน์ที่ JWT + Session เท่านั้น

2. JWT ไม่ได้ถูก verify ทุก request

  • ใน vtrc-api JWT ถูก verify เฉพาะ field ที่มี @auth
  • field ที่ไม่มี @auth ไม่ตรวจ JWT แม้ client จะส่งมา

3. cu-central-api เปิด public เยอะ

  • 26 operations ของ HRMI (leave/medical) เป็น public — ใครมี apiKey ก็ยิงได้
  • อาจเป็นเพราะ vtrc-api เป็น trust boundary แล้ว แต่ก็เสี่ยงถ้ามี client อื่น

4. centralize-api ไม่มี auth จริง

  • ทุก route มี @Public() — ใครใน intranet ก็ยิงได้
  • ระวังถ้า centralize-api เปิดสู่ internet โดยไม่ตั้งใจ

เคล็ดลับตอน debug auth

ถ้าได้ ACCESS_KEY_INVALID

  • ตรวจ apiKey header ว่าถูกต้อง
  • ตรวจว่า origin ของ frontend ตรงกับที่คาดไว้ (เพราะ apiKey เลือกตาม origin)

ถ้าได้ TOKEN_EXPIRED

  • JWT หมดอายุ (default 15m vtrc-api / 20m cu-central-api)
  • client ควร refresh อัตโนมัติ

ถ้าได้ TOKEN_INVALID

  • JWT ไม่ถูกต้อง หรือ session ถูก revoke
  • ลอง logout + login ใหม่

ถ้าได้ SESSION_EXPIRED

  • Session row หมดอายุใน DB (vtrc-api) หรือ Redis (cu-central-api)
  • ต้อง login ใหม่

ถ้าได้ CANNOT_ACCESS

  • JWT ผ่าน แต่ role/menu ไม่ตรง
  • ตรวจว่า role ของ user มีสิทธิ์ใน field นั้น

ขั้นตอนถัดไป

ไป บท 10.3 vtrc-api GraphQL catalog