ํ์ฌ Express.js๋ฅผ ์ฌ์ฉํ์ฌ REST API๋ฅผ ๊ตฌ์ถ ์ค์ ๋๋ค.
๋์ค์ ํ์์ด ํฉ๋ฅํ๊ฑฐ๋, ๋ฐ์ ์ผ์ ํ์ ๋ค์ ํ๋ก์ ํธ๋ฅผ ๋ด์ผ ํ ๋๋ฅผ ๋๋นํด ๋ฌธ์ํ๋ฅผ ํ๋ ค ํฉ๋๋ค.
์๊ฐ์ ์ ์ฝ ๋๋ฌธ์ Swagger์ OpenAPI Specification์ ์ฌ์ฉํด Design First ๋ฐฉ์์ผ๋ก ๊ท๊ฒฉ์ ์ ์ํ๊ณ ,
์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฌธ์ํํ๊ณ ์์ต๋๋ค.
API์ ๊ฒ์ฆ ๋ฐ ๋ผ์ฐํ ์ ์๋ํํ๊ธฐ ์ํด express-openapi-validator ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๊ณ ์์ง๋ง, ๋ช ๊ฐ์ง ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ด์ ๐ง
JWT๋ฅผ http-only cookie๋ก ์ ๋ฌํ๋ ค๋๋ฐ, Authorization ํค๋์ CSRF ํ ํฐ ๊ด๋ จ ๊ฒ์ฆ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ ๋๋ค!!!
๋ฌธ์ ์ํฉ
- CSRF ํ ํฐ์ด ์๊ณ Authorization ํค๋๊ฐ ์์ ๋
- Authorization ํค๋ ํ์ ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์์
- ์ด๋ JWT ํ ํฐ์ ์ฟ ํค๋ก ์ ์ก
- CSRF ํ ํฐ๊ณผ Authorization ํค๋ ๋ชจ๋ ์์ ๋
- Authorization ํค๋ ํ์๋ผ๋ ์๋ฌ ๋ฐ์
- ์ด๋๋ JWT ํ ํฐ์ ์ฟ ํค๋ก ์ ์ก
- 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๋ฅผ ๊ฒ์ฆํ๊ฒ ๋์ต๋๋ค!
์ด์ ์ผ ์๋ํ ๋๋ก ์ ๋์ํ๋ค์!
๋ค๋ง, 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()์์ ์ฟ ํค๋ฅผ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
์ ๊ฐ ์๊ฐํ ๋ฐฉ์์ด ๋ง๋์ง, ๋์น ๋ถ๋ถ์ ์๋์ง ๊ฒํ ํ๊ธฐ ์ํด validateHttp ๋ฉ์๋๋ฅผ ์์ ํ PR์ ๋จ๊ฒจ๋ ์ํ์
๋๋ค.
๋น ๋ฅธ ์ ์ฉ ํ ํผ๋๋ฐฑ์ ์ํด ์ฐ๊ด๋ ํ
์คํธ ์ฝ๋๊น์ง ์์ ๋์ง ์์์ง๋ง,
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉฐ ๋ ๊น๊ฒ ๊ณ ๋ฏผํด ๋ณผ ์์ ์
๋๋ค!
๐ ์ฐธ๊ณ ๋ฌธํ | ||
npm-package | ||
GitHub - cdimascio/express-openapi-validator |