@labshare/services-auth
Version:
Loopback 4 plugin for resource scope-based HTTP route authz
175 lines • 7.98 kB
JavaScript
;
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