6.5 · Auth pipeline (JWT guard + SECRET_KEY)
บทนี้อธิบาย auth pipeline ของ benefit-api ซึ่งใช้ NestJS Passport JWT strategy เป็น global guard — แต่ bypass ด้วย @Public() decorator เยอะมาก
libs/jwt/jwt.strategy.ts:7-23 — strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
private logger = new Logger(JwtStrategy.name);
data: any;
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('jwtSecretKey'),
});
}
async validate(payload: any) {
// this.logger.log(`JwtStrategy payload -> ${JSON.stringify(payload)} `);
this.data = payload;
return payload;
}
}จุดที่สังเกต
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()— รับ JWT จากAuthorization: Bearer <token>headerignoreExpiration: false— ตรวจexpclaim (good)secretOrKey: configService.get('jwtSecretKey')— โหลดจาก config ซึ่ง map ไปยังSECRET_KEYใน.env:64validate(payload: any)— return payload ทั้งหมดกลับไปเป็นreq.user(no further check)data: anyfield ของ strategy class เป็น anti-pattern — เก็บ state ล่าสุด ทำให้ strategy เป็น stateful (แต่เป็น singleton ใน NestJS จึงไม่ใช่ race condition ร้าย แค่ dirty)
libs/jwt/jwt-auth.guard.ts:6-41 — global guard + @Public()
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(isPublicKey, [context.getHandler(), context.getClass()]);
const request = context.switchToHttp().getRequest();
this.serviceName = request.url;
if (isPublic) {
return true;
}
return super.canActivate(context);
}@Public() decorator — jwt-auth.guard.ts:41
export const Public = () => SetMetadata(isPublicKey, true);Decorator นี้ตั้ง metadata isPublicKey: true บน method/class — guard เช็คก่อนว่า method นี้ mark @Public() ไว้หรือไม่ ถ้าใช่ return true (skip auth)
ทำให้ developer สามารถ bypass auth ทุกกรณีด้วยการเพิ่ม @Public() บน controller method — ซึ่งในปัจจุบันมีการใช้งานเยอะมาก (ดู 6.3 และ 6.8 S6)
SECRET_KEY source — .env:64
SECRET_KEY=AAAAB3NzaC1yc2EAAAADAQABAAABAQCMH5xOB6PH3INC7ScxkR2b3IChoJJDpCcQqgwwtzKxqkXBTUNqb+qE20Z2WJcnca9rPTjiuahlOChjRo8mELybKZt65NrWHYbj2hv4Yliybz8xPZJC9KIT3hlcwDv5jqqUmeDRNk1MUEn1SNXJDquaerv1vQU5EZYw0/hXYNc2Y+hqltWcdPCJ12cpBO9VXnOvtGWLzufMLOHTsupaPqcjRpid5xI7tx/cQZvSKw8gUA/9UNlX/XhSToU72NEQLpxBFyGhRqPYJQprvFpqdGOmhIzjsSTut5cKdxqYmsWUbGZgVVgEzLBdA/JXrCLWXoiLaohNciqvG/xdnzyKldJbปัญหาเร่งด่วน
ค่านี้คือ SSH RSA public key (prefix AAAAB3NzaC1yc2EAAAADAQABAAABAQ... เป็น base64-encoded SSH wire format) — ไม่ใช่ JWT secret
ผลกระทบ:
- ถ้า JWT signing party ใช้ private key ที่ corresponding กับ key นี้ด้วย algorithm RS256 — config นี้ผิด เพราะ
passport-jwtต้องการ PEM-format public key ไม่ใช่ raw base64 SSH format อาจ fail verification หรือ fallback เป็น HMAC - ถ้าทั้ง signer และ verifier ใช้ string นี้เป็น HMAC secret — ก็คือใช้ SSH pubkey ซึ่งเป็น public ตามนิยาม เป็น anti-pattern ใครที่เคยอ่าน repo (หรือ git history) ก็สามารถ forge token ได้
- กรณีใดก็ตาม secret ถูก commit เข้า git → ต้องถือว่าถูก compromise แล้ว
ดู 6.8 S1 สำหรับ remediation
JWT payload schema — src/dto/user.dto.ts:1-9
export interface UserType {
id: string;
uid: string;
profile: ProfileType;
iat: number;
exp: number;
iss: string;
jti: string;
}ProfileType (บรรทัด 11-40) มี field ประมาณ 30 ตัว:
empCode,identityCard,fullName,firstName,lastNameorgCode,diviCode,deptCode,positionCodesourceDB,employeeLevelCode- ฯลฯ
คือ employee snapshot ทั้งหมดฝังใน token — ทำให้ token มีขนาดใหญ่ และเก็บ PII ที่ sensitive
Token expiry
| รายการ | ค่า |
|---|---|
ignoreExpiration | false — ตรวจ exp |
expiresIn (การ sign) | ไม่ได้ตั้งใน benefit-api |
benefit-api ไม่ได้ issue token — ทำหน้าที่ verify เท่านั้น Token issuance อยู่ที่ upstream service (น่าจะเป็น SSO ของ vtrc-api หรือ service อื่น) ค่า exp claim จึงขึ้นกับ upstream
Flow เมื่อ request เข้ามา
HTTP request
└─ Authorization: Bearer <jwt>
└─ JwtAuthGuard.canActivate()
├─ เช็ค @Public() metadata?
│ ├─ YES → return true (skip auth)
│ └─ NO → super.canActivate() (Passport JWT)
│ ├─ ExtractJwt.fromAuthHeaderAsBearerToken()
│ ├─ verify(jwt, SECRET_KEY)
│ │ ├─ invalid/expired → 401 Unauthorized
│ │ └─ valid → JwtStrategy.validate(payload)
│ │ └─ return payload (เป็น req.user)
│ └─ req.user พร้อมใช้ใน controller
└─ controller.method(@Req() req)จุดที่ทำให้ auth ไม่มีประสิทธิภาพ
@Public()ใช้เยอะ — มี endpoint mutation หลายตัวที่@Public()(ดู 6.3) → auth guard ไม่ทำงานเลย- SECRET_KEY เป็นค่าสาธารณะ — ถือว่าถูก compromise → JWT verification ไม่มีความหมาย
- ไม่มี role check —
validate()return payload ทั้งหมด ไม่มีการเช็คว่า user มี role ที่เหมาะสมกับ endpoint นั้นหรือไม่ - ไม่มี audit log ของ auth event —
this.logger.log(...)ในvalidateถูก comment ไว้ (jwt.strategy.ts:18)
เปรียบเทียบกับ auth ของ service อื่น
| Service | Auth รูปแบบ | หมายเหตุ |
|---|---|---|
| benefit-api | JWT (Passport) + @Public() bypass | secret ถูก commit, @Public() เยอะ |
| centralize-api (Vol 5) | JWT guard + @Public() บนทุก route ปัจจุบัน | auth ไม่ทำงานเลย |
| vtrc-api (Vol 3) | JWT + @auth(accessRole, accessMenu) directive | มี role-based ผ่าน GraphQL directive |
| cu-central-api (Vol 4) | LDAP/AD + @auth directive | sourceDB sharding |
ดู 6.9 สำหรับการเปรียบเทียบเชิงลึก