/************************************************************************* * * Troven CONFIDENTIAL * __________________ * * (c) 2017-2020 Troven Ventures Pty Ltd * All Rights Reserved. * * NOTICE: All information contained herein is, and remains * the property of Troven Pty Ltd and its licensors, * if any. The intellectual and technical concepts contained * herein are proprietary to Troven Pty Ltd * and its suppliers and may be covered by International and Regional Patents, * patents in process, and are protected by trade secret or copyright law. * Dissemination of this information or reproduction of this material * is strictly forbidden unless prior written permission is obtained * from Troven Pty Ltd. */ import * as assert from "assert"; import { Utils } from "../helpers/"; import { Request, Response } from "express"; import { IOperation, IChassisMiddleware, IChassisContext, IJWT, } from "../interfaces"; import _ = require("lodash"); import JWTPlugin from "../plugins/jwt"; import { OpenAPI } from "../openapi/OpenAPI"; import { jwt } from "."; import JWT from "../helpers/JWT"; const DEFAULT_CLAIM_MATCHERS = [ "$.roles", "$.realm_access.roles", "$.resource_access.account.roles", ]; /** * Authorize JWT * ------------- * Ensure the JWT contains specified claims. * * Unless disabled, an audit event is emitted. * */ export default class openapi implements IChassisMiddleware { name = "api.openapi"; title = "OpenAPI (Security & Validation)"; constructor(protected plugin: JWTPlugin, protected openapi: OpenAPI) {} security_scheme(rules_type: string, rules_scheme: string) { // collect the set of rules that match scheme/type .. // let rules = this.find_scheme(options, rules_type, rules_scheme); let securityScheme = this.openapi.security.findScheme( rules_type, rules_scheme ); // we want to fail fast, if configuration doesn't work ... assert( securityScheme, "missing scheme matching type=" + rules_type + " & scheme=" + rules_scheme ); assert( securityScheme.bearerFormat == "JWT", "securityScheme.bearerFormat not JWT" ); return securityScheme; } /** * Returns OpenAPI authorization middleware * * The middleware is bound to the scopes/claims in the x-feature definition: * * scopes: * * * Injected by openapi/paths.ts * * @param {IChassisContext} context * @param oper * @returns {(req, res, next) => boolean} */ fn(operation: IOperation, _options: any): Function { let context: IChassisContext = operation.context; let options = _.extend( { jwt: false, claims: {}, params: {} }, _options ); if (options.jwt === false) { context.log({ code: "api:openapi:jwt:disabled", message: "JWT disabled", resource: operation.resource, method: operation.actionId, }); return null; } // collect the set of rules that match scheme/type .. let securityScheme = this.openapi.security.findSchemeByName("jwt") || this.security_scheme("http", "bearer"); // find all the scopes for current resource let scope_claims = JWT.normalize_scopes( securityScheme.scopes || securityScheme.claims ); let schema_matchers = securityScheme.matchers || DEFAULT_CLAIM_MATCHERS; // claim matchers are 'jsonpaths' - "$.roles", "$.realm_access.roles", etc // each claim is jsonpaths into jsonsata 'claim' matchers let claims_matchers = JWT.claims_matchers( scope_claims, schema_matchers ); let is_protected = claims_matchers.length > 0; // re-use middleware - to pre-validate our JWT ... let jwt_options = { debug: options.debug, params: options.params, claims: options.claims, }; let jwt_middleware_fn: Function = new jwt(this.plugin).fn( operation, jwt_options ); context.log({ code: "api:openapi:protected", message: (is_protected ? "" : "not ") + "protected", protected: is_protected, resource: operation.resource, method: operation.actionId, matchers: _.keys(claims_matchers), expected: scope_claims, jwt_options: jwt_options, }); return function (req: Request, res: Response, next: Function) { if (!is_protected) { next(); return; } // always ensure jwt middleware is executed first jwt_middleware_fn(req, res, () => { let jwt: IJWT = (req as any).jwt; if (!jwt) { return Utils.sendError(context, res, { statusCode: 401, code: "api:token-missing", message: "missing token", }); } // does our JWT match our required OAUTH claims let has_claims = JWT.hasClaims(jwt, claims_matchers); let debug_oper = { resource: operation.resource, method: operation.actionId, scope_claims: scope_claims, claims: options.claims, jwt: jwt.jti, }; // send an error if (!has_claims) { return Utils.sendError(this.context, res, { statusCode: 403, code: "api:unauthorized", message: "unauthorized", debug: debug_oper, }); } // we're good - continue processing request ... this.context.audit({ code: "api:openapi:authorized", message: "authorized", debug: debug_oper, }); next(); }); }; } }