11.2 · Auth & headers reference
บทนี้รวบรวม header, apiKey, JWT, และ auth flow ของทั้ง 3 services เพื่อให้เวลาเขียน client หรือ debug สามารถเช็คได้ในที่เดียว
ภาพรวม — แต่ละ service ใช้ auth อะ่างไร
| Service | apiKey header | Authorization header | Auth mechanism |
|---|---|---|---|
| vtrc-api | apiKey: <uuid> | Authorization: Bearer <jwt> | JWT (HS256) + apiKey (6 APP_TYPE) |
| cu-central-api | apiKey: <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/jsonGraphQL multipart (upload) เพิ่ม Content-Type: multipart/form-data
apiKey — Device key mapping
apiKey เป็นตัวบอกว่า request มาจากแอปไหน — ฝังใน config.js:41-149
| APP_TYPE | APP_NAME (prod) | ใช้โดย |
|---|---|---|
WEB | VTRC-WEB | vtrc-web frontend |
MOBILE | VTRC-MOBILE | mobile app |
WEB_ADMIN | VTRC-BACKOFFICE | vtrc-rc-backoffice frontend |
INTEGRATE_APP | INFOMA / CONICLE | บุคคลที่สาม (integration) |
TWOWAY | VTRC-TWOWAY | Two-way feedback sub-app |
MEETING | VTRC-MEETING | Meeting sub-app |
ที่มา: vtrc-api/api/src/config.js:42-84 (prod), 86-148 (UAT — มี LOAD_TEST_* เพิ่ม)
apiKey validation
// ใน 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_TYPEJWT — issue + verify
Issue (login)
// 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)
// 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
# 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 ตรง
ตัวอย่าง
# 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
// config.js:150-151
ACCESS_BACK_OFFICE = ['SUPER_ADMIN', 'ADMIN', 'STAFF']
ACCESS_USER = ['USER']Menu keys (accessMenu)
รายการ menu key อยู่ใน config.js:159-370 — ตรงกับ MENUS_BACK_OFFICE — ใช้เฉพาะใน wdHospital.js (4 fields) ในปัจจุบัน
Session
นอกจาก JWT แล้ว vtrc-api ยังเก็บ session ใน DB ตาราง Session — ทำให้สามารถ revoke token ได้กลางวัน
// 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
# typeDefs/authentication.js
refreshToken(token: String, refreshToken: String): AccessTokensflow:
- client ส่ง token เก่า + refreshToken
- server ตรวจ refreshToken ใน DB
- issue JWT + refreshToken ใหม่
- 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
// 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 }
]| appName | level | ใช้โดย |
|---|---|---|
VTRC_APP | 1 | vtrc-api (ฝั่ง user) |
VTRC_ADMIN_APP | 4 | vtrc-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
// 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
# typeDefs/root.js:4
directive @auth(requires: [String], accessLevel: Int, ignoreAuth: Boolean) on FIELD_DEFINITIONaccessLevel— เช็คclientAppInfo.level >= accessLevelignoreAuth: 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:
// src/main.ts:27
app.useGlobalGuards(new JwtAuthGuard(reflector))// 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():
// 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 + apiKeyJWT 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:
| Domain | Fields ตัวอย่าง |
|---|---|
| Authentication | login, loginBackoffice, refreshToken, generateCaptcha, verifyEmpCodeLogin, logout |
| User | forgetPassword, checkSMSforgetPassword, searchUser, createUser, resetPassword |
| News | fetchListPost, createPostRead |
| HRMI | fetchLeaveDocumentList, fetchLeaveDocumentDetail, fetchLeaveDocumentHistory |
REST ที่ไม่ต้อง auth:
GET /file/:id/downloadGET /download/excel/:tablePOST /leave/attachment/draftGET /wdHospital/*(export, report)GET /conicle/*- ทุก
/fund/*
cu-central-api
GraphQL fields ที่ไม่มี @auth:
| Domain | Fields ตัวอย่าง |
|---|---|
| HRMI | ทั้งหมด 26 operations (leave, medical, approver) — public ทั้งหมด |
| Registration | createAccountByVerifyCode, requestOtp, getVerifyCodeByEmail |
| User verification | profileVerification |
| Master/Reference data | ทั้งหมด |
| Fund | fetchEmployeeProvidentFund, fetchEmployeeSalary (@auth ถูก comment ออก) |
REST:
/auth/signin,/auth/signinbypass,/auth/token,/auth/signout— ต้องมี apiKey แต่ไม่ต้องมี JWT/conicle/longterm— ไม่ต้อง auth เลย (gated ด้วย magic paramrunLong=20k)
centralize-api
- ทุก endpoint public เพราะ
@Public()
เปรียบเทียบ apiKey 3 แบบ
| Service | apiKey รูปแบบ | จำนวน key | เก็บที่ไหน |
|---|---|---|---|
| vtrc-api | UUID (36 ตัวอักษร) | 6 APP_TYPE × 2 env (prod + UAT) | config.js (inline) |
| cu-central-api | secret string (ยาว) | 2 levels | lib/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
- ตรวจ
apiKeyheader ว่าถูกต้อง - ตรวจว่า 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 นั้น