diff --git a/README.md b/README.md index ab47e432b..f4de908e5 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ You can see the details in `swagger-ui` module is built with authorization component, which will show up by adding the security schema and operation security spec in the OpenAPI spec. -You can check the swagger +You can check the OpenAPI [doc](https://swagger.io/docs/specification/authentication/bearer-authentication/) to learn how to add it, see section "Describing Bearer Authentication". @@ -151,6 +151,57 @@ is injected in the request: ![me](/imgs/me.png) +### Operation Level Security Policy + +You can also specify security policy for a single endpoint, by adding the +security spec in the operation decorator, like: + +```ts +// Define your operation level security policy +const SECURITY_SPEC_OPERATION = { + [{basicAuth: []}]; +} + +// Also add the corresponding security schema into `components.securitySchemas` +const SECURITY_SCHEMA_SPEC = { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + basicAuth: { + type: 'http', + scheme: 'basic', + }, + }, +}; + +// Add security policy in the operation decorator +@get('/users/{userId}', { + security: SECURITY_SPEC_OPERATION, + responses: RESPONSE_SPEC, + }) + async findById(@param.path.string('userId') userId: string): Promise { + // Print out the header, you will see the Basic auth string in the header + console.log(`header ${inspect(this.request.headers)}`); + // business logic + } +``` + +The "Authorize" dialog will show up your new entry as: + +![operation-level-security](/imgs/operation-level-security.png) + +Provide the username and password to login: + +![set-basic-auth](/imgs/set-basic-auth.png) + +Try endpoint `GET users/{userId}`, the printed header contains the basic auth +string: + +![basic-auth-header](/imgs/basic-auth-header.png) + ### Follow-up Stories As you could find in the `security-spec.ts` file, security related spec is diff --git a/imgs/basic-auth-header.png b/imgs/basic-auth-header.png new file mode 100644 index 000000000..8e1ea6c44 Binary files /dev/null and b/imgs/basic-auth-header.png differ diff --git a/imgs/operation-level-security.png b/imgs/operation-level-security.png new file mode 100644 index 000000000..47601d715 Binary files /dev/null and b/imgs/operation-level-security.png differ diff --git a/imgs/set-basic-auth.png b/imgs/set-basic-auth.png new file mode 100644 index 000000000..d9e77b5dc Binary files /dev/null and b/imgs/set-basic-auth.png differ diff --git a/packages/shopping/src/application.ts b/packages/shopping/src/application.ts index 20f4b96c8..c6e150a05 100644 --- a/packages/shopping/src/application.ts +++ b/packages/shopping/src/application.ts @@ -6,7 +6,7 @@ import {BootMixin} from '@loopback/boot'; import {ApplicationConfig, BindingKey} from '@loopback/core'; import {RepositoryMixin} from '@loopback/repository'; -import {RestApplication} from '@loopback/rest'; +import {RestApplication, RestBindings} from '@loopback/rest'; import {ServiceMixin} from '@loopback/service-proxy'; import {MyAuthenticationSequence} from './sequence'; import { @@ -29,6 +29,7 @@ import { import {PasswordHasherBindings} from './keys'; import {BcryptHasher} from './services/hash.password.bcryptjs'; import {JWTAuthenticationStrategy} from './authentication-strategies/jwt-strategy'; +import {SECURITY_SCHEMA_SPEC, SECURITY_SPEC} from './utils/security-spec'; /** * Information from package.json @@ -99,4 +100,18 @@ export class ShoppingApplication extends BootMixin( this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService); } + + async start(): Promise { + await super.start(); + const oaiSpec = this.getSync(RestBindings.API_SPEC); + console.log(oaiSpec); + this.api( + Object.assign(oaiSpec, { + components: { + ...SECURITY_SCHEMA_SPEC, + }, + security: SECURITY_SPEC, + }), + ); + } } diff --git a/packages/shopping/src/controllers/user.controller.ts b/packages/shopping/src/controllers/user.controller.ts index 226a159ac..ff6c19859 100644 --- a/packages/shopping/src/controllers/user.controller.ts +++ b/packages/shopping/src/controllers/user.controller.ts @@ -5,11 +5,19 @@ import {repository} from '@loopback/repository'; import {validateCredentials} from '../services/validator'; -import {post, param, get, requestBody, HttpErrors} from '@loopback/rest'; +import { + post, + param, + get, + requestBody, + HttpErrors, + RestBindings, + Request, +} from '@loopback/rest'; import {User, Product} from '../models'; import {UserRepository} from '../repositories'; import {RecommenderService} from '../services/recommender.service'; -import {inject} from '@loopback/core'; +import {inject, Context} from '@loopback/core'; import { authenticate, UserProfile, @@ -23,6 +31,7 @@ import { } from './specs/user-controller.specs'; import {Credentials} from '../repositories/user.repository'; import {PasswordHasher} from '../services/hash.password.bcryptjs'; +import {inspect} from 'util'; import { TokenServiceBindings, @@ -30,6 +39,7 @@ import { UserServiceBindings, } from '../keys'; import * as _ from 'lodash'; +import {SECURITY_SPEC_OPERATION} from '../utils/security-spec'; export class UserController { constructor( @@ -42,6 +52,8 @@ export class UserController { public jwtService: TokenService, @inject(UserServiceBindings.USER_SERVICE) public userService: UserService, + @inject(RestBindings.Http.CONTEXT) public ctx: Context, + @inject(RestBindings.Http.REQUEST) private request: Request, ) {} @post('/users', { @@ -83,6 +95,7 @@ export class UserController { } @get('/users/{userId}', { + security: SECURITY_SPEC_OPERATION, responses: { '200': { description: 'User', @@ -97,6 +110,7 @@ export class UserController { }, }) async findById(@param.path.string('userId') userId: string): Promise { + console.log(`header ${inspect(this.request.headers)}`); return this.userRepository.findById(userId, { fields: {password: false}, }); diff --git a/packages/shopping/src/index.ts b/packages/shopping/src/index.ts index 25f7ea348..3b70b30a8 100644 --- a/packages/shopping/src/index.ts +++ b/packages/shopping/src/index.ts @@ -6,17 +6,12 @@ import {ShoppingApplication} from './application'; import {ApplicationConfig} from '@loopback/core'; import {RestBindings} from '@loopback/rest'; -import {addSecuritychema} from './utils/security-spec'; export {ShoppingApplication, PackageInfo, PackageKey} from './application'; export async function main(options?: ApplicationConfig) { const app = new ShoppingApplication(options); await app.boot(); - let oaiSchema = app.getSync(RestBindings.API_SPEC); - addSecuritychema(oaiSchema); - console.log(oaiSchema.components!.securitySchemes); - app.bind(RestBindings.API_SPEC).to(oaiSchema); await app.start(); const url = app.restServer.url; diff --git a/packages/shopping/src/utils/security-spec.ts b/packages/shopping/src/utils/security-spec.ts index 4f07b7603..cd49c84dd 100644 --- a/packages/shopping/src/utils/security-spec.ts +++ b/packages/shopping/src/utils/security-spec.ts @@ -1,18 +1,23 @@ -import {OpenApiSpec} from '@loopback/rest'; +// import {OpenApiSpec} from '@loopback/rest'; export const SECURITY_SPEC = [{bearerAuth: []}]; -export const BEARER_SECURITY_SCHEMA_SPEC = { +export const SECURITY_SPEC_OPERATION = [{basicAuth: []}]; +export const SECURITY_SCHEMA_SPEC = { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', }, + basicAuth: { + type: 'http', + scheme: 'basic', + }, }, }; -export function addSecuritychema(schema: OpenApiSpec) { - schema.components = schema.components || {}; - Object.assign(schema.components, BEARER_SECURITY_SCHEMA_SPEC); - Object.assign(schema, {security: SECURITY_SPEC}); -} +// export function addSecuritychema(schema: OpenApiSpec) { +// schema.components = schema.components || {}; +// Object.assign(schema.components, SECURITY_SCHEMA_SPEC); +// Object.assign(schema, {security: SECURITY_SPEC}); +// }