Skip to main content

การเข้าสู่ระบบและ JWT (Auth)

ภาพรวม

โมดูล auth เป็นประตูทางเข้าของ cms-web ทั้งหมด และทำหน้าที่สองอย่างพร้อมกัน

  1. ให้ endpoint สำหรับ login, เลือก LINE OA, ต่ออายุ token และเพิกถอน token
  2. ให้ global JWT guard ที่ router นำไปครอบ route group authed ของทุกโมดูล

จุดสำคัญของระบบนี้คือการ login เป็น สองขั้น ขั้นแรกผู้ใช้ได้ token ที่ยังไม่ผูกกับ LINE OA ขั้นที่สองผู้ใช้เลือก OA แล้วจึงได้ token ที่มี lineOaId อยู่ใน claim ทำให้ route ส่วนใหญ่ที่ต้องการ tenant scope สามารถบังคับได้ว่าผู้ใช้ต้องเลือก OA ก่อนเสมอ

Business Flow

  1. POST /api/auth/login — ตรวจสอบ email และ password ด้วย bcrypt แล้วออก access token พร้อม refresh token โดย token ขั้นนี้ยังไม่มี claim lineOaId
  2. cms-web แสดงรายการ LINE OA ที่ผู้ใช้เข้าถึงได้ ให้ผู้ใช้เลือกหนึ่ง OA
  3. POST /api/auth/login-with-line-oa ผ่าน guard JwtLoginAuth ซึ่งยอมรับ token ที่ยังไม่มี lineOaId จากนั้นออก token ชุดใหม่ที่มี lineOaId, lineOaHash, organizationId และ roleId หาก OA นั้นยังไม่มี lineOaHash ระบบจะ generate ให้แล้วเขียนกลับลงตาราง line_oa เป็นการ backfill
  4. token ทุกใบที่ออกจะถูก cache ไว้ใน Redis hash ชื่อ h_session:<userId> โดยเก็บเพียง 15 ตัวอักษรแรกของ SHA256(token) ไว้ในฟิลด์ accessToken และ refreshToken
  5. ทุก request ที่เข้ามาที่ group authed จะผ่าน JwtAuth ซึ่งจะ verify signature, ตรวจว่าต้องมี lineOaId, เติมค่าลง CLS แล้วเทียบ hash กับที่เก็บใน Redis หาก hash ไม่ตรง (ถูก revoke หรือมีการ login ที่อื่นทับ) จะตอบ 401 พร้อม code APP_001
  6. POST /api/auth/refresh-access-token ผ่าน guard RefreshTokenAuth ต้องใช้ refresh token ที่ยังอยู่ใน Redis และต้องมี claim nbf จึงจะออก access token ใหม่ให้
  7. POST /api/auth/revoke-token ลบ key h_session:<userId> ทำให้ token ทุกใบของผู้ใช้รายนั้นใช้งานไม่ได้ทันที

ไฟล์และฟังก์ชันหลัก

โค้ดอยู่ที่ internal/modules/auth/ ประกอบด้วย controller.go, service.go, guards.go, jwt.go, hash.go, lineoa.go, oaaccess.go และ module.go

route ของโมดูลนี้ถูก register จาก cmd/api/main.go โดยตรง ไม่ได้อยู่ใน featureModules เพราะโมดูลนี้ต้องให้ guard กลางแก่ router ด้วย

MethodRouteHandlerGuard
POST/api/auth/loginm.loginไม่มี (public)
POST/api/auth/login-with-line-oam.loginWithLineOaJwtLoginAuth
POST/api/auth/refresh-access-tokenm.refreshAccessTokenRefreshTokenAuth
POST/api/auth/revoke-tokenm.revokeTokenไม่มี (มี TODO เดิมระบุว่ายังไม่ได้ตรวจ admin role)

Guard ที่ export ให้โมดูลอื่นใช้ (internal/modules/auth/guards.go)

  • (*Module).JwtAuth() — global guard ของ group authed บังคับว่าต้องมี lineOaId
  • (*Module).JwtLoginAuth() — guard สำหรับ route ที่ยังไม่ได้เลือก OA
  • (*Module).RefreshTokenAuth() — guard ของ refresh flow
  • auth.SuperAdmin() — บังคับว่า roleId ต้องเท่ากับ 1
  • auth.InternalApiKey() — ตรวจ header X-Internal-Key เทียบกับ environment INTERNAL_API_KEY

ฟังก์ชันอื่นที่โมดูลอื่นเรียกใช้ ได้แก่ auth.GenerateLineOaHash, auth.GenerateShortHash และ interface app.AuthTokenService (IsAllowToken, RevokeToken)

จุดเชื่อมต่อกับ Service อื่น

  • Permission — ตัว route ไม่ต้องการ permission เพราะเป็น public แต่โมดูลนี้เป็นตัวเติมค่าลง CLS ที่ระบบ permission ทั้งหมดพึ่งพา
  • ตารางuser, line_oa, user_line_oa (สิทธิ์เข้าถึง OA รายบุคคล) และ system_role
  • Redis — hash h_session:<userId> สำหรับเก็บ token hash โดยใช้ namespace ตาม REDIS_NAMESPACE
  • Environment สำคัญJWT_SECRET (ถ้าไม่มีระบบจะ boot ไม่ผ่าน) และ INTERNAL_API_KEY
  • Error code — 401 ตอบ APP_001 ส่วน refresh ที่ล้มเหลวตอบ 400 พร้อม APP_002 หรือ APP_003
  • เอกสารที่เกี่ยวข้อง — สิทธิ์ผู้ใช้ Permission และ Module Gate, และ User Management