express-openapi-validator

 

์•ˆ๋…•ํ•˜์„ธ์š”! express-openapi-validator์˜ HTTP ์ธ์ฆ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด PR์„ ์ œ์ถœํ–ˆ๋˜ ๊ณผ์ •์„ ๊ณต์œ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ด์ „ ํฌ์ŠคํŠธ์—์„œ express-openapi-validator๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ฐœ๊ฒฌํ•œ ์ธ์ฆ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ๋‹ค๋ค˜์—ˆ๋Š”๋ฐ์š”, ๊ฐ„๋‹จํžˆ ์š”์•ฝ๋“œ๋ฆฌ๋ฉด:

Express.js๋กœ REST API๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉฐ JWT๋ฅผ http-only ์ฟ ํ‚ค๋กœ ์ „๋‹ฌํ•˜๋ ค ํ–ˆ๋Š”๋ฐ,
Authorization ํ—ค๋”์™€ CSRF ํ† ํฐ ๊ฒ€์ฆ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ validateHttp() ๋ฉ”์„œ๋“œ๊ฐ€ Authorization ํ—ค๋”๋ฅผ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•˜๋Š” ๋ฐ”๋žŒ์—,
์ฟ ํ‚ค๋กœ JWT๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋„ ํ—ค๋”๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ์ด ๋์ฃ .


์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด๋ณด๋‹ˆ SecuritySchemes์™€ AuthValidator ํด๋ž˜์Šค์˜ validateHttp ๋ฉ”์„œ๋“œ์—์„œ ์ด ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ๊ณ ,
type: http๋กœ ์„ค์ •๋œ ์Šคํ‚ด์—์„œ Authorization ํ—ค๋”๋ฅผ ๊ฐ•์ œํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Pull Request๋ฅผ ์ œ์ถœํ–ˆ๊ณ , ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ์™€ ์ˆ˜์ • ๊ณผ์ •์„ ๊ฑฐ์ณ ๋งˆ์นจ๋‚ด Merge๊ฐ€ ๋˜์–ด express-openapi-validator์˜ Contributor๊ฐ€ ๋œ ์—ฌ์ •์„ ์˜ค๋Š˜ ์ƒ์„ธํžˆ ๊ณต์œ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰

๐ŸŽฏ ์ฒซ ๋ฒˆ์งธ ์‹œ๋„: ์ฟ ํ‚ค ์ง€์› ์ถ”๊ฐ€

์ฒ˜์Œ์—๋Š” ๋‹จ์ˆœํžˆ Authorization ํ—ค๋” ์™ธ์—๋„ ์ฟ ํ‚ค๋ฅผ ํ†ตํ•œ ์ธ์ฆ์„ ์ง€์›ํ•˜๋„๋ก ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:

if (!authHeader && !authCookie) {
  throw Error(`Authorization header or cookie required`);
}

if (type === 'bearer') {
  if (authHeader && !authHeader.includes('bearer')) {
    throw Error(`Authorization header with scheme 'Bearer' required`);
  }

  if (!authHeader && authCookie === undefined) {
    throw Error(`Bearer token required in authorization header or cookie`);
  }
}

๐Ÿ’ก ์ฝ”๋“œ ๋ฆฌ๋ทฐ ํ”ผ๋“œ๋ฐฑ

์ œ๊ฐ€ ์ œ์ถœํ•œ PR์— ๋Œ€ํ•ด ๋ฉ”์ธํ…Œ์ด๋„ˆ๊ป˜์„œ ์ค‘์š”ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ์…จ์Šต๋‹ˆ๋‹ค:

  1. OpenAPI ๋ช…์„ธ์— ๋”ฐ๋ฅด๋ฉด, in: cookie๋กœ ์ง€์ •๋œ ๊ฒฝ์šฐ์™€ ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•จ
  2. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋„ ๊ฐ๊ฐ์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ
    • ์ผ๋ฐ˜์ ์ธ ๊ฒฝ์šฐ: "Authorization header required"
    • ์ฟ ํ‚ค ์ธ์ฆ์˜ ๊ฒฝ์šฐ: "Cookie authentication required"

๐Ÿ”จ ๋‘ ๋ฒˆ์งธ ์‹œ๋„: ๋ช…์„ธ ๊ธฐ๋ฐ˜ ์ˆ˜์ •

ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ๋˜ ํ•˜๋‚˜์˜ ์‹ค์ˆ˜๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ํ•ด์น˜๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ํฌํ•จ๋œ ๊ฒƒ์ด์—ˆ์ฃ ..

 

์˜ˆ๋ฅผ ๋“ค์–ด ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋‚˜ ๋“ค์—ฌ์“ฐ๊ธฐ, ๊ด„ํ˜ธ ์œ„์น˜ ๋“ฑ์ด ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ์Šคํƒ€์ผ๊ณผ ๋‹ฌ๋ž์ฃ .

// ์ œ๊ฐ€ ์ˆ˜์ •ํ•œ ์ฝ”๋“œ
function extractErrorsFromResults(
  results: (SecurityHandlerResult | SecurityHandlerResult[])[],
) {
  return results
    .map((result) => {
      if (Array.isArray(result)) {
        return result.map((it) => it).filter((it) => !it.success);
      }
      return [result].filter((it) => !it.success);
    })
    .flatMap((it) => [...it]);
}

// ์›๋ž˜ ํ”„๋กœ์ ํŠธ ์Šคํƒ€์ผ
function extractErrorsFromResults(results: (SecurityHandlerResult | SecurityHandlerResult[])[]) {
  return results.map(result => {
    if (Array.isArray(result)) {
      return result.map(it => it).filter(it => !it.success);
    }
    return [result].filter(it => !it.success);
  }).flatMap(it => [...it]);
}

 

์ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์›๋ž˜ ํ”„๋กœ์ ํŠธ์˜ ์Šคํƒ€์ผ๋Œ€๋กœ ๋˜๋Œ๋ฆฌ๊ณ  ์ปค๋ฐ‹์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ๋์ด ์•„๋‹ˆ์—ˆ์ฃ . CI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๋‹ˆ ๋‘ ๊ฐ€์ง€ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

๐Ÿ› ํ…Œ์ŠคํŠธ ์‹คํŒจ ๋ฐœ๊ฒฌ

์ดํ›„ CI์—์„œ ๋‘ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. Basic ์ธ์ฆ์—์„œ ํ—ค๋”๊ฐ€ ์—†์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ
  2. undefined.includes() ํ˜ธ์ถœ๋กœ ์ธํ•œ ์—๋Ÿฌ
AssertionError: expected 'Cannot read properties of undefined (…' 
to equal 'Authorization header required'

 

๊ฒฐ๋ก ์€ authHeader๊ฐ€ undefined์ผ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฌธ์ œ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์‹คํŒจ์˜€์Šต๋‹ˆ๋‹ค.

โœจ ์ตœ์ข… ํ•ด๊ฒฐ์ฑ…

๋ชจ๋“  ํ”ผ๋“œ๋ฐฑ๊ณผ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ตœ์ข… ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:

private validateHttp(): void {
  if (['http'].includes(scheme.type.toLowerCase())) {
    const authHeader = req.headers['authorization']?.toLowerCase();
    const authCookie = req.cookies[scheme.name] || req.signedCookies?.[scheme.name];

    const type = scheme.scheme?.toLowerCase();
    if (type === 'bearer') {
      if (authHeader && !authHeader.includes('bearer')) {
        throw Error(`Authorization header with scheme 'Bearer' required`);
      }
      
      if (!authHeader && !authCookie) {
        throw Error(scheme.in === 'cookie' 
          ? `Cookie authentication required`
          : `Authorization header required`);
      }
    }

    if (type === 'basic') {
      if (!authHeader) {
        throw Error(`Authorization header required`);
      }
      if (!authHeader.includes('basic')) {
        throw Error(`Authorization header with scheme 'Basic' required`);
      }
    }
  }
}

์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ:

  1. Basic ์ธ์ฆ๊ณผ Bearer ์ธ์ฆ์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„
  2. ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ง€์› (OpenAPI ๋ช…์„ธ ์ค€์ˆ˜)
  3. ์ ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  4. undefined ์ฒดํฌ ๊ฐ•ํ™”

๐ŸŽ“ ๋ฐฐ์šด ์ 

  1. OpenAPI ๋ช…์„ธ๋ฅผ ๋” ๊ผผ๊ผผํžˆ ์‚ดํŽด๋ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค
  2. ํ”„๋กœ์ ํŠธ์˜ ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰์˜ ์ค‘์š”์„ฑ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค
  3. ์ฝ”๋“œ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ๋ฅผ ์ค€์ˆ˜ํ•˜๋Š” ๊ฒƒ์˜ ์ค‘์š”์„ฑ์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค

์ด๋ ‡๊ฒŒ ํ•ด์„œ express-openapi-validator์— ์ž‘์€ ๊ธฐ์—ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๋„ค์š”. ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ๋Š” ์–ธ์ œ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฐ์›€์˜ ๊ธฐํšŒ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์—๋Š” ๋˜ ์–ด๋–ค ์žฌ๋ฏธ์žˆ๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ฒŒ ๋ ์ง€ ๊ธฐ๋Œ€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค! ๐Ÿ˜Š

API์˜ ๊ฒ€์ฆ ๋ฐ ๋ผ์šฐํŒ…์„ ์ž๋™ํ™”!

 

ํ˜„์žฌ Express.js๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ REST API๋ฅผ ๊ตฌ์ถ• ์ค‘์ž…๋‹ˆ๋‹ค.

๋‚˜์ค‘์— ํŒ€์›์ด ํ•ฉ๋ฅ˜ํ•˜๊ฑฐ๋‚˜, ๋ฐ”์œ ์ผ์ • ํ›„์— ๋‹ค์‹œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋ด์•ผ ํ•  ๋•Œ๋ฅผ ๋Œ€๋น„ํ•ด ๋ฌธ์„œํ™”๋ฅผ ํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.

 

์‹œ๊ฐ„์˜ ์ œ์•ฝ ๋•Œ๋ฌธ์— Swagger์™€ OpenAPI Specification์„ ์‚ฌ์šฉํ•ด Design First ๋ฐฉ์‹์œผ๋กœ ๊ทœ๊ฒฉ์„ ์ •์˜ํ•˜๊ณ ,

์ด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฌธ์„œํ™”ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

 

API์˜ ๊ฒ€์ฆ ๋ฐ ๋ผ์šฐํŒ…์„ ์ž๋™ํ™”ํ•˜๊ธฐ ์œ„ํ•ด express-openapi-validator ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์ง€๋งŒ, ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ๐Ÿง

JWT๋ฅผ http-only cookie๋กœ ์ „๋‹ฌํ•˜๋ ค๋Š”๋ฐ, Authorization ํ—ค๋”์™€ CSRF ํ† ํฐ ๊ด€๋ จ ๊ฒ€์ฆ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค!!!

 

๋ฌธ์ œ ์ƒํ™ฉ

  1. CSRF ํ† ํฐ์ด ์žˆ๊ณ  Authorization ํ—ค๋”๊ฐ€ ์—†์„ ๋•Œ
    • Authorization ํ—ค๋” ํ•„์ˆ˜ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Œ
    • ์ด๋•Œ JWT ํ† ํฐ์€ ์ฟ ํ‚ค๋กœ ์ „์†ก
  2. CSRF ํ† ํฐ๊ณผ Authorization ํ—ค๋” ๋ชจ๋‘ ์—†์„ ๋•Œ
    • Authorization ํ—ค๋” ํ•„์ˆ˜๋ผ๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ
    • ์ด๋•Œ๋„ JWT ํ† ํฐ์€ ์ฟ ํ‚ค๋กœ ์ „์†ก
  3. CSRF ํ† ํฐ ์—†์ด Authorization ํ—ค๋”๋งŒ ์žˆ์„ ๋•Œ
    • CSRF ํ† ํฐ ํ•„์ˆ˜๋ผ๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ

์›์ธ ๋ถ„์„

http-only cookie๋ฅผ ์‚ฌ์šฉ ์ค‘์ด๋ฏ€๋กœ, Authorization ํ—ค๋”๋ฅผ ๋ณด๋‚ด์ง€ ์•Š๋”๋ผ๋„ ์ฟ ํ‚ค์˜ JWT ํ† ํฐ๋งŒ์œผ๋กœ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ validateSecurity ๋ฏธ๋“ค์›จ์–ด ๋กœ์ง์—์„œ CSRF๋‚˜ JWT ๋‘˜ ๋‹ค ๋ˆ„๋ฝ๋  ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฏธ๋“ค์›จ์–ด์˜ ๋ณด์•ˆ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์‚ฌ์‹ค์„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ฝ”๋“œ ๋ถ„์„

express-openapi-validator์˜ ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•˜๋ฉด์„œ ์ธ์ฆ ๊ด€๋ จ ๋กœ์ง์ด SecuritySchemes ํด๋ž˜์Šค์™€ AuthValidator ํด๋ž˜์Šค์—์„œ ์ฒ˜๋ฆฌ๋œ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

  • SecuritySchemes ํด๋ž˜์Šค๋Š” securityHandlers๋ฅผ ํ†ตํ•ด ๋ณด์•ˆ ๊ฒ€์ฆ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  • AuthValidator ํด๋ž˜์Šค๋Š” ์š”์ฒญ ํ—ค๋”๋ฅผ ๊ฒ€์‚ฌํ•˜๊ณ , Authorization ํ—ค๋”๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ํŠนํžˆ validateHttp() ๋ฉ”์„œ๋“œ๋Š” ์ด Authorization ํ—ค๋”๋ฅผ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋•Œ๋ฌธ์— type: http๋กœ ์„ค์ •๋œ ์Šคํ‚ด์—์„œ๋Š” Authorization ํ—ค๋”๊ฐ€ ํ•„์ˆ˜์˜€๋˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค!!

์•„๋ž˜๋Š” ๊ด€๋ จ ์ฝ”๋“œ์ด๋‹ˆ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”!

  • SecuritySchemes
๋”๋ณด๊ธฐ
class SecuritySchemes {
95    constructor(securitySchemes, securityHandlers, securities) {
96        this.securitySchemes = securitySchemes;
97        this.securityHandlers = securityHandlers;
98        this.securities = securities;
99    }
100    async executeHandlers(req) {
101        // use a fallback handler if security handlers is not specified
102        // This means if security handlers is specified, the user must define
103        // all security handlers
104        const fallbackHandler = !this.securityHandlers
105            ? defaultSecurityHandler
106            : null;
107        const promises = this.securities.map(async (s) => {
108            if (Util.isEmptyObject(s)) {
109                // anonymous security
110                return [{ success: true }];
111            }
112            return Promise.all(Object.keys(s).map(async (securityKey) => {
113                var _a, _b, _c;
114                try {
115                    const scheme = this.securitySchemes[securityKey];
116                    const handler = (_b =
117                        (_a = this.securityHandlers) === null || _a === void 0
118                            ? void 0
119                            : _a[securityKey]) !== null && _b !== void 0
120                        ? _b
121                        : fallbackHandler;
122                    const scopesTmp = s[securityKey];
123                    const scopes = Array.isArray(scopesTmp) ? scopesTmp : [];
124                    if (!scheme) {
125                        const message = `components.securitySchemes.${securityKey} does not exist`;
126                        throw new types_1.InternalServerError({ message });
127                    }
128                    if (!scheme.hasOwnProperty('type')) {
129                        const message = `components.securitySchemes.${securityKey} must have property 'type'`;
130                        throw new types_1.InternalServerError({ message });
131                    }
132                    if (!handler) {
133                        const message = `a security handler for '${securityKey}' does not exist`;
134                        throw new types_1.InternalServerError({ message });
135                    }
136                    new AuthValidator(req, scheme, scopes).validate();
137                    // expected handler results are:
138                    // - throw exception,
139                    // - return true,
140                    // - return Promise<true>,
141                    // - return false,
142                    // - return Promise<false>
143                    // everything else should be treated as false
144                    const securityScheme = scheme;
145                    const success = await handler(req, scopes, securityScheme);
146                    if (success === true) {
147                        return { success };
148                    }
149                    else {
150                        throw Error();
151                    }
152                }
153                catch (e) {
154                    return {
155                        success: false,
156                        status: (_c = e.status) !== null && _c !== void 0 ? _c : 401,
157                        error: e,
158                    };
159                }
160            }));
161        });
162        return Promise.all(promises);
163    }
164}
  • AuthValidator
๋”๋ณด๊ธฐ
class AuthValidator {
166    constructor(req, scheme, scopes = []) {
167        const openapi = req.openapi;
168        this.req = req;
169        this.scheme = scheme;
170        this.path = openapi.openApiRoute;
171        this.scopes = scopes;
172    }
173    validate() {
174        this.validateApiKey();
175        this.validateHttp();
176        this.validateOauth2();
177        this.validateOpenID();
178    }
179    validateOauth2() {
180        const { req, scheme, path } = this;
181        if (['oauth2'].includes(scheme.type.toLowerCase())) {
182            // TODO oauth2 validation
183        }
184    }
185    validateOpenID() {
186        const { req, scheme, path } = this;
187        if (['openIdConnect'].includes(scheme.type.toLowerCase())) {
188            // TODO openidconnect validation
189        }
190    }
191    validateHttp() {
192        const { req, scheme, path } = this;
193        if (['http'].includes(scheme.type.toLowerCase())) {
194            const authHeader = req.headers['authorization'] &&
195                req.headers['authorization'].toLowerCase();
196            if (!authHeader) {
197                throw Error(`Authorization header required`);
198            }
199            const type = scheme.scheme && scheme.scheme.toLowerCase();
200            if (type === 'bearer' && !authHeader.includes('bearer')) {
201                throw Error(`Authorization header with scheme 'Bearer' required`);
202            }
203            if (type === 'basic' && !authHeader.includes('basic')) {
204                throw Error(`Authorization header with scheme 'Basic' required`);
205            }
206        }
207    }
208    validateApiKey() {
209        var _d;
210        const { req, scheme, path } = this;
211        if (scheme.type === 'apiKey') {
212            if (scheme.in === 'header') {
213                if (!req.headers[scheme.name.toLowerCase()]) {
214                    throw Error(`'${scheme.name}' header required`);
215                }
216            }
217            else if (scheme.in === 'query') {
218                if (!req.query[scheme.name]) {
219                    throw Error(`query parameter '${scheme.name}' required`);
220                }
221            }
222            else if (scheme.in === 'cookie') {
223                if (!req.cookies[scheme.name] && !((_d = req.signedCookies) === null || _d === void 0 ? void 0 : _d[scheme.name])) {
224                    throw Error(`cookie '${scheme.name}' required`);
225                }
226            }
227        }
228    }
229}

 

๋ฌธ์ œ ํ•ด๊ฒฐ

 

๊ธฐ์กด ์„ค์ •

jwt_auth:
  description: Bearer token authorization with JWT
  type: http
  scheme: bearer
  bearerFormat: JWT
  • jwt_auth๋Š” type: http๋กœ ์„ค์ •๋˜์–ด ์žˆ์—ˆ๊ณ , ์ด ๋•Œ๋ฌธ์— validateHttp() ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์–ด Authorization ํ—ค๋”๊ฐ€ ํ•„์ˆ˜๋ผ๋Š” ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์ƒˆ๋กœ์šด ์„ค์ •

jwt_auth:
  type: apiKey
  in: cookie
  name: token
  description: JWT token authentication using HTTP-Only cookies
  • jwt_auth๋ฅผ type: apiKey๋กœ ์„ค์ •ํ•˜์—ฌ, ์ด์ œ ์ฟ ํ‚ค์—์„œ JWT๋ฅผ ๊ฒ€์ฆํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.
 

๋ณ€๊ฒฝ ํ›„ validateApiKey() ๋ฉ”์„œ๋“œ๊ฐ€ ์ฟ ํ‚ค์—์„œ JWT๋ฅผ ๊ฒ€์ฆํ•˜๊ฒŒ ๋์Šต๋‹ˆ๋‹ค!

 

Swagger UI
CSRF Error

์ด์ œ์•ผ ์˜๋„ํ•œ ๋Œ€๋กœ ์ž˜ ๋™์ž‘ํ•˜๋„ค์š”!

 

๋‹ค๋งŒ, Authorization ํ—ค๋”๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์•˜์„ ๋•Œ, ์—๋Ÿฌ์˜ ์›์ธ์€ ํŒŒ์•…๋์ง€๋งŒ ์•„์ง๋„ CSRF ํ† ํฐ์„ ๋ณด๋‚ด์คฌ์„ ๋• ์™œ ํ†ต๊ณผ๋˜๋Š” ๊ฑธ๊นŒ?๋ผ๋Š” ์˜๋ฌธ์€ ํ•ด๊ฒฐ์ด ์•ˆ ๋์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๊ทธ ์˜๋ฌธ์€ ์ œ๊ฐ€ ์ž‘์„ฑํ•ด๋‘” ์•„๋ž˜์˜ validateSecurity ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ๋Œ์•„๋ณด๋ฉฐ ํ’€๋ฆฌ๊ฒŒ ๋์–ด์š”..๐Ÿ˜ญ

validateSecurity: {
	handlers: {
        jwt_auth: async (req) => {
          const isTokenValid = await authHandler(req);
          return isTokenValid && csrfCheck(req);
        },
        csrf_token: authHandler,
     },
},

 

csrf_token์˜ handler๋Š” csrf_token์„ ๋ณด๋‚ด์ฃผ์ง€ ์•Š์•˜์„ ๋• ๋™์ž‘ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ €๋Š” ์œ„์ฒ˜๋Ÿผ jwt_auth์—์„œ jwt_auth๊ฐ€ ์žˆ์„ ์‹œ csrf_token์˜ ์ฒดํฌ๋„ ํ•จ๊ป˜ํ•ด ์ฃผ๋„๋ก ๋งŒ๋“ค์—ˆ์–ด์š”.

ํ•˜์ง€๋งŒ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ด๋Ÿฐ ์‹์œผ๋กœ ๋“ฑ๋กํ•ด์•ผ ํ•˜๋Š” ๊ฑด ๋ถ€์ž์—ฐ์Šค๋Ÿฝ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ด ๋ถ€๋ถ„์€ ๋” ๊ฒ€ํ† ํ•ด ๋ณธ ํ›„ ๊ณต์œ ํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ ์˜ค๋Š˜์€ type: http์— ๋Œ€ํ•œ validateHttp ๋กœ์ง์ด AuthValidator Header๋ฅผ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•œ๋‹ค๋Š” ๊ฒŒ ๋ฉ”์ธ ์ฃผ์ œ์ด๋‹ˆ ์—ฌ๊ธฐ์„œ ๋” ๋‹ค๋ฃจ์ง„ ์•Š๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ถ”๊ฐ€ ๊ณ ๋ ค ์‚ฌํ•ญ

type: apiKey๋กœ ๋ณ€๊ฒฝํ•˜๋ฉด Bearer ํ† ํฐ์— ๋Œ€ํ•œ ๊ฒ€์ฆ์ด ์‚ฌ๋ผ์ง„๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด์—๋Š” type: http ์„ค์ • ๋•๋ถ„์— validateHttp ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜์—ˆ๊ณ , ์ด ๋ฉ”์„œ๋“œ๋Š” Authorization ํ—ค๋”์— Bearer ํ† ํฐ์„ ์š”๊ตฌํ•˜๋ฉด์„œ JWT๋ฅผ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ type: apiKey๋กœ ๋ณ€๊ฒฝํ•˜๊ฒŒ ๋˜๋ฉด ์ด์ œ validateApiKey ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ณ  validateApiKey๋Š” Bearer ํ† ํฐ ํ˜•์‹์„ ์ „ํ˜€ ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

์™œ Bearer ๊ฒ€์ฆ์ด ์ค‘์š”ํ• ๊นŒ์š”?
JWT ํ† ํฐ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ Bearer ํ† ํฐ์œผ๋กœ ์ „์†ก๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.

์ฆ‰, Authorization: Bearer <JWT> ํ˜•ํƒœ๋กœ ์ „์†ก๋˜๊ธฐ ๋•Œ๋ฌธ์—, ์ด ๋ฐฉ์‹์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด type: http์™€ scheme: bearer ์„ค์ •์„ ์œ ์ง€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•ด์•ผ validateHttp๊ฐ€ ์ ์ ˆํžˆ Bearer ํ† ํฐ์„ ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•  ํ…Œ๋‹ˆ๊นŒ์š”.

 

ํ•˜์ง€๋งŒ ๋‹จ์ˆœํžˆ JWT๋งŒ ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ๊ณ  Bearer ํ˜•์‹์— ์–ฝ๋งค์ด์ง€ ์•Š๊ฒ ๋‹ค๋ฉด, ํ˜„์žฌ์ฒ˜๋Ÿผ type: apiKey๋กœ ์„ค์ •ํ•œ ํ›„ ์ฟ ํ‚ค์—์„œ JWT๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋กœ์ง์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

 

ํ˜„์žฌ๊นŒ์ง„, JWT๋ฅผ Bearer ํ˜•์‹์œผ๋กœ ์œ ์ง€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด validateHttp() ๋ฉ”์„œ๋“œ์—์„œ ์ฟ ํ‚ค๋กœ ์ „๋‹ฌ๋œ JWT๋„ ํ•จ๊ป˜ ๊ฒ€์ฆํ•˜๋„๋ก ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๋ฐ”๋žŒ์งํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋ฉ๋‹ˆ๋‹ค.

 


๊ฒฐ๋ก 

  • ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ JWT ์ธ์ฆ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, type: apiKey๋กœ ์„ค์ •ํ•œ ํ›„ ์ฟ ํ‚ค์—์„œ JWT๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ํ•˜์ง€๋งŒ Bearer ํ† ํฐ ๋ฐฉ์‹์˜ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด type: http ์„ค์ •์„ ์œ ์ง€ํ•˜๊ณ , validateHttp()์—์„œ ์ฟ ํ‚ค๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Pull requests

 

์ œ๊ฐ€ ์ƒ๊ฐํ•œ ๋ฐฉ์‹์ด ๋งž๋Š”์ง€, ๋†“์นœ ๋ถ€๋ถ„์€ ์—†๋Š”์ง€ ๊ฒ€ํ† ํ•˜๊ธฐ ์œ„ํ•ด validateHttp ๋ฉ”์„œ๋“œ๋ฅผ ์ˆ˜์ •ํ›„ PR์„ ๋‚จ๊ฒจ๋‘” ์ƒํƒœ์ž…๋‹ˆ๋‹ค.
๋น ๋ฅธ ์ ์šฉ ํ›„ ํ”ผ๋“œ๋ฐฑ์„ ์œ„ํ•ด ์—ฐ๊ด€๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊นŒ์ง„ ์†์„ ๋Œ€์ง€ ์•Š์•˜์ง€๋งŒ,
ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ ๋” ๊นŠ๊ฒŒ ๊ณ ๋ฏผํ•ด ๋ณผ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค!

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
npm-package
GitHub - cdimascio/express-openapi-validator

 

+ Recent posts