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์— ์ž‘์€ ๊ธฐ์—ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๋„ค์š”. ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ๋Š” ์–ธ์ œ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฐ์›€์˜ ๊ธฐํšŒ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

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

+ Recent posts