Created
January 24, 2021 10:39
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } }); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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