4 · Auth model + external integrations (deep dive)
บทนี้อธิบาย auth pipeline ของ benefit-api (NestJS JWT guard + @Public() override) และทุก integration ภายนอกที่ยืนยันจากโค้ด — centralize server, vtrc-api, vtrc-common, SFTP/FMIS
Auth architecture overview
benefit-api ใช้ Passport JWT strategy แบบ global guard — ทุก route ถูก guard เว้นแต่ประกาศ @Public() override
HTTP request
→ JwtAuthGuard.canActivate()
├── if @Public() → allow (no JWT check)
└── else → PassportStrategy('jwt').validate(payload)
├── ExtractJwt.fromAuthHeaderAsBearerToken()
├── jwt.verify(token, SECRET_KEY)
└── attach payload to request.user
→ controller → service → entityManagerไม่มี RBAC (role-based access control) ระดับ guard — ถ้า login ได้แล้วเรียก endpoint ที่ไม่ใช่ @Public() ได้ทุกตัว การ authorize ตาม role/menu (ถ้ามี) ต้องทำเองใน service layer แต่จากการอ่าน service ทุกตัวในรอบนี้ ไม่พบ role check ใน service layer เช่นกัน
JwtAuthGuard — global guard
benefit-api/libs/jwt/jwt-auth.guard.ts:1-41 (branch: prod) — อยู่ที่ libs/ ไม่ใช่ src/libs/:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
constructor(private reflector: Reflector) {
super();
}
private logger = new Logger(JwtAuthGuard.name);
private serviceName = '';
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);
}
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException({ statusCode: 401, error: info.message });
}
return user;
}
}
export const isPublicKey = 'isPublic';
export const Public = () => SetMetadata(isPublicKey, true);จุดสำคัญ:
@Public()bypass —reflector.getAllAndOverrideตรวจ metadata ทั้ง handler และ class — ถ้ามี@Public()ที่ method หรือ class → returntrueโดยไม่ verify JWThandleRequest— ถ้า verify fail →UnauthorizedExceptionกับinfo.message(เช่นjwt expired,invalid signature)- ไม่มี role check — comment
// we can protect guard hereที่jwt-auth.guard.ts:30บ่งชี้ว่าทีมเคยคิดจะเพิ่มแต่ยังไม่ทำ - Global registration — ประกาศที่
benefit-api/src/main.ts(หรือapp.module.tsAPP_GUARD provider) ทำให้ทุก controller ถูก guard โดยอัตโนมัติ
JwtStrategy — token validation
benefit-api/libs/jwt/jwt.strategy.ts:1-23 (branch: prod):
@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.data = payload;
return payload;
}
}จุดสำคัญ:
secretOrKey: configService.get('jwtSecretKey')— map จากprocess.env.SECRET_KEYผ่านbenefit-api/config/configuration.ts:5ignoreExpiration: false— ตรวจ expiry จริง (ดีกว่าบาง service ที่ ignore)validate()returnpayloadทั้งก้อน — ไม่ตรวจ field บังคับใด ๆ (issuer, audience, empCode) — ถ้า JWT ลงชื่อด้วย secret เดียวกัน แม้ payload จะเป็นของ service อื่น ก็ผ่าน- ไม่มี
algorithmsระบุ — default ของpassport-jwtยอมรับหลาย algorithm — เปิดช่อง alg-confusion attack ในทางทฤษฎี
Secret source
benefit-api/config/configuration.ts:3-5:
export const configuration = () => ({
port: process.env.PORT,
jwtSecretKey: process.env.SECRET_KEY,
// ...SECRET_KEY อยู่ใน .env ที่ commit อยู่ใน repo (ดู SEC-BENEFIT-01) — shared secret กับ service อื่นหรือไม่ยัง UNVERIFIED แต่ pattern ชื่อ SECRET_KEY เหมือนกับหลาย NestJS service ใน platform
@Public() endpoints — catalog ครบ
ทุก endpoint ที่ bypass auth (ยืนยันจาก rg "@Public()" src/):
Critical — ควรมี auth แต่ไม่มี
| Controller | Method | Path | Impact |
|---|---|---|---|
WdhospitalsController | POST | /wdHospitals/add | สร้างรายการเบิกค่ารักษาพยาบาลได้โดยไม่ login |
WdhospitalsController | POST | /wdHospitals/update/:status | เปลี่ยนสถานะเอกสารเบิก (approve/cancel) ได้โดยไม่ login |
WdhospitalsController | GET | /wdHospitals/detail/:id | อ่านข้อมูลส่วนบุคคล (identity card, บัญชีธนาคาร) ได้โดยไม่ login |
ManualBatchController | POST | /batchJob/importFuneral | trigger batch import funeral โดยไม่ auth |
ManualBatchController | POST | /batchJob/importInheritance | trigger batch import inheritance โดยไม่ auth |
ManualBatchController | POST | /batchJob/updateFuneral | update funeral ไป HRMI โดยไม่ auth |
ManualBatchController | POST | /batchJob/updateInheritance | update inheritance ไป HRMI โดยไม่ auth |
UploadController | GET | /uploadFiles/:id | อ่านไฟล์แนบโดยไม่ auth |
UploadController | POST | /uploadFiles | อัปโหลดไฟล์โดยไม่ auth |
ExportPaymentReportController | POST | /exportPaymentReport/exportReportByOrderExcel | export Excel report โดยไม่ auth |
หลักฐาน — benefit-api/src/modules/wdhospitals/wdhospitals.controller.ts:24-40:
@Public()
@Post('/add')
insertWdHospitals(@Body() body: RequestWdHospitalsDto) {
return this.service.addWdHospitals(body);
}
@Public()
@Post('/update/:status')
updateWdHospitals(@Param() status: WdHospitalsStatusEnumDto, @Body() body: UpdateWdHospitalsDto) {
return this.service.updateWdHospitals(status.status, body);
}
@Public()
@Get('/detail/:id')
getWdHospitalsById(@Param() param: GetWdHospitalsDto) {
return this.service.getWdHospitalsById(param.id);
}Medium — dropdown / health (อาจตั้งใจเปิด)
| Controller | Method | Path | หมายเหตุ |
|---|---|---|---|
DropdownController | GET | /dropdown/get_dropdown_by_type | master data — อาจตั้งใจ public |
DropdownController | GET | /dropdown/getWelfareTypeList | master data |
DropdownController | GET | /dropdown/getWelfareTypeListByEmp | ต้องการ empCode — ควรมี auth |
DropdownController | POST | /dropdown/getCOAList | chart of account |
DropdownController | POST | /dropdown/getDropDownList | generic dropdown |
AppController | GET | /health | health check — ตั้งใจ public |
AppController | GET | /ping-centrail | ping external URL — อันตราย (SSRF risk) |
AppController | GET | /testProfile | test JWT decode — debug endpoint ค้างใน prod |
Commented-out @Public() — landmine
| Controller | Path | สถานะ |
|---|---|---|
PaymentsController | POST createExportWDHospital | @Public() ถูก comment — ปัจจุบันมี auth |
UploadController | PUT/DELETE | @Public() ถูก comment — ปัจจุบันมี auth |
ถ้า uncomment โดยไม่ตั้งใจ → endpoint กลับเป็น public อีกครั้ง
External integrations
1. Centralize server (NestJS REST)
| Field | Value |
|---|---|
| env var | CENTRALIZE_SERVER → centralizeServer |
Base URL (จาก .env) | http://10.188.128.60:3100/ |
| เรียกจาก | approvedisaster.service.ts:185-197, payments.service.ts:127, disbursement.service.ts:120,588 |
| Endpoints ที่เรียก | api/employee/getEmployeeDisaster, api/employee/getByEmpCode, api/employee/bankAccount |
| ใช้ทำอะไร | ดึงข้อมูลพนักงาน + เลขบัญชีธนาคาร |
หมายเหตุชื่อ centralizeServer — port 3100 ไม่ตรงกับ cu-central-api default 8080 หรือ NestJS centralize-api default 3000 — จาก path (api/employee/...) มีรูปแบบคล้าย REST ของ NestJS centralize-api มากกว่า GraphQL ของ cu-central-api แต่ UNVERIFIED 100% ว่าปลายทางจริงคือ repo ไหน
2. vtrc-api (legacy core) — dual coupling
| Field | Value |
|---|---|
| env var | VTRC_SERVER → vtrcServer |
| Base URL | https://vtrcapi.redcross.or.th/ |
| HTTP calls | approvedisaster.service.ts:389 (listTermFeeForApprover), payments.service.ts:589,737 (cancelStudyEducation) |
| DB coupling | raw SQL เข้าตาราง vtrc.dbo.WDHospitals / WDHistories ผ่าน vtrcEntityManager |
นี่คือ coupling แบบสองทาง (API + database) ระหว่าง domain microservice กับ legacy core — ไม่ตรงกับสมมติฐานที่ว่า domain microservice ไม่ควรพึ่งพา vtrc-api
3. vtrc-common (NestJS file service)
| Field | Value |
|---|---|
| env var | VTRC_COMMON_SERVER |
| Base URL | http://vtrc-common:9310/ |
| เรียกจาก | wdhospitals.service.ts:67, disbursement.service.ts:143,161,605,639,645, payments.service.ts:902 |
| Endpoints | file/save, file/delete, file/getFileArray |
| ใช้ทำอะไร | เก็บ/ลบ/ดึงไฟล์แนบ |
4. SFTP server (FMIS export)
| Field | Value |
|---|---|
| Config | hardcode ในโค้ด (ไม่ผ่าน env) |
| Host | 10.188.128.22:22 |
| เรียกจาก | exportpaymentreport.service.ts:5267-5274, 5820-5827, 6146-6152 |
| Paths | upload/fmis/bgdoc/, upload/fmis/ledger/, shared/fmis/ |
| ใช้ทำอะไร | อัปโหลด/ดาวน์โหลดไฟล์ JSON บัญชี FMIS |
SFTP password hardcode ในโค้ด 3 จุด — ดู SEC-BENEFIT-01
Triple DB connection
benefit-api เชื่อม 3 database พร้อมกัน (configuration.ts:8-61):
| Connection | env prefix | Schema | ใช้ทำอะไร |
|---|---|---|---|
db (main) | DB_* | benefit | entity หลักของ benefit-api |
dbHRMI | DB_HRMI_* | HRMI | ดึง profile พนักงานจาก HRMI |
vtrcDB | VTRC_DB_* | vtrc | raw SQL เข้า WDHospitals / MasterApprovers |
ทั้ง 3 password อยู่ใน .env ที่ commit — ดู SEC-BENEFIT-01
Secret ที่หลุดเข้า source control
พบ credential จริงที่ commit อยู่ใน repo — ไม่แสดงค่าจริงในเอกสารนี้ ตามนโยบาย:
| ประเภท secret | ไฟล์ | บรรทัด |
|---|---|---|
| MSSQL password (main/hrmi/vtrc ×3) | benefit-api/.env (branch: prod) | 18, 25, 32 |
JWT secret (SECRET_KEY) | benefit-api/.env (branch: prod) | 64 |
| SFTP password (FMIS) | exportpaymentreport.service.ts | 5271, 5824, 6151 (ซ้ำ 3 จุด) |
Auth vs other NestJS services
| ด้าน | benefit-api | centralize-api | sso-api |
|---|---|---|---|
| Guard | JwtAuthGuard global | JwtAuthGuard + @Public() เกือบทุก route | JWT stub (issue commented) |
| Secret env | SECRET_KEY | (ดู Vol 3.3) | (ดู Vol 6) |
| RBAC | ไม่มี | ไม่มี | ไม่มี |
@Public() count | ~20 endpoints | เกือบทั้งหมด | — |
| Dangerous public | wdHospitals CRUD, batchJob, upload | (ดู Vol 3.3) | — |
ขั้นตอนถัดไป
ไป บท 5 Cron jobs → หรือ บท 6 Scorecard →