From 451eb484f484fdeb09f228b0c2fd1f10bd489c3c Mon Sep 17 00:00:00 2001 From: Diana Lau Date: Wed, 13 May 2020 22:10:04 -0400 Subject: [PATCH] fix: refactor auth tutorial Signed-off-by: Diana Lau --- authentication-tutorial.md | 1314 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1314 insertions(+) create mode 100644 authentication-tutorial.md diff --git a/authentication-tutorial.md b/authentication-tutorial.md new file mode 100644 index 000000000..d5b008fb7 --- /dev/null +++ b/authentication-tutorial.md @@ -0,0 +1,1314 @@ +## Overview + +LoopBack 4 has an authentication package `@loopback/authentication` which allows +you to secure your application's API endpoints with custom authentication +strategies and an `@authenticate` decorator. + +This tutorial showcases how `authentication` was added to the +[loopback4-example-shopping](https://github.com/strongloop/loopback4-example-shopping) +application by **creating** and **registering** a custom authentication strategy +based on the `JSON Web Token (JWT)` approach. + +Here is a brief summary of the `JSON Web Token (JWT)` approach. + +![JSON Web Token Authentication Overview](https://loopback.io/pages/en/lb4/imgs/json_web_token_overview.png) + +In the **JSON Web Token (JWT)** authentication approach, when the user provides +the **correct** credentials to a **login** endpoint, the server creates a JWT +token and returns it in the response. The token is of type **string** and +consists of 3 parts: the **header**, the **payload**, and the **signature**. The +**header** and **payload** are digitally signed with **secret**, and the parts +are separated by a period. + +For example: + +```ts +// {base64UrlEncode-header}.{base64UrlEncode-payload}.{encrypted-signature} +eyJhbXVCJ9.eyJpZCI6Ij.I3wpRNCH4; +// actual parts have been reduced in size for viewing purposes +``` + +{% include note.html content="The payload can contain anything +the application developer wants, but at the very least contains the user id. It should never contain the user password." %} + +After logging in and obtaining this token, whenever the user attempts to access +a protected endpoint, the token must be provided in the **Authorization** +header. The server verifies that the token is valid and not expired, and then +permits access to the protected endpoint. + +Please see [JSON Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) +for more details. + +To view and run the completed `loopback4-example-shopping` application, follow +the instructions in the [Try it out](#try-it-out) section. + +To understand the details of how JWT authentication can be added to a LoopBack 4 +application, read the +[Adding JWT Authentication to a LoopBack 4 Application](#adding-jwt-authentication-to-a-loopback-4-application) +section. + +## Try it out + +If you'd like to see the final results of this tutorial as an example +application, follow these steps: + +1. Start the application: + + ```sh + git clone https://github.com/strongloop/loopback4-example-shopping.git + cd loopback4-example-shopping + npm install + npm run docker:start + npm start + ``` + + Wait until you see: + + ```sh + Recommendation server is running at http://127.0.0.1:3001. + Server is running at http://[::1]:3000 + Try http://[::1]:3000/ping + ``` + +1. In a browser, navigate to [http://[::1]:3000](http://127.0.0.1:3000) or + [http://127.0.0.1:3000](http://127.0.0.1:3000), and click on `/explorer` to + open the `API Explorer`. + +1. In the `UserController` section, click on `POST /users`, click on + `'Try it out'`, specify: + + ```json + { + "email": "user1@example.com", + "password": "thel0ngp@55w0rd", + "firstName": "User", + "lastName": "One" + } + ``` + + and click on `'Execute'` to **add** a new user named `'User One'`. + +1. In the `UserController` section, click on `POST /users/login`, click on + `'Try it out'`, specify: + + ```json + { + "email": "user1@example.com", + "password": "thel0ngp@55w0rd" + } + ``` + + and click on `'Execute'` to **log in** as `'User One'`. + + A JWT token is sent back in the response. + + For example: + + ```json + { + "token": "some.token.value" + } + ``` + +1. Scroll to the top of the API Explorer, and you should see an `Authorize` + button. This is the place where you can set the JWT token. + + ![](https://loopback.io/pages/en/lb4/imgs/api_explorer_authorize_button.png) + +1. Click on the `Authorize` button, and a dialog opens up. + + ![](https://loopback.io/pages/en/lb4/imgs/api_explorer_auth_token_dialog1.png) + +1. In the `bearerAuth` value field, enter the token string you obtained earlier, + and press the `Authorize` button. This JWT token is now available for the + `/users/me` endpoint we will interact with next. Press the `Close` button to + dismiss the dialog. + + ![](https://loopback.io/pages/en/lb4/imgs/api_explorer_auth_token_dialog2.png) + + {% include note.html content="The Logout button allows you to enter a new value; if needed. + " %} + +1. Scroll down to the `UserController` section to find `GET /users/me` + + ![](https://loopback.io/pages/en/lb4/imgs/api_explorer_usercontroller_section1.png) + + Notice it has a **lock** icon and the other endpoints in this section do not. + This is because this endpoint specified an operation-level + `security requirement object` in the OpenAPI specification. (For details, see + the + [Specifying the Security Settings in the OpenAPI Specification](#specifying-the-security-settings-in-the-openapi-specification) + section.) + +1. Expand the `GET /users/me` section, and click on `Try it out`. There is no + data to specify, so simply click on `Execute`. The JWT token you specified + earlier was automatically placed in the `Authorization` header of the + request. + + If authentication succeeds, the + [user profile](https://github.com/strongloop/loopback-next/blob/master/packages/security/src/types.ts) + of the currently authenticated user will be returned in the response. If + authentication fails due to a missing/invalid/expired token, an + [HTTP 401 UnAuthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) + is thrown. + + The response contains a unique value in the `id` field (generated by the + database) and `name` field with the full user name: + + ```json + {"id": "5dd6acee242760334f6aef65", "name": "User One"} + ``` + +## Adding JWT Authentication to a LoopBack 4 Application + +In this section, we will demonstrate how `authentication` was added to the +[loopback4-example-shopping](https://github.com/strongloop/loopback4-example-shopping) +application using the +[JSON Web Token (JWT)](https://en.wikipedia.org/wiki/JSON_Web_Token) approach. + +### Installing @loopback/authentication + +The `loopback4-example-shopping` application **already** has the +`@loopback/authentication` dependency set up in its **package.json** + +It was installed as a project dependency by performing: + +```sh +npm install --save @loopback/authentication +``` + +### Adding the AuthenticationComponent to the Application + +The core of authentication framework is found in the +[AuthenticationComponent](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/authentication.component.ts), +so it is important to add the component in the `ShoppingApplication` class in +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts). + +```ts +import {AuthenticationComponent} from '@loopback/authentication'; + +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + // ... + + // Bind authentication component related elements + this.component(AuthenticationComponent); + + // ... + } + // ... +} +``` + +### Securing an Endpoint with the Authentication Decorator + +Securing your application's API endpoints is done by decorating controller +functions with the +[Authentication Decorator](https://loopback.io/doc/en/lb4/Decorators_authenticate.html). + +The decorator's syntax is: + +```ts +@authenticate(strategyName: string, options?: object) +``` + +In the `loopback4-example-shopping` application, there is only one endpoint that +is secured. + +In the `UserController` class in the +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts), +a user can print out his/her user profile by performing a `GET` request on the +`/users/me` endpoint which is handled by the `printCurrentUser()` function. + +```ts + // ... + + @get('/users/me', { + responses: { + '200': { + description: 'The current user profile', + content: { + 'application/json': { + schema: UserProfileSchema, + }, + }, + }, + }, + }) + @authenticate('jwt') + async printCurrentUser( + @inject(SecurityBindings.USER) + currentUserProfile: UserProfile, + ): Promise { + currentUserProfile.id = currentUserProfile[securityId]; + delete currentUserProfile[securityId]; + return currentUserProfile; + } + + // ... +``` + +{% include note.html content="Since this controller method is obtaining SecurityBindings.USER via [method injection](https://loopback.io/doc/en/lb4/Dependency-injection.html#method-injection) (instead of [constructor injection](https://loopback.io/doc/en/lb4/Dependency-injection.html#constructor-injection) and this method is decorated with the @authenticate decorator, there is no need to specify @inject(SecurityBindings.USER, {optional:true}). See [Using the Authentication Decorator](https://loopback.io/doc/en/lb4/Loopback-component-authentication.html#using-the-authentication-decorator) for details. +" %} + +The `/users/me` endpoint is decorated with + +```ts +@authenticate('jwt') +``` + +and authentication will only succeed if a valid JWT token is provided in the +`Authorization` header of the request. + +Basically, the +[AuthenticateFn](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/providers/auth-action.provider.ts) +action in the custom sequence `MyAuthenticationSequence` (discussed in a later +section) asks +[AuthenticationStrategyProvider](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/providers/auth-strategy.provider.ts) +to resolve the registered authentication strategy with the name `'jwt'` (which +is `JWTAuthenticationStrategy` and discussed in a later section). Then +`AuthenticateFn` calls `JWTAuthenticationStrategy`'s `authenticate(request)` +function to authenticate the request. + +If the provided JWT token is valid, then `JWTAuthenticationStrategy`'s +`authenticate(request)` function returns the user profile. `AuthenticateFn` then +places the user profile on the request context using the `SecurityBindings.USER` +binding key. The user profile is available to the `printCurrentUser()` +controller function in a variable `currentUserProfile: UserProfile` through +dependency injection via the same `SecurityBindings.USER` binding key. The user +profile is returned in the response. + +If the JWT token is missing/expired/invalid, then `JWTAuthenticationStrategy`'s +`authenticate(request)` function fails and an +[HTTP 401 UnAuthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) +is thrown. + +If an **unknown** authentication strategy **name** is specified in the +`@authenticate` decorator: + +```ts +@authenticate('unknown') +``` + +then +[AuthenticationStrategyProvider](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/providers/auth-strategy.provider.ts)'s +`findAuthenticationStrategy(name: string)` function cannot find a registered +authentication strategy by that name, and an +[HTTP 401 UnAuthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) +is thrown. + +So, be sure to specify the correct authentication strategy name when decorating +your endpoints with the `@authenticate` decorator. + +### Creating a Custom Sequence and Adding the Authentication Action + +In a LoopBack 4 application with REST API endpoints, each request passes through +a stateless grouping of actions called a +[Sequence](https://loopback.io/doc/en/lb4/Sequence.html). + +Authentication is **not** part of the default sequence of actions, so you must +create a custom sequence and add the authentication action. + +The custom sequence `MyAuthenticationSequence` in +[loopback4-example-shopping/packages/shopping/src/sequence.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/sequence.ts) +implements the +[SequenceHandler](https://github.com/strongloop/loopback-next/blob/master/packages/rest/src/sequence.ts) +interface. + +```ts +export class MyAuthenticationSequence implements SequenceHandler { + constructor( + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) + protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) protected send: Send, + @inject(SequenceActions.REJECT) protected reject: Reject, + @inject(AuthenticationBindings.AUTH_ACTION) + protected authenticateRequest: AuthenticateFn, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + + //call authentication action + await this.authenticateRequest(request); + + // Authentication successful, proceed to invoke controller + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (error) { + // + // The authentication action utilizes a strategy resolver to find + // an authentication strategy by name, and then it calls + // strategy.authenticate(request). + // + // The strategy resolver throws a non-http error if it cannot + // resolve the strategy. When the strategy resolver obtains + // a strategy, it calls strategy.authenticate(request) which + // is expected to return a user profile. If the user profile + // is undefined, then it throws a non-http error. + // + // It is necessary to catch these errors and add HTTP-specific status + // code property. + // + // Errors thrown by the strategy implementations already come + // with statusCode set. + // + // In the future, we want to improve `@loopback/rest` to provide + // an extension point allowing `@loopback/authentication` to contribute + // mappings from error codes to HTTP status codes, so that application + // don't have to map codes themselves. + if ( + error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + error.code === USER_PROFILE_NOT_FOUND + ) { + Object.assign(error, {statusCode: 401 /* Unauthorized */}); + } + + this.reject(context, error); + return; + } + } +} +``` + +The authentication action/function is injected via the +`AuthenticationBindings.AUTH_ACTION` binding key, is given the name +`authenticateRequest` and has the type +[AuthenticateFn](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/types.ts). + +Calling + +```ts +await this.authenticateRequest(request); +``` + +before + +```ts +// ... +const result = await this.invoke(route, args); +this.send(response, result); +// ... +``` + +ensures that authentication has succeeded before a controller endpoint is +reached. + +To add the custom sequence `MyAuthenticationSequence` in the application, we +must code the following in +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts): + +```ts +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + // ... + + // Set up the custom sequence + this.sequence(MyAuthenticationSequence); + + // ... + } +} +``` + +### Creating a Custom JWT Authentication Strategy + +When creating a custom authentication strategy, it is necessary to implement the +[AuthenticationStrategy](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/types.ts) +interface. + +A custom JWT authentication strategy `JWTAuthenticationStrategy` in +[loopback4-example-shopping/packages/shopping/src/authentication-strategies/jwt-strategy.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/authentication-strategies/jwt-strategy.ts) +was implemented as follows: + +```ts +import {inject} from '@loopback/context'; +import {HttpErrors, Request} from '@loopback/rest'; +import {AuthenticationStrategy, TokenService} from '@loopback/authentication'; +import {UserProfile} from '@loopback/security'; + +import {TokenServiceBindings} from '../keys'; + +export class JWTAuthenticationStrategy implements AuthenticationStrategy { + name: string = 'jwt'; + + constructor( + @inject(TokenServiceBindings.TOKEN_SERVICE) + public tokenService: TokenService, + ) {} + + async authenticate(request: Request): Promise { + const token: string = this.extractCredentials(request); + const userProfile: UserProfile = await this.tokenService.verifyToken(token); + return userProfile; + } + + extractCredentials(request: Request): string { + if (!request.headers.authorization) { + throw new HttpErrors.Unauthorized(`Authorization header not found.`); + } + + // for example: Bearer xxx.yyy.zzz + const authHeaderValue = request.headers.authorization; + + if (!authHeaderValue.startsWith('Bearer')) { + throw new HttpErrors.Unauthorized( + `Authorization header is not of type 'Bearer'.`, + ); + } + + //split the string into 2 parts: 'Bearer ' and the `xxx.yyy.zzz` + const parts = authHeaderValue.split(' '); + if (parts.length !== 2) + throw new HttpErrors.Unauthorized( + `Authorization header value has too many parts. It must follow the pattern: 'Bearer xx.yy.zz' where xx.yy.zz is a valid JWT token.`, + ); + const token = parts[1]; + + return token; + } +} +``` + +It has a **name** `'jwt'`, and it implements the +`async authenticate(request: Request): Promise` +function. + +An extra function `extractCredentials(request: Request): string` was added to +extract the JWT token from the request. This authentication strategy expects +every request to pass a valid JWT token in the `Authorization` header. + +`JWTAuthenticationStrategy` also makes use of a token service `tokenService` of +type `TokenService` that is injected via the +`TokenServiceBindings.TOKEN_SERVICE` binding key. It is used to verify the +validity of the JWT token and return a user profile. + +This token service is explained in a later section. + +### Registering the Custom JWT Authentication Strategy + +To register the custom authentication strategy `JWTAuthenticationStrategy` with +the **name** `'jwt'` as a part of the authentication framework, we need to code +the following in +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts). + +```ts +import {registerAuthenticationStrategy} from '@loopback/authentication'; + +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + // ... + registerAuthenticationStrategy(this, JWTAuthenticationStrategy); + // ... + } +} +``` + +### Creating a Token Service + +The token service `JWTService` in +[loopback4-example-shopping/packages/shopping/src/services/jwt-service.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/services/jwt-service.ts) +implements an **optional** helper +[TokenService](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/services/token.service.ts) +interface. + +```ts +import {inject} from '@loopback/context'; +import {HttpErrors} from '@loopback/rest'; +import {promisify} from 'util'; +import {TokenService} from '@loopback/authentication'; +import {UserProfile} from '@loopback/security'; +import {TokenServiceBindings} from '../keys'; + +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +const verifyAsync = promisify(jwt.verify); + +export class JWTService implements TokenService { + constructor( + @inject(TokenServiceBindings.TOKEN_SECRET) + private jwtSecret: string, + @inject(TokenServiceBindings.TOKEN_EXPIRES_IN) + private jwtExpiresIn: string, + ) {} + + async verifyToken(token: string): Promise { + if (!token) { + throw new HttpErrors.Unauthorized( + `Error verifying token: 'token' is null`, + ); + } + + let userProfile: UserProfile; + + try { + // decode user profile from token + const decryptedToken = await verifyAsync(token, this.jwtSecret); + // don't copy over token field 'iat' and 'exp', nor 'email' to user profile + userProfile = Object.assign( + {id: '', name: ''}, + {id: decryptedToken.id, name: decryptedToken.name}, + ); + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token: ${error.message}`, + ); + } + + return userProfile; + } + + async generateToken(userProfile: UserProfile): Promise { + if (!userProfile) { + throw new HttpErrors.Unauthorized( + 'Error generating token: userProfile is null', + ); + } + + // Generate a JSON Web Token + let token: string; + try { + token = await signAsync(userProfile, this.jwtSecret, { + expiresIn: Number(this.jwtExpiresIn), + }); + } catch (error) { + throw new HttpErrors.Unauthorized(`Error encoding token: ${error}`); + } + + return token; + } +} +``` + +`JWTService` generates or verifies JWT tokens using the `sign` and `verify` +functions of [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken). + +It makes use of `jwtSecret` and `jwtExpiresIn` **string** values that are +injected via the `TokenServiceBindings.TOKEN_SECRET` and the +`TokenServiceBindings.TOKEN_EXPIRES_IN` binding keys respectively. + +The `async generateToken(userProfile: UserProfile): Promise` function +takes in a user profile of type +[UserProfile](https://github.com/strongloop/loopback-next/blob/master/packages/security/src/types.ts), +generates a JWT token of type `string` using: the **user profile** as the +payload, **jwtSecret** and **jwtExpiresIn**. + +The `async verifyToken(token: string): Promise` function takes in a +JWT token of type `string`, verifies the JWT token, and returns the payload of +the token which is a user profile of type `UserProfile`. + +To bind the JWT `secret`, `expires in` values and the `JWTService` class to +binding keys, we need to code the following in +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts): + +```ts +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + // ... + this.setUpBindings(); + // ... + } + + setUpBindings(): void { + // ... + + this.bind(TokenServiceBindings.TOKEN_SECRET).to( + TokenServiceConstants.TOKEN_SECRET_VALUE, + ); + + this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to( + TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE, + ); + + this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService); + + // ... + } +} +``` + +In the code above, `TOKEN_SECRET_VALUE` has a value of `'myjwts3cr3t'` and +`TOKEN_EXPIRES_IN_VALUE` has a value of `'600'`. + +`JWTService` is used in two places within the application: +`JWTAuthenticationStrategy` in +[loopback4-example-shopping/packages/shopping/src/authentication-strategies/jwt-strategy.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/authentication-strategies/jwt-strategy.ts), +and `UserController` in +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts). + +### Creating a User Service + +The user service `MyUserService` in +[loopback4-example-shopping/packages/shopping/src/services/user-service.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/services/user-service.ts) +implements an **optional** helper +[UserService](https://github.com/strongloop/loopback-next/blob/master/packages/authentication/src/services/user.service.ts) +interface. + +```ts +export class MyUserService implements UserService { + constructor( + @repository(UserRepository) public userRepository: UserRepository, + @inject(PasswordHasherBindings.PASSWORD_HASHER) + public passwordHasher: PasswordHasher, + ) {} + + async verifyCredentials(credentials: Credentials): Promise { + const foundUser = await this.userRepository.findOne({ + where: {email: credentials.email}, + }); + + if (!foundUser) { + throw new HttpErrors.NotFound( + `User with email ${credentials.email} not found.`, + ); + } + const passwordMatched = await this.passwordHasher.comparePassword( + credentials.password, + foundUser.password, + ); + + if (!passwordMatched) { + throw new HttpErrors.Unauthorized('The credentials are not correct.'); + } + + return foundUser; + } + + convertToUserProfile(user: User): UserProfile { + // since first name and lastName are optional, no error is thrown if not provided + let userName = ''; + if (user.firstName) userName = `${user.firstName}`; + if (user.lastName) + userName = user.firstName + ? `${userName} ${user.lastName}` + : `${user.lastName}`; + return {id: user.id, name: userName}; + } +} +``` + +The `async verifyCredentials(credentials: Credentials): Promise` function +takes in a credentials of type +[Credentials](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/repositories/user.repository.ts), +and returns a **user** of type +[User](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/models/user.model.ts). +It searches through an injected user repository of type `UserRepository`. + +The `convertToUserProfile(user: User): UserProfile` function takes in a **user** +of type `User` and returns a user profile of type +[UserProfile](https://github.com/strongloop/loopback-next/blob/master/packages/security/src/types.ts). +A user profile, in this case, is the minimum set of user properties which +identify an authenticated user. + +`MyUserService` is used in by `UserController` in +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts). + +To bind the `MyUserService` class, and the password hashing utility it uses, to +binding keys, we need to code the following in +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts): + +```ts +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + // ... + + this.setUpBindings(); + + // ... + } + + setUpBindings(): void { + // ... + + // Bind bcrypt hash services - utilized by 'UserController' and 'MyUserService' + this.bind(PasswordHasherBindings.ROUNDS).to(10); + this.bind(PasswordHasherBindings.PASSWORD_HASHER).toClass(BcryptHasher); + + this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService); + + // ... + } +} +``` + +### Adding Users + +In the `UserController` class in the +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts), +users can be added by performing a `POST` request to the `/users` endpoint which +is handled by the `create()` function. + +Because user credentials like password are stored outside of the main user +profile, we need to create a new model (a +[Data Transfer Object](https://en.wikipedia.org/wiki/Data_transfer_object)) to +describe data required to create a new user. The class inherits from `User` +model to include all user profile properties, and adds a new property `password` +allowing clients to specify the password too. + +```ts +@model() +export class NewUserRequest extends User { + @property({ + type: 'string', + required: true, + }) + password: string; +} +``` + +The controller method `UserController.create` then has to remove additional +properties like `password` before passing the data to Repository (and database). + +```ts +export class UserController { + constructor( + // ... + @repository(UserRepository) public userRepository: UserRepository, + @inject(PasswordHasherBindings.PASSWORD_HASHER) + public passwordHasher: PasswordHasher, + @inject(TokenServiceBindings.TOKEN_SERVICE) + public jwtService: TokenService, + @inject(UserServiceBindings.USER_SERVICE) + public userService: UserService, + ) {} + + // ... + + @post('/users') + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(NewUserRequest, { + title: 'NewUser', + }), + }, + }, + }) + newUserRequest: NewUserRequest, + ): Promise { + // ensure a valid email value and password value + validateCredentials(_.pick(newUserRequest, ['email', 'password'])); + + // encrypt the password + const password = await this.passwordHasher.hashPassword( + newUserRequest.password, + ); + + // create the new user + const savedUser = await this.userRepository.create( + _.omit(newUserRequest, 'password'), + ); + + // set the password + await this.userRepository + .userCredentials(savedUser.id) + .create({password}); + + return savedUser; + } + + // ... +``` + +A user of type +[User](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/models/user.model.ts) +is added to the database via the user repository if the user's email and +password values are in an acceptable format. + +### Issuing a JWT Token on Successful Login + +In the `UserController` class in the +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts), +a user can `log in` by performing a `POST` request, containing an `email` and +`password`, to the `/users/login` endpoint which is handled by the `login()` +function. + +```ts +export class UserController { + constructor( + // ... + @repository(UserRepository) public userRepository: UserRepository, + @inject(PasswordHasherBindings.PASSWORD_HASHER) + public passwordHasher: PasswordHasher, + @inject(TokenServiceBindings.TOKEN_SERVICE) + public jwtService: TokenService, + @inject(UserServiceBindings.USER_SERVICE) + public userService: UserService, + ) {} + + // ... + + @post('/users/login', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + token: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }) + async login( + @requestBody(CredentialsRequestBody) credentials: Credentials, + ): Promise<{token: string}> { + // ensure the user exists, and the password is correct + const user = await this.userService.verifyCredentials(credentials); + + // convert a User object into a UserProfile object (reduced set of properties) + const userProfile = this.userService.convertToUserProfile(user); + + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + + return {token}; + } +} +``` + +The user service returns a user object when the email and password are verified +as valid; otherwise it throws an +[HTTP 401 UnAuthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401). +The user service is then called to create a slimmer user profile from the user +object. Then this user profile is used as the payload of the JWT token created +by the token service. The token is returned in the response. + +### Specifying the Security Settings in the OpenAPI Specification + +In the shopping cart application, only one endpoint, `GET /users/me` is secured +with a custom JWT authentication strategy. In order to be able to `set` and +`use` a JWT token in the `API Explorer` (as opposed to using a REST API client), +it is necessary to specify +[security scheme object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#security-scheme-object) +and +[security requirement object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#securityRequirementObject) +information in the application's OpenAPI specification. + +In +[loopback4-example-shopping/packages/shopping/src/utils/security-spec.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/utils/security-spec.ts) +we defined the following: + +```ts +import {SecuritySchemeObject, ReferenceObject} from '@loopback/openapi-v3'; + +export const OPERATION_SECURITY_SPEC = [{bearerAuth: []}]; +export type SecuritySchemeObjects = { + [securityScheme: string]: SecuritySchemeObject | ReferenceObject; +}; +export const SECURITY_SCHEME_SPEC: SecuritySchemeObjects = { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, +}; +``` + +`SECURITY_SCHEME_SPEC` is a map of security scheme object definitions that are +defined globally for the application. For our purposes, it only contains a +single security scheme object that contains the +[bearerAuth](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#jwt-bearer-sample) +definition. + +`OPERATION_SECURITY_SPEC` is an **operation-level** +[security requirement object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#securityRequirementObject) +that references the `bearerAuth` security scheme object definition. It is used +by the `/users/me` endpoint in +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts). + +Notice the line + +``` +security: OPERATION_SECURITY_SPEC, +``` + +in the code below: + +```ts +@get('/users/me', { + security: OPERATION_SECURITY_SPEC, + responses: { + '200': { + description: 'The current user profile', + content: { + 'application/json': { + schema: UserProfileSchema, + }, + }, + }, + }, +}) +@authenticate('jwt') +async printCurrentUser( + @inject(SecurityBindings.USER) + currentUserProfile: UserProfile, +): Promise { + currentUserProfile.id = currentUserProfile[securityId]; + delete currentUserProfile[securityId]; + return currentUserProfile; +} +``` + +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts) +contributes the `security scheme object` definitions to the OpenAPI +specification in the following manner: + +```ts +import {SECURITY_SCHEME_SPEC} from './utils/security-spec'; + +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + this.api({ + openapi: '3.0.0', + info: {title: pkg.name, version: pkg.version}, + paths: {}, + components: {securitySchemes: SECURITY_SCHEME_SPEC}, + servers: [{url: '/'}], + }); +// ... +``` + +Later, when you visit +[http://[::1]:3000/openapi.json](http://[::1]:3000/openapi.json) while the +application is running, search for the text `bearerAuth`. You should find these +two occurrences: + +```json +"components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, +``` + +and + +```json +"/users/me": { + "get": { + "x-controller-name": "UserController", + "x-operation-name": "printCurrentUser", + "tags": [ + "UserController" + ], + "security": [ + { + "bearerAuth": [] + } + ], +``` + +Later, when you visit [http://[::1]:3000/explorer/](http://[::1]:3000/explorer/) +while the application is running, you should see an `Authorize` button at the +top. + +![](https://loopback.io/pages/en/lb4/imgs/api_explorer_authorize_button.png) + +as well as a **lock** icon on the `GET /users/me` endpoint in the +`UserController` section + +![](https://loopback.io/pages/en/lb4/imgs/api_explorer_usercontroller_section1.png) + +### How to Specify A Single OpenAPI Specification Security Requirement Object For All Endpoints + +Currently, the `loopback4-example-shopping` application does not implement this, +but there is a way to specify the same OpenAPI specification security +requirement object to **all** endpoints of your application. + +The security scheme object definition is still defined in `components` section, +but the `security` property value is set at the **top** level instead of the +**operation** level. + +```json +"components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + // ... +}, +"security": [ + { + "bearerAuth": [] + } + ] +``` + +To accomplish this, we only need to make some minor changes to the code examples +provided earlier. + +In +[loopback4-example-shopping/packages/shopping/src/utils/security-spec.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/utils/security-spec.ts), +we simply rename `OPERATION_SECURITY_SPEC` to `SECURITY_SPEC`. + +```ts +import {SecuritySchemeObject, ReferenceObject} from '@loopback/openapi-v3'; + +export const SECURITY_SPEC = [{bearerAuth: []}]; +export type SecuritySchemeObjects = { + [securityScheme: string]: SecuritySchemeObject | ReferenceObject; +}; +export const SECURITY_SCHEME_SPEC: SecuritySchemeObjects = { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, +}; +``` + +In +[loopback4-example-shopping/packages/shopping/src/controllers/user.controller.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/controllers/user.controller.ts), +we remove the line: + +``` +security: SECURITY_SPEC_OPERATION, +``` + +from the `/users/me` endpoint: + +```ts +@get('/users/me', { + responses: { + '200': { + description: 'The current user profile', + content: { + 'application/json': { + schema: UserProfileSchema, + }, + }, + }, + }, +}) +@authenticate('jwt') +async printCurrentUser( + @inject(SecurityBindings.USER) + currentUserProfile: UserProfile, +): Promise { + currentUserProfile.id = currentUserProfile[securityId]; + delete currentUserProfile[securityId]; + return currentUserProfile; +} +``` + +In +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts), +we simply add the line + +``` +security: SECURITY_SPEC +``` + +to the call to `this.api({...})`. This basically means the security requirement +object definition, `bearerAuth`, will be applied to all endpoints. + +```ts +import {SECURITY_SCHEME_SPEC, SECURITY_SPEC} from './utils/security-spec'; + +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + this.api({ + openapi: '3.0.0', + info: {title: pkg.name, version: pkg.version}, + paths: {}, + components: {securitySchemes: SECURITY_SCHEME_SPEC}, + servers: [{url: '/'}], + security: SECURITY_SPEC + }); +// ... +``` + +Visiting [http://[::1]:3000/explorer/](http://[::1]:3000/explorer/) while the +application is running, you should still see an `Authorize` button at the top as +before. + +![](https://loopback.io/pages/en/lb4/imgs/api_explorer_authorize_button.png) + +But now, **all** the endpoints have the lock icon. + +![](https://loopback.io/pages/en/lb4/imgs/api_explorer_all_sections_lock_icons1.png) + +This means that you can set the JWT token once via the +`Authorize button/dialog`, and the token will be available to all the endpoints +your interact with. + +There are plans to allow contributions to the OpenAPI specification via an +extensionPoint/extensions pattern ( +[Issue #3854](https://github.com/strongloop/loopback-next/issues/3854) ); +including having authentication strategies automatically contribute security +scheme/requirement object information ( +[Issue #3669](https://github.com/strongloop/loopback-next/issues/3669) ). + +### Summary + +We've gone through the steps that were used to add JWT `authentication` to the +`loopback4-example-shopping` application, and add +`security scheme/requirement object` settings to its OpenAPI specification. + +The final `ShoppingApplication` class in +[loopback4-example-shopping/packages/shopping/src/application.ts](https://github.com/strongloop/loopback4-example-shopping/blob/master/packages/shopping/src/application.ts) +should look like this: + +```ts +import {BootMixin} from '@loopback/boot'; +import {ApplicationConfig, BindingKey} from '@loopback/core'; +import {RepositoryMixin} from '@loopback/repository'; +import {RestApplication} from '@loopback/rest'; +import {ServiceMixin} from '@loopback/service-proxy'; +import {MyAuthenticationSequence} from './sequence'; +import { + RestExplorerBindings, + RestExplorerComponent, +} from '@loopback/rest-explorer'; +import { + TokenServiceBindings, + UserServiceBindings, + TokenServiceConstants, +} from './keys'; +import {JWTService} from './services/jwt-service'; +import {MyUserService} from './services/user-service'; + +import path from 'path'; +import { + AuthenticationComponent, + registerAuthenticationStrategy, +} from '@loopback/authentication'; +import {PasswordHasherBindings} from './keys'; +import {BcryptHasher} from './services/hash.password.bcryptjs'; +import {JWTAuthenticationStrategy} from './authentication-strategies/jwt-strategy'; +import {SECURITY_SCHEME_SPEC} from './utils/security-spec'; + +/** + * Information from package.json + */ +export interface PackageInfo { + name: string; + version: string; + description: string; +} +export const PackageKey = BindingKey.create('application.package'); + +const pkg: PackageInfo = require('../package.json'); + +export class ShoppingApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options?: ApplicationConfig) { + super(options); + + this.api({ + openapi: '3.0.0', + info: {title: pkg.name, version: pkg.version}, + paths: {}, + components: {securitySchemes: SECURITY_SCHEME_SPEC}, + servers: [{url: '/'}], + }); + + this.setUpBindings(); + + // Bind authentication component related elements + this.component(AuthenticationComponent); + + registerAuthenticationStrategy(this, JWTAuthenticationStrategy); + + // Set up the custom sequence + this.sequence(MyAuthenticationSequence); + + // Set up default home page + this.static('/', path.join(__dirname, '../public')); + + // Customize @loopback/rest-explorer configuration here + this.configure(RestExplorerBindings.COMPONENT).to({ + path: '/explorer', + }); + this.component(RestExplorerComponent); + + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + // Customize ControllerBooter Conventions here + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + }, + }; + } + + setUpBindings(): void { + // Bind package.json to the application context + this.bind(PackageKey).to(pkg); + + this.bind(TokenServiceBindings.TOKEN_SECRET).to( + TokenServiceConstants.TOKEN_SECRET_VALUE, + ); + + this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to( + TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE, + ); + + this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService); + + // // Bind bcrypt hash services + this.bind(PasswordHasherBindings.ROUNDS).to(10); + this.bind(PasswordHasherBindings.PASSWORD_HASHER).toClass(BcryptHasher); + + this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService); + } +} +``` + +## Running the Completed Application + +To run the completed application, follow the instructions in the +[Try it out](#try-it-out) section. + +For more information, please visit +[Authentication Component](https://loopback.io/doc/en/lb4/Loopback-component-authentication.html).