diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index b74a448f..07ce1f4d 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -40,6 +40,7 @@ export class RequestValidator { } // cache middleware by combining method, path, and contentType + // TODO contentType could have value not_provided const contentType = extractContentType(req); const key = `${req.method}-${req.path}-${contentType}`; diff --git a/src/middlewares/openapi.security.ts b/src/middlewares/openapi.security.ts index a075fb9f..8cec23aa 100644 --- a/src/middlewares/openapi.security.ts +++ b/src/middlewares/openapi.security.ts @@ -1,5 +1,5 @@ import { SecurityHandlers } from '../index'; -import { OpenAPIV3 } from '../framework/types'; +import { OpenAPIV3, OpenApiRequest } from '../framework/types'; import { validationError } from './util'; import { OpenApiContext } from '../framework/openapi.context'; @@ -7,7 +7,7 @@ export function security( context: OpenApiContext, securityHandlers: SecurityHandlers, ) { - return (req, res, next) => { + return async (req, res, next) => { if (!req.openapi) { // this path was not found in open api and // this path is not defined under an openapi base path @@ -19,7 +19,7 @@ export function security( req.openapi.schema.security ); - const path = req.openapi.openApiRoute; + const path: string = req.openapi.openApiRoute; if (!path || !Array.isArray(securitySchema)) { return next(); @@ -28,60 +28,134 @@ export function security( const securitySchemes = context.apiDoc.components && context.apiDoc.components.securitySchemes; if (!securitySchemes) { - // TODO throw error securitySchemes don't exist, but a security is referenced in this model + const message = `security referenced at path ${path}, but not defined in components.securitySchemes`; + return next(validationError(500, path, message)); } // TODO security could be boolean or promise bool, handle both const promises = securitySchema.map(s => { - const securityKey = Object.keys(s)[0]; - const scheme: any = securitySchemes[securityKey]; - const handler = securityHandlers[securityKey]; - if (!scheme) { - const message = `components.securitySchemes.${securityKey} does not exist`; - return Promise.reject(validationError(401, path, message)); - } - if (!handler) { - const message = `a handler for ${securityKey} does not exist`; - return Promise.reject(validationError(401, path, message)); - } - if (scheme.type === 'apiKey') { - // check defined header - if (scheme.in === 'header') { - if (!req.headers[scheme.name.toLowerCase()]) { - return Promise.reject(validationError(401, path, `'${scheme.name}' header required.`)); - } - } else if (scheme.in === 'query') { - if (!req.headers[scheme.name]) { - return Promise.reject(validationError(401, path, `query parameter '${scheme.name}' required.`)); - } + try { + const securityKey = Object.keys(s)[0]; + const scheme: any = securitySchemes[securityKey]; + const handler = securityHandlers[securityKey]; + + if (!scheme) { + const message = `components.securitySchemes.${securityKey} does not exist`; + throw validationError(401, path, message); } - } - if (['http'].includes(scheme.type)) { - if (!req.headers['authorization']) { - return Promise.reject(validationError(401, path, `'authorization' header required.`)); + if (!scheme.type) { + const message = `components.securitySchemes.${securityKey} must have property 'type'`; + throw validationError(401, path, message); + } + if (!handler) { + const message = `a handler for ${securityKey} does not exist`; + throw validationError(401, path, message); } - } - // TODO handle other security types - // TODO get scopes - const scopes = []; - try { + const { scopes } = new AuthValidator(req, scheme).validate(); + return Promise.resolve(handler(req, scopes, securitySchema)); } catch (e) { return Promise.reject(e); } }); - return Promise.all(promises) - .then(results => { - const authFailed = results.filter(b => !b).length > 0; - if (authFailed) throw Error(); - else next(); - }) - .catch(e => { - const message = (e && e.message) || 'unauthorized'; - const err = validationError(401, path, message); - next(err); - }); + try { + const results = await Promise.all(promises); + const authFailed = results.filter(b => !b).length > 0; + if (authFailed) throw validationError(401, path, 'unauthorized'); + else next(); + } catch (e) { + const message = (e && e.message) || 'unauthorized'; + const err = validationError(401, path, message); + next(err); + } }; } + +class AuthValidator { + private req: OpenApiRequest; + private scheme; + private path: string; + private scopes: string[] = []; + constructor(req: OpenApiRequest, scheme) { + this.req = req; + this.scheme = scheme; + this.path = req.openapi.openApiRoute; + } + + validate() { + this.validateApiKey(); + this.validateHttp(); + this.validateOauth2(); + this.validateOpenID(); + return { + scopes: this.scopes, + }; + } + + private validateOauth2() { + // TODO get scopes from auth validator + const { req, scheme, path } = this; + if (['oauth2'].includes(scheme.type.toLowerCase())) { + // TODO handle oauth2 + } + } + + private validateOpenID() { + // TODO get scopes from auth validator + const { req, scheme, path } = this; + if (['openIdConnect'].includes(scheme.type.toLowerCase())) { + // TODO handle openidconnect + } + } + + private validateHttp() { + const { req, scheme, path } = this; + if (['http'].includes(scheme.type.toLowerCase())) { + const authHeader = + req.headers['authorization'] && + req.headers['authorization'].toLowerCase(); + + if (!authHeader) { + throw validationError(401, path, `Authorization header required.`); + } + + const type = scheme.scheme && scheme.scheme.toLowerCase(); + if (type === 'bearer' && !authHeader.includes('bearer')) { + throw validationError( + 401, + path, + `Authorization header with scheme 'Bearer' required.`, + ); + } + + if (type === 'basic' && !authHeader.includes('basic')) { + throw validationError( + 401, + path, + `Authorization header with scheme 'Basic' required.`, + ); + } + } + } + + private validateApiKey() { + const { req, scheme, path } = this; + if (scheme.type === 'apiKey') { + if (scheme.in === 'header') { + if (!req.headers[scheme.name.toLowerCase()]) { + throw validationError(401, path, `'${scheme.name}' header required.`); + } + } else if (scheme.in === 'query') { + if (!req.headers[scheme.name]) { + throw validationError( + 401, + path, + `query parameter '${scheme.name}' required.`, + ); + } + } + } + } +} diff --git a/test/resources/security.yaml b/test/resources/security.yaml index f4168fe8..567423e2 100644 --- a/test/resources/security.yaml +++ b/test/resources/security.yaml @@ -20,6 +20,26 @@ paths: #'401': # description: unauthorized + /bearer: + get: + security: + - BearerAuth: [] + responses: + '200': + description: OK + '400': + description: Bad Request + + /basic: + get: + security: + - BasicAuth: [] + responses: + '200': + description: OK + '400': + description: Bad Request + components: securitySchemes: BasicAuth: diff --git a/test/security.spec.ts b/test/security.spec.ts index 0b23dfb0..68ded1d0 100644 --- a/test/security.spec.ts +++ b/test/security.spec.ts @@ -14,7 +14,6 @@ describe(packageJson.name, () => { apiSpec: path.join('test', 'resources', 'security.yaml'), securityHandlers: { ApiKeyAuth: function(req, scopes, schema) { - console.log('apikey handler throws custom error'); throw { errors: [] }; }, }, @@ -28,7 +27,9 @@ describe(packageJson.name, () => { `${basePath}`, express .Router() - .get(`/api_key`, (req, res) => res.json({ logged_in: true })), + .get(`/api_key`, (req, res) => res.json({ logged_in: true })) + .get(`/bearer`, (req, res) => res.json({ logged_in: true })) + .get(`/basic`, (req, res) => res.json({ logged_in: true })), ); }); @@ -50,7 +51,6 @@ describe(packageJson.name, () => { it('should return 401 if apikey handler returns false', async () => { eovConf.securityHandlers.ApiKeyAuth = function(req, scopes, schema) { - console.log('apikey handler returns false'); return false; }; return request(app) @@ -67,7 +67,6 @@ describe(packageJson.name, () => { it('should return 401 if apikey handler returns Promise with false', async () => { eovConf.securityHandlers.ApiKeyAuth = function(req, scopes, schema) { - console.log('apikey handler returns promise false'); return Promise.resolve(false); }; return request(app) @@ -84,7 +83,6 @@ describe(packageJson.name, () => { it('should return 401 if apikey header is missing', async () => { eovConf.securityHandlers.ApiKeyAuth = function(req, scopes, schema) { - console.log('apikey handler returns promise false'); return true; }; return request(app) @@ -105,39 +103,92 @@ describe(packageJson.name, () => { return request(app) .get(`${basePath}/api_key`) .set('X-API-Key', 'test') - .send({}) .expect(200); }); - // TODO add these tests - // it('should return 401 if auth header is missing for basic auth', async () => { - // eovConf.securityHandlers.BasicAuth = function(req, scopes, schema) { - // return true; - // }; - // return request(app) - // .get(`${basePath}/api_key`) - // .send({}) - // .expect(401); - // }); + it('should return 401 if auth header is missing for basic auth', async () => { + (eovConf.securityHandlers).BasicAuth = function(req, scopes, schema) { + return true; + }; + return request(app) + .get(`${basePath}/basic`) + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include('Authorization'); + }); + }); + + it('should return 401 if auth header has malformed basic auth', async () => { + (eovConf.securityHandlers).BasicAuth = ( + function(req, scopes, schema) { + return true; + } + ); + return request(app) + .get(`${basePath}/basic`) + .set('Authorization', 'XXXX') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include( + "Authorization header with scheme 'Basic' required.", + ); + }); + }); + + it('should return 401 if auth header is missing for bearer auth', async () => { + (eovConf.securityHandlers).BearerAuth = ( + function(req, scopes, schema) { + return true; + } + ); + return request(app) + .get(`${basePath}/bearer`) + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include('Authorization'); + }); + }); + + it('should return 401 if auth header has malformed bearer auth', async () => { + (eovConf.securityHandlers).BearerAuth = ( + function(req, scopes, schema) { + return true; + } + ); + return request(app) + .get(`${basePath}/bearer`) + .set('Authorization', 'XXXX') + .expect(401) + .then(r => { + const body = r.body; + expect(body.errors).to.be.an('array'); + expect(body.errors).to.have.length(1); + expect(body.errors[0].message).to.include( + "Authorization header with scheme 'Bearer' required.", + ); + }); + }); - // it('should return 401 if auth header is missing for bearer auth', async () => { - // eovConf.securityHandlers.BearerAuth = function(req, scopes, schema) { - // return true; - // }; - // return request(app) - // .get(`${basePath}/api_key`) - // .send({}) - // .expect(401); - // }); + it('should return 200 if bearer auth succeeds', async () => { + (eovConf.securityHandlers).BearerAuth = ( + function(req, scopes, schema) { + return true; + } + ); + return request(app) + .get(`${basePath}/bearer`) + .set('Authorization', 'Bearer XXXX') + .expect(200); + }); - // it('should return 401 if auth header is missing for malformed bearer auth', async () => { - // eovConf.securityHandlers.BearerAuth = function(req, scopes, schema) { - // return true; - // }; - // return request(app) - // .get(`${basePath}/api_key`) - // .set('Authorization', 'XXXX') - // .send({}) - // .expect(401); - // }); + // TODO create tests for oauth2 and openid });