Skip to content

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

7:23:benefit-api/libs/jwt/jwt.strategy.ts
@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> header
  • ignoreExpiration: false — ตรวจ exp claim (good)
  • secretOrKey: configService.get('jwtSecretKey') — โหลดจาก config ซึ่ง map ไปยัง SECRET_KEY ใน .env:64
  • validate(payload: any) — return payload ทั้งหมดกลับไปเป็น req.user (no further check)
  • data: any field ของ strategy class เป็น anti-pattern — เก็บ state ล่าสุด ทำให้ strategy เป็น stateful (แต่เป็น singleton ใน NestJS จึงไม่ใช่ race condition ร้าย แค่ dirty)

libs/jwt/jwt-auth.guard.ts:6-41 — global guard + @Public()

15:24:benefit-api/libs/jwt/jwt-auth.guard.ts
  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

41:41:benefit-api/libs/jwt/jwt-auth.guard.ts
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

ผลกระทบ:

  1. ถ้า JWT signing party ใช้ private key ที่ corresponding กับ key นี้ด้วย algorithm RS256 — config นี้ผิด เพราะ passport-jwt ต้องการ PEM-format public key ไม่ใช่ raw base64 SSH format อาจ fail verification หรือ fallback เป็น HMAC
  2. ถ้าทั้ง signer และ verifier ใช้ string นี้เป็น HMAC secret — ก็คือใช้ SSH pubkey ซึ่งเป็น public ตามนิยาม เป็น anti-pattern ใครที่เคยอ่าน repo (หรือ git history) ก็สามารถ forge token ได้
  3. กรณีใดก็ตาม secret ถูก commit เข้า git → ต้องถือว่าถูก compromise แล้ว

ดู 6.8 S1 สำหรับ remediation


JWT payload schema — src/dto/user.dto.ts:1-9

1:9:benefit-api/src/dto/user.dto.ts
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, lastName
  • orgCode, diviCode, deptCode, positionCode
  • sourceDB, employeeLevelCode
  • ฯลฯ

คือ employee snapshot ทั้งหมดฝังใน token — ทำให้ token มีขนาดใหญ่ และเก็บ PII ที่ sensitive


Token expiry

รายการค่า
ignoreExpirationfalse — ตรวจ 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 ไม่มีประสิทธิภาพ

  1. @Public() ใช้เยอะ — มี endpoint mutation หลายตัวที่ @Public() (ดู 6.3) → auth guard ไม่ทำงานเลย
  2. SECRET_KEY เป็นค่าสาธารณะ — ถือว่าถูก compromise → JWT verification ไม่มีความหมาย
  3. ไม่มี role checkvalidate() return payload ทั้งหมด ไม่มีการเช็คว่า user มี role ที่เหมาะสมกับ endpoint นั้นหรือไม่
  4. ไม่มี audit log ของ auth eventthis.logger.log(...) ใน validate ถูก comment ไว้ (jwt.strategy.ts:18)

เปรียบเทียบกับ auth ของ service อื่น

ServiceAuth รูปแบบหมายเหตุ
benefit-apiJWT (Passport) + @Public() bypasssecret ถูก 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 directivesourceDB sharding

ดู 6.9 สำหรับการเปรียบเทียบเชิงลึก


ไปต่อ