UNPKG

@labshare/services-auth

Version:

Loopback 4 plugin for resource scope-based HTTP route authz

175 lines 7.98 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthenticateActionProvider = void 0; const tslib_1 = require("tslib"); const context_1 = require("@loopback/context"); const rest_1 = require("@loopback/rest"); const keys_1 = require("../keys"); const jwksClient = tslib_1.__importStar(require("jwks-rsa")); const parse_bearer_token_1 = tslib_1.__importDefault(require("parse-bearer-token")); const core_1 = require("@loopback/core"); const lodash_1 = require("lodash"); const authenticate_decorator_1 = require("../decorators/authenticate.decorator"); const express_jwt_1 = require("express-jwt"); const getToken = parse_bearer_token_1.default; const defaultJwksClientOptions = { cache: true, rateLimit: true, jwksRequestsPerMinute: 10, }; const defaultAlgorithms = ["HS256", "RS256"]; /** * @description Provider of a function which authenticates * @example `context.bind('authentication_key') * .toProvider(AuthenticateActionProvider)` */ let AuthenticateActionProvider = class AuthenticateActionProvider { constructor(getConfig, getController, getMethod, secretProvider, isRevokedCallbackProvider, audienceProvider, parseParams, findRoute) { this.getConfig = getConfig; this.getController = getController; this.getMethod = getMethod; this.secretProvider = secretProvider; this.isRevokedCallbackProvider = isRevokedCallbackProvider; this.audienceProvider = audienceProvider; this.parseParams = parseParams; this.findRoute = findRoute; } value() { return (request, response) => this.action(request, response); } /** * The implementation of authenticate() sequence action. * @param request The incoming request provided by the REST layer * @param response The response provided by the REST layer */ async action(request, response) { const controller = await this.getController(); const method = await this.getMethod(); if (!controller || !method) { return; } const metadata = (0, authenticate_decorator_1.getAuthenticateMetadata)(controller, method); // If REST method or class is not decorated, we skip the authentication check if (!metadata) { return; } const { authUrl, tenant, audience, issuer } = await this.getConfig(); if (!authUrl && !this.secretProvider) { throw new Error("`authUrl` is required"); } if (!tenant && !this.secretProvider) { throw new Error("`tenant` is required"); } const jwksClientOptions = { ...defaultJwksClientOptions, jwksUri: `${authUrl}/auth/${tenant}/.well-known/jwks.json`, }; const secret = (await this.secretProvider()) || jwksClient.expressJwtSecret(jwksClientOptions); const isRevoked = await this.isRevokedCallbackProvider(); const jwtAudience = (await this.audienceProvider()) || audience; const credentialsRequired = typeof metadata.credentialsRequired !== undefined ? metadata.credentialsRequired : true; const expressJwtMiddleware = (0, express_jwt_1.expressjwt)({ getToken, isRevoked, secret, audience: jwtAudience, issuer, credentialsRequired, algorithms: defaultAlgorithms, requestProperty: "user", // New default is "auth" }); // Validate JWT in Authorization Bearer header using RS256 await new Promise(async (resolve, reject) => { await expressJwtMiddleware(request, response, (error) => { if (error) { reject(error); } resolve(); }); }); // If user authentication is optional on the request, avoid checking the user's scopes if (!request.user && !credentialsRequired) { return; } const scope = [...((metadata && metadata.scope) || [])]; // Validate JWT Resource Scopes against one or more scopes required by the API. // For example: 'read:users' if (scope.length) { await this.validateResourceScopes(scope)(request, response); } } /** * @description Validates Resource Scopes required by an API definition against the user's bearer token scope claim. * @param {string[]} expectedScopes */ validateResourceScopes(expectedScopes) { if (!Array.isArray(expectedScopes)) { throw new Error("Parameter expectedScopes must be an array of strings representing the scopes for the endpoint(s)"); } return async (req, _res) => { if (expectedScopes.length === 0) { return; } const insufficientScopeError = `Insufficient scope. Required scopes: ${expectedScopes.join(" ")}`; if (!req.user || typeof req.user.scope !== "string") { throw new rest_1.HttpErrors.Forbidden(insufficientScopeError); } const replaceValue = (parsedParamsObj) => ($0, context) => { return (0, lodash_1.get)(parsedParamsObj, context); }; const route = this.findRoute(req); const args = await this.parseParams(req, route); const params = route.spec.parameters; const parsedParams = { path: {}, query: {}, }; if (params) { for (let i = 0; i < args.length; ++i) { const spec = params[i]; if (!spec) continue; // when executing patch, the spec might be undefined const paramIn = spec.in; switch (paramIn) { case "path": case "query": parsedParams[paramIn][spec.name] = args[i]; break; } } } // Fill in dynamic scope parameters with the request parameters. // For example, "{path.tenantId}:read:users" becomes "ls:read:users" assuming // "tenantId" is a path parameter. const expandedScopes = expectedScopes.map((scope) => { return scope.replace(/{([^}]+)}/g, replaceValue(parsedParams)); }); const scopes = req.user.scope.split(" "); const allowed = expandedScopes.some((scope) => scopes.includes(scope)); if (allowed) { return; } throw new rest_1.HttpErrors.Forbidden(insufficientScopeError); }; } }; AuthenticateActionProvider = tslib_1.__decorate([ tslib_1.__param(0, context_1.inject.getter(keys_1.AuthenticationBindings.AUTH_CONFIG)), tslib_1.__param(1, context_1.inject.getter(core_1.CoreBindings.CONTROLLER_CLASS, { optional: true })), tslib_1.__param(2, context_1.inject.getter(core_1.CoreBindings.CONTROLLER_METHOD_NAME, { optional: true })), tslib_1.__param(3, context_1.inject.getter(keys_1.AuthenticationBindings.SECRET_PROVIDER, { optional: true })), tslib_1.__param(4, context_1.inject.getter(keys_1.AuthenticationBindings.IS_REVOKED_CALLBACK_PROVIDER, { optional: true, })), tslib_1.__param(5, context_1.inject.getter(keys_1.AuthenticationBindings.AUDIENCE_PROVIDER, { optional: true, })), tslib_1.__param(6, (0, context_1.inject)(rest_1.SequenceActions.PARSE_PARAMS)), tslib_1.__param(7, (0, context_1.inject)(rest_1.SequenceActions.FIND_ROUTE)), tslib_1.__metadata("design:paramtypes", [Function, Function, Function, Function, Function, Function, Function, Function]) ], AuthenticateActionProvider); exports.AuthenticateActionProvider = AuthenticateActionProvider; //# sourceMappingURL=authentication.provider.js.map