diff --git a/docs/site/imgs/auth-tutorial-apiexplorer.png b/docs/site/imgs/auth-tutorial-apiexplorer.png new file mode 100644 index 000000000000..de960eae75c1 Binary files /dev/null and b/docs/site/imgs/auth-tutorial-apiexplorer.png differ diff --git a/docs/site/imgs/auth-tutorial-auth-button.png b/docs/site/imgs/auth-tutorial-auth-button.png new file mode 100644 index 000000000000..a68210f7528a Binary files /dev/null and b/docs/site/imgs/auth-tutorial-auth-button.png differ diff --git a/docs/site/imgs/auth-tutorial-jwt-token.png b/docs/site/imgs/auth-tutorial-jwt-token.png new file mode 100644 index 000000000000..1a3d2dfe1757 Binary files /dev/null and b/docs/site/imgs/auth-tutorial-jwt-token.png differ diff --git a/docs/site/tutorials/authentication/Authentication-Tutorial.md b/docs/site/tutorials/authentication/Authentication-Tutorial.md index 12c70a66992f..b35dce5b44a3 100644 --- a/docs/site/tutorials/authentication/Authentication-Tutorial.md +++ b/docs/site/tutorials/authentication/Authentication-Tutorial.md @@ -13,10 +13,23 @@ 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. +This tutorial shows you how to add JWT authentication to your LoopBack +application using the extension +[`@loopback/authentication-jwt`](https://github.com/strongloop/loopback-next/tree/master/extensions/authentication-jwt). + +For demonstration purpose, we will be using the +[Todo application](https://loopback.io/doc/en/lb4/todo-tutorial.html) as the +base application. + +At the end of this tutorial, it supports the following scenarios: + +- User needs to log in first to get the token before calling any of the todo + endpoints +- User can sign up if they don’t have any account already +- If the token is valid, user can call the REST APIs, otherwise an error with + 401 status code will occur. + +## JSON Web Token Approach Here is a brief summary of the `JSON Web Token (JWT)` approach. @@ -48,1298 +61,251 @@ 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. - - ![](../../imgs/api_explorer_authorize_button.png) - -1. Click on the `Authorize` button, and a dialog opens up. - - ![](../../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. - - ![](../../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` - - ![](../../imgs/api_explorer_usercontroller_section1.png) +## Before we begin - 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: +Let’s download the Todo example and install the `@loopback/authentication-jwt` +extension. ```sh -npm install --save @loopback/authentication +$ lb4 example todo +$ cd loopback4-example-todo +$ npm i --save @loopback/authentication @loopback/authentication-jwt ``` -### Adding the AuthenticationComponent to the Application +## Step 1: Bind JWT Component in 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). +In `src/application.ts`, bind the authentication components to your application +class. + +{% include code-caption.html content="src/application.ts" %} ```ts +// ---------- ADD IMPORTS ------------- import {AuthenticationComponent} from '@loopback/authentication'; +import { + JWTAuthenticationComponent, + SECURITY_SCHEME_SPEC, + UserServiceBindings, +} from '@loopback/authentication-jwt'; +import {DbDataSource} from './datasources'; +// ------------------------------------ -export class ShoppingApplication extends BootMixin( +export class TodoListApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), ) { - constructor(options?: ApplicationConfig) { - super(options); - - // ... - - // Bind authentication component related elements + constructor(options: ApplicationConfig = {}) { + //... + // ------ ADD SNIPPET AT THE BOTTOM --------- + // Mount authentication system this.component(AuthenticationComponent); - - // ... + // Mount jwt component + this.component(JWTAuthenticationComponent); + // Bind datasource + this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + // ------------- END OF SNIPPET ------------- } - // ... } ``` -### Securing an Endpoint with the Authentication Decorator - -Securing your application's API endpoints is done by decorating controller -functions with the -[Authentication Decorator](../../decorators/Decorators_authenticate.md). - -The decorator's syntax is: - -```ts -@authenticate(strategyName: string, options?: object) -``` +## Step 2: Add Authenticate Action -In the `loopback4-example-shopping` application, there is only one endpoint that -is secured. +Next, we will add the authenticate action in the Sequence. We’ll also modify the +error when authentication fails to return status code 401. -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. +{% include code-caption.html content="/src/sequence.ts" %} ```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](../../Dependency-injection.md#method-injection) (instead of [constructor injection](../../Dependency-injection.md#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](../../Loopback-component-authentication.md#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](../../Sequence.md). - -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 { +// ---------- ADD IMPORTS ------------- +import { + AuthenticateFn, + AuthenticationBindings, + AUTHENTICATION_STRATEGY_NOT_FOUND, + USER_PROFILE_NOT_FOUND, +} from '@loopback/authentication'; +// ------------------------------------ +export class MySequence 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, + // ---- ADD THIS LINE ------ @inject(AuthenticationBindings.AUTH_ACTION) protected authenticateRequest: AuthenticateFn, ) {} - async handle(context: RequestContext) { try { const {request, response} = context; const route = this.findRoute(request); - - //call authentication action + // - enable jwt auth - + // call authentication action + // ---------- ADD THIS LINE ------------- 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. + } catch (err) { + // ---------- ADD THIS SNIPPET ------------- + // if error is coming from the JWT authentication extension + // make the statusCode 401 if ( - error.code === AUTHENTICATION_STRATEGY_NOT_FOUND || - error.code === USER_PROFILE_NOT_FOUND + err.code === AUTHENTICATION_STRATEGY_NOT_FOUND || + err.code === USER_PROFILE_NOT_FOUND ) { - Object.assign(error, {statusCode: 401 /* Unauthorized */}); + Object.assign(err, {statusCode: 401 /* Unauthorized */}); } - - this.reject(context, error); - return; + // ---------- END OF SNIPPET ------------- + this.reject(context, err); } } } ``` -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. +## Step 3: Create the UserController for login -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): +In the UserController, we are going to create three endpoints: -```ts -export class ShoppingApplication extends BootMixin( - ServiceMixin(RepositoryMixin(RestApplication)), -) { - constructor(options?: ApplicationConfig) { - super(options); +- `/login` for users to provide credential to login +- `/whoami` for showing who is the current user +- `/signup` for users to sign up - // ... - - // Set up the custom sequence - this.sequence(MyAuthenticationSequence); - - // ... - } -} -``` +### 3a. Create the UserController -### Creating a Custom JWT Authentication Strategy +Create the controller using `lb4 controller` command with the empty controller +option. -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; +```sh +$ lb4 controller +? Controller class name: User +Controller User will be created in src/controllers/user.controller.ts - if (!authHeaderValue.startsWith('Bearer')) { - throw new HttpErrors.Unauthorized( - `Authorization header is not of type 'Bearer'.`, - ); - } +? What kind of controller would you like to generate? Empty Controller - //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]; +create src/controllers/user.controller.ts + update src/controllers/index.ts - return token; - } -} +Controller User was created in src/controllers/ ``` -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. +### 3b. Add endpoints in UserController -### Registering the Custom JWT Authentication Strategy +For UserService which verifies the credentials, we are going to use the default +implementation in the `@loopback/authentication-jwt` extension. Therefore, the +constructor injects the `MyUserService` from this extension. -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). +{% include code-caption.html content="/src/controllers/user.controller.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'; +// ---------- ADD IMPORTS ------------- +import {inject} from '@loopback/core'; +import { + TokenServiceBindings, + MyUserService, + UserServiceBindings, + UserRepository, +} from '@loopback/authentication-jwt'; 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); +import {SecurityBindings, UserProfile} from '@loopback/security'; +import {repository} from '@loopback/repository'; +// ---------------------------------- - // ... - 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, +constructor( @inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService, @inject(UserServiceBindings.USER_SERVICE) - public userService: UserService, + public userService: MyUserService, + @inject(SecurityBindings.USER, {optional: true}) + public user: UserProfile, + @repository(UserRepository) protected userRepository: UserRepository, ) {} - - // ... - - @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. +For the implementation of all the 3 endpoints, you can take a look at this +[user.controller.ts](https://github.com/strongloop/loopback-next/blob/master/examples/todo-jwt/src/controllers/user.controller.ts) +in the [`todo-jwt` example]. -### Issuing a JWT Token on Successful Login +## Step 4: Protect the Todo APIs -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. +Finally, we need to protect other endpoints that we have, i.e. the `/todos` +APIs. Go to `src/controllers/todo.controller.ts`. Simple add +`@authenticate('jwt')` before the `TodoController` class. This will protect all +the APIs in this controller. -```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: +{% include code-caption.html content="/src/controllers/user.controller.ts" %} ```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; +// ---------- ADD IMPORTS ------------- +import {authenticate} from '@loopback/authentication'; +// ------------------------------------ +@authenticate('jwt') // <---- Apply the @authenticate decorator at the class level +export class TodoController { + //... } ``` -[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'; +If there are particular API that you want to make it available to everyone +without authentication, you can add `@authenticate.skip()` before that function. +See https://loopback.io/doc/en/lb4/Decorators_authenticate.html for more +details. -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. - -![](../../imgs/api_explorer_authorize_button.png) - -as well as a **lock** icon on the `GET /users/me` endpoint in the -`UserController` section - -![](../../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. - -![](../../imgs/api_explorer_authorize_button.png) - -But now, **all** the endpoints have the lock icon. - -![](../../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); +## Try it out - this.bind(TokenServiceBindings.TOKEN_SECRET).to( - TokenServiceConstants.TOKEN_SECRET_VALUE, - ); +Start the application by running npm start and go to +http://localhost:3000/explorer. You’ll see the 3 new endpoints under +`UserController` together with the other endpoints under `TodoController`. - this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to( - TokenServiceConstants.TOKEN_EXPIRES_IN_VALUE, - ); +![](../../imgs/auth-tutorial-apiexplorer.png) - this.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(JWTService); +1. Sign up using the `/signup` API - // // Bind bcrypt hash services - this.bind(PasswordHasherBindings.ROUNDS).to(10); - this.bind(PasswordHasherBindings.PASSWORD_HASHER).toClass(BcryptHasher); + Since we don’t have any users created, click on `POST /signup`. For the + requestBody, the minimum you need is `email` and `password`. i.e. - this.bind(UserServiceBindings.USER_SERVICE).toClass(MyUserService); - } -} -``` + ```json + { + "email": "testuser2@abc.com", + "password": "testuser2" + } + ``` -## Running the Completed Application +2. Log in using the `POST /users/login` API -To run the completed application, follow the instructions in the -[Try it out](#try-it-out) section. + After calling `/users/login`, the response body will look something like: -For more information, please visit -[Authentication Component](../../Loopback-component-authentication.md). + ```json + { + "token": "aaaaaaaaa.aaaaaaaaaaaaaaaaa" + } + ``` -## Bugs/Feedback + Copy the token. Go to the top of the API Explorer, click the “Authorize” + button. -Open an issue in -[loopback4-example-shopping](https://github.com/strongloop/loopback4-example-shopping) -and we'll take a look! + ![API Explorer with Authorize Button](../../imgs/auth-tutorial-auth-button.png) -## Contributions + Paste the token that you previously copied to the “Value” field and then + click Authorize. -- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) -- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + ![authorize dialog](../../imgs/auth-tutorial-jwt-token.png) -## Tests + In the future API calls, this token will be added to the `Authorization` + header . -Run `npm test` from the root folder. +3. Get all todos using `GET /todos` API You should be able to call this API + successfully. -## Contributors +## Conclusion -See -[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). +Congratulations! You have successfully added the JWT authentication to the +LoopBack application! We did the following: -## License +- bind the JWT Component in the Application +- add Authenticate Action in the sequence +- create the UserController for login and signup functions +- protect the APIs by adding the `@authenticate` decorator -MIT +See the +[todo-jwt example](https://github.com/strongloop/loopback-next/blob/master/examples/todo-jwt) +for the working application. You can also run `lb4 example todo-jwt` to download +the example.