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