Skip to content

4.4 · @auth directive + bypass

บทนี้เจาะ @auth directive ของ cu-central-api ซึ่งมี complexity ที่ vtrc-api ไม่มี — accessLevel + level system, ignoreAuth bypass ที่ใช้ apiKey แทน JWT, และ requires role check ที่ commented out


Directive declaration

4:4:main-api/src/typeDefs/root.js
  directive @auth(requires: [String], accessLevel: Int, ignoreAuth: Boolean) on FIELD_DEFINITION

3 arguments

ArgTypeหน้าที่
requires[String]role names ที่จำเป็น — commented out ใน implementation
accessLevelIntAPI key level ขั้นต่ำ (1=general, 4=admin)
ignoreAuthBooleanข้าม JWT verification — ใช้ apiKey อย่างเดียว

Implementation

8:75:main-api/src/directives/auth.js
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition (field) {
    const { resolve = defaultFieldResolver } = field
    const { requires, accessLevel, ignoreAuth } = this.args

    field.resolve = async function (...args) {
      const [, , context] = args
      const { clientAppInfo } = context

      // 1. apiKey check
      if (typeof clientAppInfo === 'undefined') {
        throw new AuthenticationError('NOT_AUTHORIZED')
      }

      // 2. accessLevel check
      if (accessLevel > 0 && clientAppInfo.level < accessLevel) {
        throw new AuthenticationError('NOT_AUTHORIZED')
      }

      // 3. ignoreAuth bypass
      if (ignoreAuth === true) {
        const data = await resolve.apply(this, args)
        return data
      }

      // 4. JWT verify + session check
      // ... verifyToken, isLoggedIn ...

      // 5. requires check — COMMENTED OUT
      // if (requires) { ... }

      const data = await resolve.apply(this, args)
      return data
    }
  }
}

flow มี 5 gate

  1. apiKey checkclientAppInfo มาจาก checkApiKey(req.headers.apikey) ใน context builder (4.3)
  2. accessLevel check — เทียบ clientAppInfo.level กับ directive arg
  3. ignoreAuth bypass — ถ้า true ข้ามไป resolve โดยตรง
  4. JWT verify + session check — เกิดเฉพาะถ้าไม่ใช่ bypass
  5. requires checkcommented out ทั้ง block RBAC ไม่ทำงาน

Hardcoded apiKey table

253:274:main-api/src/lib/auth/auth.controller.js
export const checkApiKey = (apiKey) => {
  if (typeof apiKey === 'undefined') {
    return apiKey
  }
  // access key level 1 is general, 4 is able to search users.
  const allowedApiKeys = [
    {
      apiKey: 'OGJlYTEzOTBjMzk3NjlmYWQ1ZTcxZDZhN2NhZWMyYTk3YTQ2NGIzMDhiMGM4NmRjM2NlNjZkM2I2ZDIyOGQyMQ',
      appId: 'acwfa3t-wafbAS-fawkgt3948yf-egrhe3fwe',
      appName: 'VTRC_APP',
      level: 1
    },
    {
      apiKey: '562B132B1E87695A052717C20336CDA57FBC4C5B19EF31356CC07C5A43656DE47DEFE452D241C3F443DE54',
      appId: '7d962c5b-68d1-4a8f-849d-4d617711d94b',
      appName: 'VTRC_ADMIN_APP',
      level: 4
    }
  ]

  return allowedApiKeys.find(key => key.apiKey === apiKey)
}

🔴 Critical — 2 apiKeys commit เป็น literal ใน code

KeyAppLevel
OGJlYTEzOTBj...VTRC_APP1 (general)
562B132B1E87...VTRC_ADMIN_APP4 (admin — search users ได้)

ต่างจาก vtrc-api ที่เก็บใน config.js (API_DEVICE_KEY) — cu-central-api hardcode ใน controller เลย

level system

Levelความหมายตัวอย่าง field
1general (VTRC_APP)query ข้อมูลตัวเอง
4admin (VTRC_ADMIN_APP)searchUser — search employee อื่น

@auth(accessLevel: 4) บน field บังคับว่าต้องใช้ admin apiKey


ignoreAuth bypass pattern

searchProfile.js:12
searchUser @auth(accessLevel: 4, ignoreAuth: true)

การใช้งาน @auth(accessLevel: 4, ignoreAuth: true) หมายถึง

  • ต้องมี admin apiKey (level 4)
  • แต่ข้าม JWT verification + session check ทั้งหมด

คือ "apiKey อย่างเดียวพอ" — ไม่ต้อง login เป็น user

Use case ที่น่าจะเป็น

endpoint นี้อาจออกแบบมาสำหรับ service-to-service call — admin service ที่ถือ apiKey แล้วอยาก search user โดยไม่ต้อง maintain session ของตัวเอง

ความเสี่ยง

ถ้า apiKey level 4 รั่ว (และมัน commit อยู่ใน code อยู่แล้ว) — attacker เรียก searchUser ได้ทันทีโดยไม่ต้องผ่านขั้นตอน login ใด ๆ


requires — commented out

48:53:main-api/src/directives/auth.js
// if (requires) {
//   const role = ...
//   if (!requires.includes(role)) {
//     throw new AuthenticationError('NOT_AUTHORIZED')
//   }
// }

RBAC role check ที่ประกาศใน schema (requires: [String]) ไม่ถูก implement — ทุก field ที่ใช้ @auth(requires: [...]) จะผ่านเสมอ

Impact

  • ไม่มี RBAC จริงในระบบ
  • การ authorize พึ่ง apiKey level + JWT verification เท่านั้น
  • ถ้ามีคนวาง @auth(requires: ['ADMIN']) เพื่อจำกัด field — มันไม่ทำงาน

Fix ที่ควรทำ

  • ลบ requires arg ออกจาก directive declaration (clean up schema)
  • หรือ implement ให้ครบ + เพิ่ม test matrix

Context builder

36:72:main-api/src/index.js
  context: async ({ req, res }) => {
    const clientAppInfo = checkApiKey(req.headers.apikey)

    const tokenWithBearer = req.headers.authorization || ''
    const token = tokenWithBearer.split(' ')[1]

    let data = await decodeJwtToken(token)
    data = data.data
    let userData = null
    if (data.userCredential) {
      userData = await getSessionFromTokenCredential(data.userCredential)
    }
    // ... return { clientAppInfo, token, userData, ... }
  }

context builder ส่ง clientAppInfo ให้ directive — แต่ ไม่ throw ถ้า apiKey invalid — gate นั้นอยู่ที่ directive เอง

Bug — context ไม่ fail-fast

ถ้า apiKey invalid (clientAppInfo === undefined) context ยังคง return ปกติ — directive ถึงจะ throw ทีหลัง ทำให้ logging ของ request ที่ invalid ไม่ครบถ้วน


JWT verification

ใน directive (gate 4 หลัง ignoreAuth bypass)

verifyToken(token)
  ├── jwt.verify(token, JWT_SECRET, { issuer: JWT_ISSUER })
  └── return { data, error }

ไม่มี algorithms: ['HS256'] allow-list — เหมือน vtrc-api (บท 9.3)


Session check (isLoggedIn)

หลัง JWT verify แล้ว directive ตรวจ session ใน Redis

isLoggedIn(verify.data.uid, verify.data.sid)
  └── getSessionFromTokenCredential → Redis GET

ถ้า session ไม่อยู่ → throw

Session lookup ไม่มี JSON scan

ต่างจาก vtrc-api ที่ใช้ LIKE '%jwtid%' scan JSON blob (บท 9.3 PERF-3) — cu-central-api lookup ผ่าน key pattern SESSION:<prefix>:<uid>_<sid> โดยตรง — ทำงานเร็วกว่า


เปรียบเทียบกับ vtrc-api @auth

Aspectvtrc-apicu-central-api
ArgsaccessRole, accessMenurequires, accessLevel, ignoreAuth
apiKey ที่ใช้จาก config (7 prod keys)hardcoded 2 keys
RBACgetCurrentRoleUser + checkMenuAuth (มี bug CORR-4)requires commented out — ไม่ทำงาน
Bypassไม่มี🔴 ignoreAuth: true สำหรับ apiKey-only
Session lookupLIKE '%jwtid%' JSON scanRedis key pattern (ดีกว่า)
Default-denyไม่มี (field ที่ลืม @auth = public)ไม่มี (เหมือนกัน)

ตารารางสรุปช่องโหว่

#ปัญหาSeverityFile:line
1apiKeys hardcoded🔴 Criticalauth.controller.js:253-274
2ignoreAuth bypass🔴 Criticaldirectives/auth.js:32-35 + searchProfile.js:12
3requires commented out🟡 Highdirectives/auth.js:48-53
4ไม่มี algorithms allow-list🟡 HighverifyToken impl
5ไม่มี default-deny🟡 Highdirective เฉพาะ field ที่ annotated
6/auth/signinbypass endpoint🔴 Criticalroutes.js:27-31

อ่านต่อ → 4.5 Multi-tenant SourceDB sharding