Skip to content

Instantly share code, notes, and snippets.

@nflaig
Created January 24, 2021 10:39
Show Gist options
  • Save nflaig/d8b6b30d0d328eae11eefe3e4ff50dce to your computer and use it in GitHub Desktop.
Save nflaig/d8b6b30d0d328eae11eefe3e4ff50dce to your computer and use it in GitHub Desktop.
Interface to allow strategies to specify security spec and enhance openapi spec based on authentication metadata
import {
Application,
bind,
ControllerClass,
CoreBindings,
extensions,
Getter,
inject
} from "@loopback/core";
import {
asSpecEnhancer,
mergeOpenAPISpec,
OASEnhancer,
OpenApiSpec,
OperationObject,
SecurityRequirementObject
} from "@loopback/rest";
import { AuthenticationBindings } from "../keys";
import { getAuthenticateMetadata } from "../decorators";
import { AuthenticationStrategy, SecuritySchemes } from "../types";
import { castArray, createStrategyMapping } from "../utils";
@bind(asSpecEnhancer)
export class SecuritySpecEnhancer implements OASEnhancer {
name = "security";
constructor(
@extensions(AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME)
private getStrategies: Getter<AuthenticationStrategy[]>,
@inject(CoreBindings.APPLICATION_INSTANCE)
private app: Application
) {}
async modifySpec(spec: OpenApiSpec): Promise<OpenApiSpec> {
const securitySchemes: SecuritySchemes = {};
const existingStrategies = await this.getStrategies();
const strategyMapping = createStrategyMapping(existingStrategies);
const { paths } = spec;
for (const path in paths) {
for (const op in paths[path]) {
const operation: OperationObject = paths[path][op];
const methodName: string = operation["x-operation-name"];
const controllerName: string = operation["x-controller-name"];
const binding = this.app.getBinding(`${CoreBindings.CONTROLLERS}.${controllerName}`, {
optional: true
});
if (!binding) continue;
const controllerClass: ControllerClass = binding.valueConstructor!;
const metadata = getAuthenticateMetadata(controllerClass, methodName);
if (!metadata) continue;
const strategyNames = metadata.map(m => m.strategy);
const security: SecurityRequirementObject[] = operation.security ?? [];
for (const name of strategyNames) {
const strategy = strategyMapping[name];
if (strategy?.securitySpec) {
const securitySpecs = await strategy.securitySpec();
for (const securitySpec of castArray(securitySpecs)) {
security.push(securitySpec.operationSecurity);
securitySchemes[securitySpec.schemeName] = securitySpec.securityScheme;
}
}
}
operation.security = security;
}
}
return mergeOpenAPISpec(spec, { components: { securitySchemes } });
}
}
export interface AuthenticationStrategy {
name: string;
authenticate(request: Request): Promise<UserProfile | RedirectRoute | undefined>;
securitySpec?(): ValueOrPromise<SecuritySpec | SecuritySpec[]>;
}
export type SecuritySpec = {
schemeName: string;
securityScheme: SecuritySchemeObject;
operationSecurity: SecurityRequirementObject;
};
export type SecuritySchemes = { [securityScheme: string]: SecuritySchemeObject };
export type StrategyMapping = { [name: string]: AuthenticationStrategy };
export function createStrategyMapping(strategies: AuthenticationStrategy[]): StrategyMapping {
const strategyMapping: StrategyMapping = {};
for (const strategy of strategies) {
strategyMapping[strategy.name] = strategy;
}
return strategyMapping;
}
export function castArray<T = unknown>(value?: T | T[]): T[] {
return Array.isArray(value) ? value : value !== undefined ? [value] : [];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment