From fa8bf5dacb6eba6179ec4aa06fa3571496e9c0fc Mon Sep 17 00:00:00 2001 From: Madaky <17172989+madaky@users.noreply.github.com> Date: Thu, 28 May 2020 12:39:51 +0530 Subject: [PATCH] feat(authentication-jwt): implementation of refresh token feature refresh token implementation through Service chore(authentication-jwt): readme updated readme.md to use refresh token and extra configurations chore(context): readmemd refactor Apply suggestions from code review Co-authored-by: Diana Lau feat: implemented awthentication-jwt refreshtoken services Signed-off-by: Madaky <17172989+madaky@users.noreply.github.com> --- extensions/authentication-jwt/README.md | 84 +++++++++++- .../acceptance/jwt.component.test.ts | 26 +++- .../src/__tests__/fixtures/application.ts | 8 +- .../fixtures/controllers/user.controller.ts | 109 ++++++++++++++- .../src/__tests__/unit/jwt.service.ts | 3 +- extensions/authentication-jwt/src/index.ts | 1 + .../src/jwt-authentication-component.ts | 29 +++- extensions/authentication-jwt/src/keys.ts | 47 +++++++ .../authentication-jwt/src/models/index.ts | 1 + .../src/models/refresh-token.model.ts | 37 +++++ .../src/repositories/index.ts | 1 + .../repositories/refresh-token.repository.ts | 17 +++ .../authentication-jwt/src/services/index.ts | 1 + .../src/services/refreshtoken.service.ts | 128 ++++++++++++++++++ .../src/services/user.service.ts | 18 ++- extensions/authentication-jwt/src/types.ts | 33 +++++ 16 files changed, 532 insertions(+), 11 deletions(-) create mode 100644 extensions/authentication-jwt/src/models/refresh-token.model.ts create mode 100644 extensions/authentication-jwt/src/repositories/refresh-token.repository.ts create mode 100644 extensions/authentication-jwt/src/services/refreshtoken.service.ts create mode 100644 extensions/authentication-jwt/src/types.ts diff --git a/extensions/authentication-jwt/README.md b/extensions/authentication-jwt/README.md index 9b5c7e97a0c8..a3496533edbf 100644 --- a/extensions/authentication-jwt/README.md +++ b/extensions/authentication-jwt/README.md @@ -88,7 +88,7 @@ export class MySequence implements SequenceHandler { - mount jwt component in application - +- bind datasource to user service and refresh token
Check The Code @@ -116,8 +116,10 @@ export class TestApplication extends BootMixin( this.component(AuthenticationComponent); // Mount jwt component this.component(JWTAuthenticationComponent); - // Bind datasource + // Bind datasource for user this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + // Bind datasource for refresh token + this.dataSource(DbDataSource, RefreshTokenBindings.DATASOURCE_NAME); this.component(RestExplorerComponent); this.projectRoot = __dirname; @@ -140,7 +142,24 @@ login, then decorate endpoints with `@authentication('jwt')` to inject the logged in user's profile. This module contains an example application in the `fixtures` folder. It has a -controller with endpoints `/login` and `/whoAmI`. +controller with endpoints `/login`, `/refreshlogin`, `/refresh` and `/whoAmI`. + +Before using the below snippet do not forget to inject below repositories and +bindingings +in your controller's constructor + +```ts + @inject(TokenServiceBindings.TOKEN_SERVICE) + public jwtService: TokenService, + @inject(UserServiceBindings.USER_SERVICE) + public userService: UserService, + @inject(SecurityBindings.USER, {optional: true}) + private user: UserProfile, + @inject(UserServiceBindings.USER_REPOSITORY) + public userRepository: UserRepository, + @inject(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE) + public refreshService: RefreshTokenService, +``` The code snippet for login function: @@ -170,6 +189,44 @@ The code snippet for whoAmI function: } ``` +### Endpoints with refresh token + +To add refresh token mechanism in your app, you can follow below example code at +the endpoint. + +1. `To generate refresh token` : to generate the refresh token and access token + when user logins to your app with provided credentials. + +```ts + async refreshLogin( + @requestBody(CredentialsRequestBody) credentials: Credentials, + ): Promise { + // 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: UserProfile = this.userService.convertToUserProfile( + user, + ); + const accessToken = await this.jwtService.generateToken(userProfile); + const tokens = await this.refreshService.generateToken( + userProfile, + accessToken, + ); + return tokens; + } +``` + +2. `To refresh the token`: to generate the access token by the refresh token + obtained from the the last login endpoint. + +```ts + async refresh( + @requestBody(RefreshGrantRequestBody) refreshGrant: RefreshGrant, + ): Promise { + return this.refreshService.refreshToken(refreshGrant.refreshToken); + } +``` + The complete file is in [user.controller.ts](https://github.com/strongloop/loopback-next/tree/master/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts) @@ -298,6 +355,27 @@ provide your own `User` model and repository. } ``` +### Extra configurations + +1. To change the token secret in your application.ts + +``` + // for jwt access token + this.bind(TokenServiceBindings.TOKEN_SECRET).to(""); + // for refresh token + this.bind(RefreshTokenServiceBindings.TOKEN_SECRET).to(""); +``` + +2. To change token expiration. to learn more about expiration time here at + [Ziet/ms](https://github.com/zeit/ms) + +``` + // for jwt access token expiration + this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to(""); + // for refresh token expiration + this.bind(RefreshTokenServiceBindings.TOKEN_EXPIRES_IN).to(""); +``` + ## Future Work The security specification is currently manually added in the application file. diff --git a/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts b/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts index 527360344622..15e2fd9a4c2d 100644 --- a/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts +++ b/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts @@ -21,7 +21,8 @@ describe('jwt authentication', () => { let client: Client; let token: string; let userRepo: UserRepository; - + let refreshToken: string; + let tokenAuth: string; before(givenRunningApplication); before(() => { client = createRestAppClient(app); @@ -50,6 +51,29 @@ describe('jwt authentication', () => { expect(spec.components?.securitySchemes).to.eql(SECURITY_SCHEME_SPEC); }); + it(`user login and token granted successfully`, async () => { + const credentials = {email: 'jane@doe.com', password: 'opensesame'}; + const res = await client + .post('/users/refresh-login') + .send(credentials) + .expect(200); + refreshToken = res.body.refreshToken; + }); + + it(`user sends refresh token and new access token issued`, async () => { + const tokenArg = {refreshToken: refreshToken}; + const res = await client.post('/refresh/').send(tokenArg).expect(200); + tokenAuth = res.body.accessToken; + }); + + it('whoAmI returns the login user id using token generated from refresh', async () => { + const res = await client + .get('/whoAmI') + .set('Authorization', 'Bearer ' + tokenAuth) + .expect(200); + expect(res.text).to.equal('f48b7167-8d95-451c-bbfc-8a12cd49e763'); + }); + /* ============================================================================ TEST HELPERS diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/application.ts b/extensions/authentication-jwt/src/__tests__/fixtures/application.ts index fb4b44288ab4..7044cf2bf6b8 100644 --- a/extensions/authentication-jwt/src/__tests__/fixtures/application.ts +++ b/extensions/authentication-jwt/src/__tests__/fixtures/application.ts @@ -11,7 +11,11 @@ import {RestApplication} from '@loopback/rest'; import {RestExplorerComponent} from '@loopback/rest-explorer'; import {ServiceMixin} from '@loopback/service-proxy'; import path from 'path'; -import {JWTAuthenticationComponent, UserServiceBindings} from '../../'; +import { + JWTAuthenticationComponent, + UserServiceBindings, + RefreshTokenServiceBindings, +} from '../../'; import {DbDataSource} from './datasources/db.datasource'; import {MySequence} from './sequence'; @@ -34,6 +38,8 @@ export class TestApplication extends BootMixin( this.component(JWTAuthenticationComponent); // Bind datasource this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + //Bind datasource for refreshtoken table + this.dataSource(DbDataSource, RefreshTokenServiceBindings.DATASOURCE_NAME); this.component(RestExplorerComponent); this.projectRoot = __dirname; diff --git a/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts b/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts index 33b5a2734174..41a7a5935601 100644 --- a/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts +++ b/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts @@ -13,10 +13,43 @@ import {model, property} from '@loopback/repository'; import {get, post, requestBody} from '@loopback/rest'; import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; import {genSalt, hash} from 'bcryptjs'; -import {TokenServiceBindings, User, UserServiceBindings} from '../../../'; +import { + RefreshTokenServiceBindings, + TokenObject, + TokenServiceBindings, + User, + UserServiceBindings, +} from '../../../'; import {UserRepository} from '../../../repositories'; import {Credentials} from '../../../services/user.service'; +import {RefreshTokenService} from '../../../types'; + +// Describes the type of grant object taken in by method "refresh" +type RefreshGrant = { + refreshToken: string; +}; + +// Describes the schema of grant object +const RefreshGrantSchema = { + type: 'object', + required: ['refreshToken'], + properties: { + refreshToken: { + type: 'string', + }, + }, +}; + +// Describes the request body of grant object +const RefreshGrantRequestBody = { + description: 'Reissuing Acess Token', + required: true, + content: { + 'application/json': {schema: RefreshGrantSchema}, + }, +}; +// Describe the schema of user credentials const CredentialsSchema = { type: 'object', required: ['email', 'password'], @@ -59,6 +92,8 @@ export class UserController { private user: UserProfile, @inject(UserServiceBindings.USER_REPOSITORY) public userRepository: UserRepository, + @inject(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE) + public refreshService: RefreshTokenService, ) {} @post('/users/signup', { @@ -94,6 +129,11 @@ export class UserController { return savedUser; } + /** + * A login function that returns an access token. After login, include the token + * in the next requests to verify your identity. + * @param credentials User email and password + */ @post('/users/login', { responses: { '200': { @@ -142,4 +182,71 @@ export class UserController { async whoAmI(): Promise { return this.user[securityId]; } + /** + * A login function that returns refresh token and access token. + * @param credentials User email and password + */ + @post('/users/refresh-login', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + accessToken: { + type: 'string', + }, + refreshToken: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }) + async refreshLogin( + @requestBody(CredentialsRequestBody) credentials: Credentials, + ): Promise { + // 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: UserProfile = this.userService.convertToUserProfile( + user, + ); + const accessToken = await this.jwtService.generateToken(userProfile); + const tokens = await this.refreshService.generateToken( + userProfile, + accessToken, + ); + return tokens; + } + + @post('/refresh', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + accessToken: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + }) + async refresh( + @requestBody(RefreshGrantRequestBody) refreshGrant: RefreshGrant, + ): Promise { + return this.refreshService.refreshToken(refreshGrant.refreshToken); + } } diff --git a/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts b/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts index 283d9bbf430b..901b5926a8ee 100644 --- a/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts +++ b/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts @@ -19,8 +19,10 @@ describe('token service', () => { id: '1', name: 'test', }; + const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; const TOKEN_EXPIRES_IN_VALUE = '60'; + const jwtService = new JWTService(TOKEN_SECRET_VALUE, TOKEN_EXPIRES_IN_VALUE); it('token service generateToken() succeeds', async () => { @@ -31,7 +33,6 @@ describe('token service', () => { it('token service verifyToken() succeeds', async () => { const token = await jwtService.generateToken(USER_PROFILE); const userProfileFromToken = await jwtService.verifyToken(token); - expect(userProfileFromToken).to.deepEqual(DECODED_USER_PROFILE); }); diff --git a/extensions/authentication-jwt/src/index.ts b/extensions/authentication-jwt/src/index.ts index 9a66b82b84eb..141367bdf58a 100644 --- a/extensions/authentication-jwt/src/index.ts +++ b/extensions/authentication-jwt/src/index.ts @@ -8,3 +8,4 @@ export * from './keys'; export * from './models'; export * from './repositories'; export * from './services'; +export * from './types'; diff --git a/extensions/authentication-jwt/src/jwt-authentication-component.ts b/extensions/authentication-jwt/src/jwt-authentication-component.ts index 6fa2ae8942a7..4b77d20822e0 100644 --- a/extensions/authentication-jwt/src/jwt-authentication-component.ts +++ b/extensions/authentication-jwt/src/jwt-authentication-component.ts @@ -13,12 +13,18 @@ import { inject, } from '@loopback/core'; import { + RefreshTokenConstants, + RefreshTokenServiceBindings, TokenServiceBindings, TokenServiceConstants, UserServiceBindings, } from './keys'; -import {UserCredentialsRepository, UserRepository} from './repositories'; -import {MyUserService} from './services'; +import { + RefreshTokenRepository, + UserCredentialsRepository, + UserRepository, +} from './repositories'; +import {MyUserService, RefreshtokenService} from './services'; import {JWTAuthenticationStrategy} from './services/jwt.auth.strategy'; import {JWTService} from './services/jwt.service'; import {SecuritySpecEnhancer} from './services/security.spec.enhancer'; @@ -41,6 +47,25 @@ export class JWTAuthenticationComponent implements Component { UserCredentialsRepository, ), createBindingFromClass(SecuritySpecEnhancer), + ///refresh bindings + Binding.bind(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE).toClass( + RefreshtokenService, + ), + + // Refresh token bindings + Binding.bind(RefreshTokenServiceBindings.REFRESH_SECRET).to( + RefreshTokenConstants.REFRESH_SECRET_VALUE, + ), + Binding.bind(RefreshTokenServiceBindings.REFRESH_EXPIRES_IN).to( + RefreshTokenConstants.REFRESH_EXPIRES_IN_VALUE, + ), + Binding.bind(RefreshTokenServiceBindings.REFRESH_ISSUER).to( + RefreshTokenConstants.REFRESH_ISSUER_VALUE, + ), + //refresh token repository binding + Binding.bind(RefreshTokenServiceBindings.REFRESH_REPOSITORY).toClass( + RefreshTokenRepository, + ), ]; constructor(@inject(CoreBindings.APPLICATION_INSTANCE) app: Application) { registerAuthenticationStrategy(app, JWTAuthenticationStrategy); diff --git a/extensions/authentication-jwt/src/keys.ts b/extensions/authentication-jwt/src/keys.ts index 3e05d9a88aa0..50ad2f75b2c3 100644 --- a/extensions/authentication-jwt/src/keys.ts +++ b/extensions/authentication-jwt/src/keys.ts @@ -7,6 +7,7 @@ import {TokenService, UserService} from '@loopback/authentication'; import {BindingKey} from '@loopback/core'; import {User} from './models'; import {Credentials} from './services/user.service'; +import {RefreshTokenService} from './types'; export namespace TokenServiceConstants { export const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; @@ -34,3 +35,49 @@ export namespace UserServiceBindings { export const USER_CREDENTIALS_REPOSITORY = 'repositories.UserCredentialsRepository'; } + +/** + * Constant values used when generating refresh token. + */ +export namespace RefreshTokenConstants { + /** + * The default secret used when generating refresh token. + */ + export const REFRESH_SECRET_VALUE = 'r3fr35htok3n'; + /** + * The default expiration time for refresh token. + */ + export const REFRESH_EXPIRES_IN_VALUE = '216000'; + /** + * The default issuer used when generating refresh token. + */ + export const REFRESH_ISSUER_VALUE = 'loopback4'; +} + +/** + * Bindings related to token refresh service. The omitted explanation can be + * found in namespace `RefreshTokenConstants`. + */ +export namespace RefreshTokenServiceBindings { + export const REFRESH_TOKEN_SERVICE = BindingKey.create( + 'services.authentication.jwt.refresh.tokenservice', + ); + export const REFRESH_SECRET = BindingKey.create( + 'authentication.jwt.refresh.secret', + ); + export const REFRESH_EXPIRES_IN = BindingKey.create( + 'authentication.jwt.refresh.expires.in.seconds', + ); + export const REFRESH_ISSUER = BindingKey.create( + 'authentication.jwt.refresh.issuer', + ); + /** + * The backend datasource for refresh token's persistency. + */ + export const DATASOURCE_NAME = 'refreshdb'; + /** + * Key for the repository that stores the refresh token and its bound user + * information + */ + export const REFRESH_REPOSITORY = 'repositories.RefreshTokenRepository'; +} diff --git a/extensions/authentication-jwt/src/models/index.ts b/extensions/authentication-jwt/src/models/index.ts index 22b98273688b..486b0608195e 100644 --- a/extensions/authentication-jwt/src/models/index.ts +++ b/extensions/authentication-jwt/src/models/index.ts @@ -5,3 +5,4 @@ export * from './user-credentials.model'; export * from './user.model'; +export * from './refresh-token.model'; diff --git a/extensions/authentication-jwt/src/models/refresh-token.model.ts b/extensions/authentication-jwt/src/models/refresh-token.model.ts new file mode 100644 index 000000000000..54ac80d841e8 --- /dev/null +++ b/extensions/authentication-jwt/src/models/refresh-token.model.ts @@ -0,0 +1,37 @@ +import {belongsTo, Entity, model, property} from '@loopback/repository'; +import {User} from '.'; + +@model({settings: {strict: false}}) +export class RefreshToken extends Entity { + @property({ + type: 'number', + id: 1, + generated: true, + }) + id: number; + + @belongsTo(() => User) + userId: string; + + @property({ + type: 'string', + required: true, + }) + refreshToken: string; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface RefreshTokenRelations { + // describe navigational properties here +} + +export type RefereshTokenWithRelations = RefreshToken & RefreshTokenRelations; diff --git a/extensions/authentication-jwt/src/repositories/index.ts b/extensions/authentication-jwt/src/repositories/index.ts index bd75740ca210..02bb9b6e4d3b 100644 --- a/extensions/authentication-jwt/src/repositories/index.ts +++ b/extensions/authentication-jwt/src/repositories/index.ts @@ -5,3 +5,4 @@ export * from './user-credentials.repository'; export * from './user.repository'; +export * from './refresh-token.repository'; diff --git a/extensions/authentication-jwt/src/repositories/refresh-token.repository.ts b/extensions/authentication-jwt/src/repositories/refresh-token.repository.ts new file mode 100644 index 000000000000..b0f6db53c262 --- /dev/null +++ b/extensions/authentication-jwt/src/repositories/refresh-token.repository.ts @@ -0,0 +1,17 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository, juggler} from '@loopback/repository'; +import {RefreshToken, RefreshTokenRelations} from '../models'; +import {RefreshTokenServiceBindings} from '../keys'; + +export class RefreshTokenRepository extends DefaultCrudRepository< + RefreshToken, + typeof RefreshToken.prototype.id, + RefreshTokenRelations +> { + constructor( + @inject(`datasources.${RefreshTokenServiceBindings.DATASOURCE_NAME}`) + dataSource: juggler.DataSource, + ) { + super(RefreshToken, dataSource); + } +} diff --git a/extensions/authentication-jwt/src/services/index.ts b/extensions/authentication-jwt/src/services/index.ts index c611cee39d63..0ec9d3079ee3 100644 --- a/extensions/authentication-jwt/src/services/index.ts +++ b/extensions/authentication-jwt/src/services/index.ts @@ -7,3 +7,4 @@ export * from './jwt.auth.strategy'; export * from './jwt.service'; export * from './security.spec.enhancer'; export * from './user.service'; +export * from './refreshtoken.service'; diff --git a/extensions/authentication-jwt/src/services/refreshtoken.service.ts b/extensions/authentication-jwt/src/services/refreshtoken.service.ts new file mode 100644 index 000000000000..bda1d724cbec --- /dev/null +++ b/extensions/authentication-jwt/src/services/refreshtoken.service.ts @@ -0,0 +1,128 @@ +import {TokenService} from '@loopback/authentication'; +import {bind, BindingScope, inject, uuid} from '@loopback/core'; +import {repository} from '@loopback/repository'; +import {HttpErrors} from '@loopback/rest'; +import {securityId, UserProfile} from '@loopback/security'; +import {promisify} from 'util'; +import { + RefreshTokenServiceBindings, + TokenServiceBindings, + UserServiceBindings, +} from '../keys'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import {RefreshToken, RefreshTokenRelations} from '../models'; +import {RefreshTokenRepository} from '../repositories'; +import {TokenObject} from '../types'; +import {MyUserService} from './user.service'; +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +const verifyAsync = promisify(jwt.verify); + +@bind({scope: BindingScope.TRANSIENT}) +export class RefreshtokenService { + constructor( + @inject(RefreshTokenServiceBindings.REFRESH_SECRET) + private refreshSecret: string, + @inject(RefreshTokenServiceBindings.REFRESH_EXPIRES_IN) + private refreshExpiresIn: string, + @inject(RefreshTokenServiceBindings.REFRESH_ISSUER) + private refreshIssure: string, + @repository(RefreshTokenRepository) + public refreshTokenRepository: RefreshTokenRepository, + @inject(UserServiceBindings.USER_SERVICE) public userService: MyUserService, + @inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService, + ) {} + /** + * Generate a refresh token, bind it with the given user profile + access + * token, then store them in backend. + */ + async generateToken( + userProfile: UserProfile, + token: string, + ): Promise { + const data = { + token: uuid(), + }; + const refreshToken = await signAsync(data, this.refreshSecret, { + expiresIn: Number(this.refreshExpiresIn), + issuer: this.refreshIssure, + }); + const result = { + accessToken: token, + refreshToken: refreshToken, + }; + await this.refreshTokenRepository.create({ + userId: userProfile[securityId], + refreshToken: result.refreshToken, + }); + return result; + } + + /* + * Refresh the access token bound with the given refresh token. + */ + async refreshToken(refreshToken: string): Promise { + try { + if (!refreshToken) { + throw new HttpErrors.Unauthorized( + `Error verifying token : 'refresh token' is null`, + ); + } + + const userRefreshData = await this.verifyToken(refreshToken); + const user = await this.userService.findUserById( + userRefreshData.userId.toString(), + ); + const userProfile: UserProfile = this.userService.convertToUserProfile( + user, + ); + // create a JSON Web Token based on the user profile + const token = await this.jwtService.generateToken(userProfile); + + return { + accessToken: token, + }; + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + } + + /* + * [TODO] test and endpoint + */ + async revokeToken(refreshToken: string) { + try { + await this.refreshTokenRepository.delete( + new RefreshToken({refreshToken: refreshToken}), + ); + } catch (e) { + // ignore + } + } + + /** + * Verify the validity of a refresh token, and make sure it exists in backend. + * @param refreshToken + */ + async verifyToken(refreshToken: string) { + try { + await verifyAsync(refreshToken, this.refreshSecret); + const userRefreshData = await this.refreshTokenRepository.findOne({ + where: {refreshToken: refreshToken}, + }); + + if (!userRefreshData) { + throw new HttpErrors.Unauthorized( + `Error verifying token : Invalid Token`, + ); + } + return userRefreshData; + } catch (error) { + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + } +} diff --git a/extensions/authentication-jwt/src/services/user.service.ts b/extensions/authentication-jwt/src/services/user.service.ts index 45e825bef89d..975b06e2629a 100644 --- a/extensions/authentication-jwt/src/services/user.service.ts +++ b/extensions/authentication-jwt/src/services/user.service.ts @@ -8,9 +8,10 @@ import {repository} from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; import {securityId, UserProfile} from '@loopback/security'; import {compare} from 'bcryptjs'; -import {User} from '../models'; import {UserRepository} from '../repositories'; - +/* eslint-disable*/ +import {User, UserRelations} from '../models'; +/* eslint-enable */ /** * A pre-defined type for user credentials. It assumes a user logs in * using the email and password. You can modify it if your app has different credential fields @@ -62,4 +63,17 @@ export class MyUserService implements UserService { email: user.email, }; } + + //function to find user by id + async findUserById(id: string) { + const userNotfound = 'invalid User'; + const foundUser = await this.userRepository.findOne({ + where: {id: id}, + }); + + if (!foundUser) { + throw new HttpErrors.Unauthorized(userNotfound); + } + return foundUser; + } } diff --git a/extensions/authentication-jwt/src/types.ts b/extensions/authentication-jwt/src/types.ts new file mode 100644 index 000000000000..78601539cb5a --- /dev/null +++ b/extensions/authentication-jwt/src/types.ts @@ -0,0 +1,33 @@ +// Copyright IBM Corp. 2020. All Rights Reserved. +// Node module: @loopback/authentication-jwt +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {UserProfile} from '@loopback/security'; + +/** + * Describes the token object that returned by the refresh token service functions. + */ +export type TokenObject = { + accessToken: string; + expiresIn?: string | undefined; + refreshToken?: string | undefined; +}; + +/** + * The token refresh service. An access token expires in limited time. Therefore + * token refresh service is needed to keep replacing the old access token with + * a new one periodically. + */ +export interface RefreshTokenService { + /** + * Generate a refresh token, bind it with the given user profile + access + * token, then store them in backend. + */ + generateToken(userProfile: UserProfile, token: string): Promise; + + /** + * Refresh the access token bound with the given refresh token. + */ + refreshToken(refreshToken: string): Promise; +}