From 0a1cb1884e6a7280a1f33d4be463dc6ffa49079d Mon Sep 17 00:00:00 2001 From: Madaky <17172989+madaky@users.noreply.github.com> Date: Wed, 27 May 2020 03:01:10 +0530 Subject: [PATCH] feat(authentication-jwt): implementing refresh token Refresh token implementation through interceptor --- .../authentication-jwt/package-lock.json | 76 ++++++++++++++++- extensions/authentication-jwt/package.json | 1 + .../acceptance/jwt.component.test.ts | 24 +++++- .../src/__tests__/fixtures/application.ts | 2 + .../fixtures/controllers/user.controller.ts | 78 ++++++++++++++++- .../src/__tests__/unit/jwt.service.ts | 8 +- .../src/interceptors/index.ts | 3 + .../refresh-token-generate.interceptor.ts | 73 ++++++++++++++++ .../refresh-token-grant.interceptor.ts | 85 +++++++++++++++++++ .../src/jwt-authentication-component.ts | 22 ++++- extensions/authentication-jwt/src/keys.ts | 49 +++++++++++ .../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 ++++ .../src/services/jwt.service.ts | 9 +- .../src/services/user.service.ts | 12 +++ extensions/authentication-jwt/tsconfig.json | 3 + tsconfig.json | 3 + 19 files changed, 493 insertions(+), 11 deletions(-) create mode 100644 extensions/authentication-jwt/src/interceptors/index.ts create mode 100644 extensions/authentication-jwt/src/interceptors/refresh-token-generate.interceptor.ts create mode 100644 extensions/authentication-jwt/src/interceptors/refresh-token-grant.interceptor.ts create mode 100644 extensions/authentication-jwt/src/models/refresh-token.model.ts create mode 100644 extensions/authentication-jwt/src/repositories/refresh-token.repository.ts diff --git a/extensions/authentication-jwt/package-lock.json b/extensions/authentication-jwt/package-lock.json index 2adc791524f4..9e51561b6a25 100644 --- a/extensions/authentication-jwt/package-lock.json +++ b/extensions/authentication-jwt/package-lock.json @@ -4,11 +4,40 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@loopback/context": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@loopback/context/-/context-3.8.1.tgz", + "integrity": "sha512-RlT+Z3qPtvD7OEJqL0rLJE9f26DOpXyBtT/Ekh5NyEveTW5qnOZ2m5eS4moCq95Kd2GHw0hlDDk7Giid+urD7A==", + "requires": { + "@loopback/metadata": "^2.1.5", + "@types/debug": "^4.1.5", + "debug": "^4.1.1", + "p-event": "^4.1.0", + "tslib": "^2.0.0", + "uuid": "^8.0.0" + } + }, + "@loopback/metadata": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@loopback/metadata/-/metadata-2.1.5.tgz", + "integrity": "sha512-mAE9bkm9WYGtkvMqIw3B57nbr8fAW/GNfApwgcIEpcQkCMDIPSIxKSVr94kT1Snag/OWl7GohxQFD8aZ+4XA/A==", + "requires": { + "debug": "^4.1.1", + "lodash": "^4.17.15", + "reflect-metadata": "^0.1.13", + "tslib": "^2.0.0" + } + }, "@types/bcryptjs": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.2.tgz", "integrity": "sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==" }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, "@types/lodash": { "version": "4.14.153", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.153.tgz", @@ -31,6 +60,14 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -78,8 +115,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.includes": { "version": "4.3.0", @@ -121,6 +157,32 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "p-event": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.1.0.tgz", + "integrity": "sha512-4vAd06GCsgflX4wHN1JqrMzBh/8QZ4j+rzp0cd2scXRwuBEv+QR3wrVA5aLhWDLw4y2WgDKvzWF3CCLmVM1UgA==", + "requires": { + "p-timeout": "^2.0.1" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "requires": { + "p-finally": "^1.0.0" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -131,11 +193,21 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + }, "typescript": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz", "integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==", "dev": true + }, + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" } } } diff --git a/extensions/authentication-jwt/package.json b/extensions/authentication-jwt/package.json index 74bca5307302..960c42d9ad51 100644 --- a/extensions/authentication-jwt/package.json +++ b/extensions/authentication-jwt/package.json @@ -19,6 +19,7 @@ "license": "MIT", "dependencies": { "@loopback/authentication": "^4.2.5", + "@loopback/context": "^3.8.1", "@loopback/core": "^2.7.0", "@loopback/openapi-v3": "^3.4.1", "@loopback/rest": "^5.0.1", 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..34aa0cb9e96b 100644 --- a/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts +++ b/extensions/authentication-jwt/src/__tests__/acceptance/jwt.component.test.ts @@ -15,13 +15,15 @@ import {UserServiceBindings} from '../..'; import {OPERATION_SECURITY_SPEC, SECURITY_SCHEME_SPEC} from '../../'; import {UserRepository} from '../../repositories'; import {TestApplication} from '../fixtures/application'; +import { RefreshTokenBindings } from '../../keys'; describe('jwt authentication', () => { let app: TestApplication; let client: Client; let token: string; let userRepo: UserRepository; - + let refreshToken: string; + let tokenAuth: string; before(givenRunningApplication); before(() => { client = createRestAppClient(app); @@ -50,6 +52,26 @@ 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.refresh_token; + }); + + it(`user sends refresh token and new access token issued`, async () => { + const tokenArg = {"refresh_token": refreshToken}; + const res = await client.post('/refresh/').send(tokenArg).expect(200); + tokenAuth = res.body.access_token; + }); + + 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..a09c860aa666 100644 --- a/extensions/authentication-jwt/src/__tests__/fixtures/application.ts +++ b/extensions/authentication-jwt/src/__tests__/fixtures/application.ts @@ -14,6 +14,7 @@ import path from 'path'; import {JWTAuthenticationComponent, UserServiceBindings} from '../../'; import {DbDataSource} from './datasources/db.datasource'; import {MySequence} from './sequence'; +import { RefreshTokenBindings } from '../../keys'; export class TestApplication extends BootMixin( ServiceMixin(RepositoryMixin(RestApplication)), @@ -34,6 +35,7 @@ export class TestApplication extends BootMixin( this.component(JWTAuthenticationComponent); // Bind datasource this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME); + this.dataSource(DbDataSource, RefreshTokenBindings.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 235c9fb83fa9..f7025fa7f586 100644 --- a/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts +++ b/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts @@ -8,10 +8,10 @@ import { TokenService, UserService, } from '@loopback/authentication'; -import {inject} from '@loopback/core'; +import {inject, intercept} from '@loopback/core'; import {get, post, requestBody} from '@loopback/rest'; import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; -import {TokenServiceBindings, User, UserServiceBindings} from '../../../'; +import {TokenServiceBindings, User, UserServiceBindings, RefreshGrantRequestBody, RefreshGrant} from '../../../'; import {Credentials} from '../../../services/user.service'; const CredentialsSchema = { @@ -37,6 +37,13 @@ export const CredentialsRequestBody = { }, }; +export type TokenObject = { + access_token: string; + expiresIn?: string | undefined; + refresh_token?: string | undefined; + +} + export class UserController { constructor( @inject(TokenServiceBindings.TOKEN_SERVICE) @@ -95,4 +102,71 @@ export class UserController { async whoAmI(): Promise { return this.user[securityId]; } + + @post('/users/refresh/login', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + "access_token": { + type: 'string', + }, + "refresh_token": { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }) + @intercept('refresh-token-generate') + 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 + ); + // create a JSON Web Token based on the user profile + const token = { + "access_token": await this.jwtService.generateToken(userProfile) + } + return token; + } + + @post("/refresh", { + responses: { + "200": { + description: "Token", + content: { + "application/json": { + schema: { + type: "object", + properties: { + "refresh_token": { + type: "string", + }, + }, + }, + }, + }, + }, + }, + }) + @intercept('refresh-token-grant') + async refresh( + @requestBody(RefreshGrantRequestBody) refreshGrant: RefreshGrant + ): Promise<{token: string}> { + const token = ''; + return {token}; + } + } diff --git a/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts b/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts index 283d9bbf430b..b3c4ff9a3fd6 100644 --- a/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts +++ b/extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {HttpErrors} from '@loopback/rest'; -import {securityId} from '@loopback/security'; +import {securityId, UserProfile} from '@loopback/security'; import {expect} from '@loopback/testlab'; import {JWTService} from '../../'; @@ -19,9 +19,13 @@ describe('token service', () => { id: '1', name: 'test', }; + type Setter = (value?: T) => void; const TOKEN_SECRET_VALUE = 'myjwts3cr3t'; const TOKEN_EXPIRES_IN_VALUE = '60'; - const jwtService = new JWTService(TOKEN_SECRET_VALUE, TOKEN_EXPIRES_IN_VALUE); + const setCurrentUser:Setter = (userProfile)=>{ + return userProfile; + }; + const jwtService = new JWTService(TOKEN_SECRET_VALUE, TOKEN_EXPIRES_IN_VALUE, setCurrentUser); it('token service generateToken() succeeds', async () => { const token = await jwtService.generateToken(USER_PROFILE); diff --git a/extensions/authentication-jwt/src/interceptors/index.ts b/extensions/authentication-jwt/src/interceptors/index.ts new file mode 100644 index 000000000000..e01b37c74a75 --- /dev/null +++ b/extensions/authentication-jwt/src/interceptors/index.ts @@ -0,0 +1,3 @@ +export * from './refresh-token-generate.interceptor'; + +export * from './refresh-token-grant.interceptor'; diff --git a/extensions/authentication-jwt/src/interceptors/refresh-token-generate.interceptor.ts b/extensions/authentication-jwt/src/interceptors/refresh-token-generate.interceptor.ts new file mode 100644 index 000000000000..c84da8465ff4 --- /dev/null +++ b/extensions/authentication-jwt/src/interceptors/refresh-token-generate.interceptor.ts @@ -0,0 +1,73 @@ +import {Getter, inject, Interceptor, InvocationContext, InvocationResult, Provider, uuid, ValueOrPromise} from '@loopback/context'; +import {repository} from '@loopback/repository'; +import {SecurityBindings, UserProfile} from '@loopback/security'; +import {promisify} from 'util'; +import { RefreshTokenRepository} from '../repositories'; +import { RefreshTokenInterceptorBindings } from '../keys'; +import { HttpErrors } from '@loopback/rest'; + +/** + * This class will be bound to the application as an `Interceptor` during + * `boot` + */ +const jwt = require('jsonwebtoken'); +const signAsync = promisify(jwt.sign); +export class RefreshTokenGenerateInterceptor implements Provider { + + constructor( + @inject(RefreshTokenInterceptorBindings.REFRESH_SECRET) + private refreshSecret: string, + @inject(RefreshTokenInterceptorBindings.REFRESH_EXPIRES_IN) + private refreshExpiresIn: string, + @inject(RefreshTokenInterceptorBindings.REFRESH_ISSURE) + private refreshIssure: string, + @repository(RefreshTokenRepository) public refreshTokenRepository: RefreshTokenRepository, + @inject.getter(SecurityBindings.USER, {optional: true}) + private getCurrentUser: Getter, + ) {} + + + /** + * This method is used by LoopBack context to produce an interceptor function + * for the binding. + * + * @returns An interceptor function + */ + value() { + return this.intercept.bind(this); + } + + /** + * The logic to intercept an invocation + * @param invocationCtx - Invocation context + * @param next - A function to invoke next interceptor or the target method + */ + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + try { + // Add pre-invocation logic here + let result = await next(); + // Add post-invocation logic here + const currentUser = await this.getCurrentUser(); + const data = { + token: uuid() + } + const refreshToken = await signAsync(data, this.refreshSecret, { + expiresIn: Number(this.refreshExpiresIn), + issuer: this.refreshIssure + }) + result = Object.assign(result, { + "refresh_token": refreshToken + }) + await this.refreshTokenRepository.create({userId: currentUser.id, refreshToken: result.refresh_token}); + return result; + } catch (error) { + // Add error handling logic here + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + } +} diff --git a/extensions/authentication-jwt/src/interceptors/refresh-token-grant.interceptor.ts b/extensions/authentication-jwt/src/interceptors/refresh-token-grant.interceptor.ts new file mode 100644 index 000000000000..ce6fef798e99 --- /dev/null +++ b/extensions/authentication-jwt/src/interceptors/refresh-token-grant.interceptor.ts @@ -0,0 +1,85 @@ +import {TokenService} from '@loopback/authentication'; +import {inject, Interceptor, InvocationContext, InvocationResult, Provider, ValueOrPromise} from '@loopback/context'; +import {repository} from '@loopback/repository/'; +import {HttpErrors} from '@loopback/rest'; +import {UserProfile} from '@loopback/security'; +import {promisify} from 'util'; +import { RefreshTokenInterceptorBindings, UserServiceBindings, TokenServiceBindings } from '../keys'; +import { MyUserService } from '../services'; +import { RefreshTokenRepository } from '../repositories'; + + + +const jwt = require('jsonwebtoken'); +const verifyAsync = promisify(jwt.verify); +/** + * This class will be bound to the application as an `Interceptor` during + * `boot` + */ +export class RefreshTokenGrantInterceptor implements Provider { + + constructor( + @inject(RefreshTokenInterceptorBindings.REFRESH_SECRET) + private refreshSecret: string, + @inject(UserServiceBindings.USER_SERVICE) public userService: MyUserService, + @repository(RefreshTokenRepository) public refreshTokenRepository: RefreshTokenRepository, + @inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService, + + ) {} + /** + * This method is used by LoopBack context to produce an interceptor function + * for the binding. + * + * @returns An interceptor function + */ + value() { + return this.intercept.bind(this); + } + + /** + * The logic to intercept an invocation + * @param invocationCtx - Invocation context + * @param next - A function to invoke next interceptor or the target method + */ + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + try { + // Add pre-invocation logic here + let result = await next(); + // Add post-invocation logic here + const refreshToken = invocationCtx.args[0].refresh_token; + + if (!refreshToken) { + throw new HttpErrors.Unauthorized( + `Error verifying token : 'refresh token' is null`, + ); + } + + 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`, + ); + } + 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); + result = { + "access_token": token + } + return result; + } catch (error) { + // Add error handling logic here + throw new HttpErrors.Unauthorized( + `Error verifying token : ${error.message}`, + ); + } + } +} diff --git a/extensions/authentication-jwt/src/jwt-authentication-component.ts b/extensions/authentication-jwt/src/jwt-authentication-component.ts index 6fa2ae8942a7..e3b4ceffb720 100644 --- a/extensions/authentication-jwt/src/jwt-authentication-component.ts +++ b/extensions/authentication-jwt/src/jwt-authentication-component.ts @@ -16,12 +16,16 @@ import { TokenServiceBindings, TokenServiceConstants, UserServiceBindings, + RefreshTokenInterceptorConstants, + RefreshTokenInterceptorBindings, + RefreshTokenBindings, } from './keys'; -import {UserCredentialsRepository, UserRepository} from './repositories'; +import {UserCredentialsRepository, UserRepository, RefreshTokenRepository} from './repositories'; import {MyUserService} from './services'; import {JWTAuthenticationStrategy} from './services/jwt.auth.strategy'; import {JWTService} from './services/jwt.service'; import {SecuritySpecEnhancer} from './services/security.spec.enhancer'; +import { RefreshTokenGenerateInterceptor, RefreshTokenGrantInterceptor } from './interceptors'; export class JWTAuthenticationComponent implements Component { bindings: Binding[] = [ @@ -41,6 +45,22 @@ export class JWTAuthenticationComponent implements Component { UserCredentialsRepository, ), createBindingFromClass(SecuritySpecEnhancer), + ///refresh bindings + Binding.bind(RefreshTokenInterceptorConstants.REFRESH_INTERCEPTOR_NAME).toProvider(RefreshTokenGenerateInterceptor), + Binding.bind(RefreshTokenInterceptorConstants.REFRESH_INTERCEPTOR_GRANT_TYPE).toProvider(RefreshTokenGrantInterceptor), + // Refresh token bindings + Binding.bind(RefreshTokenInterceptorBindings.REFRESH_SECRET).to( + RefreshTokenInterceptorConstants.REFRESH_SECRET_VALUE, + ), + Binding.bind(RefreshTokenInterceptorBindings.REFRESH_EXPIRES_IN).to( + RefreshTokenInterceptorConstants.REFRESH_EXPIRES_IN_VALUE + ), + Binding.bind(RefreshTokenInterceptorBindings.REFRESH_ISSURE).to( + RefreshTokenInterceptorConstants.REFRESH_ISSURE_VALUE + ), + //refresh token repository binding + Binding.bind(RefreshTokenBindings.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..37bd96c01637 100644 --- a/extensions/authentication-jwt/src/keys.ts +++ b/extensions/authentication-jwt/src/keys.ts @@ -34,3 +34,52 @@ export namespace UserServiceBindings { export const USER_CREDENTIALS_REPOSITORY = 'repositories.UserCredentialsRepository'; } + + +export namespace RefreshTokenInterceptorConstants { + export const REFRESH_INTERCEPTOR_NAME = "refresh-token-generate" + export const REFRESH_INTERCEPTOR_GRANT_TYPE = "refresh-token-grant" + export const REFRESH_SECRET_VALUE = 'r3fr35htok3n'; + export const REFRESH_EXPIRES_IN_VALUE = '216000'; + export const REFRESH_ISSURE_VALUE = 'loopback4'; +} + +export namespace RefreshTokenInterceptorBindings { + export const REFRESH_SECRET = BindingKey.create( + 'authentication.jwt.refresh.secret' + ); + export const REFRESH_EXPIRES_IN = BindingKey.create( + 'authentication.jwt.referesh.expires.in.seconds', + ); + export const REFRESH_ISSURE = BindingKey.create( + 'authentication.jwt.referesh.issure', + ); +} + +export namespace RefreshTokenBindings { + export const DATASOURCE_NAME = 'refreshdb'; + export const REFRESH_REPOSITORY = 'repositories.RefreshTokenRepository'; +} + + +export type RefreshGrant = { + //@ts-ignore + "refresh_token": string +} + +export const RefreshGrantSchema = { + type: "object", + required: ["refresh_token"], + properties: { + refresh_token: { + type: "string", + }, + }, +} +export const RefreshGrantRequestBody = { + description: "Reissuing Acess Token", + required: true, + content: { + "application/json": {schema: RefreshGrantSchema}, + }, +}; \ No newline at end of file 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..90f294c59fad --- /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..b7f204795c81 --- /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 { RefreshTokenBindings, UserServiceBindings } from '../keys'; + +export class RefreshTokenRepository extends DefaultCrudRepository< + RefreshToken, + typeof RefreshToken.prototype.id, + RefreshTokenRelations + > { + constructor( + @inject(`datasources.${RefreshTokenBindings.DATASOURCE_NAME}`) + dataSource: juggler.DataSource, + ) { + super(RefreshToken, dataSource); + } +} diff --git a/extensions/authentication-jwt/src/services/jwt.service.ts b/extensions/authentication-jwt/src/services/jwt.service.ts index 599867a9ddc1..92873c50319d 100644 --- a/extensions/authentication-jwt/src/services/jwt.service.ts +++ b/extensions/authentication-jwt/src/services/jwt.service.ts @@ -4,9 +4,9 @@ // License text available at https://opensource.org/licenses/MIT import {TokenService} from '@loopback/authentication'; -import {inject} from '@loopback/core'; +import {inject, Setter} from '@loopback/core'; import {HttpErrors} from '@loopback/rest'; -import {securityId, UserProfile} from '@loopback/security'; +import {securityId, UserProfile, SecurityBindings} from '@loopback/security'; import {promisify} from 'util'; import {TokenServiceBindings} from '../keys'; @@ -20,6 +20,8 @@ export class JWTService implements TokenService { private jwtSecret: string, @inject(TokenServiceBindings.TOKEN_EXPIRES_IN) private jwtExpiresIn: string, + @inject.setter(SecurityBindings.USER) + readonly setCurrentUser: Setter, ) {} async verifyToken(token: string): Promise { @@ -57,6 +59,7 @@ export class JWTService implements TokenService { 'Error generating token : userProfile is null', ); } + this.setCurrentUser(userProfile); const userInfoForToken = { id: userProfile[securityId], name: userProfile.name, @@ -65,7 +68,7 @@ export class JWTService implements TokenService { // Generate a JSON Web Token let token: string; try { - token = await signAsync(userInfoForToken, this.jwtSecret, { + token = await signAsync(userInfoForToken, this.jwtSecret, { expiresIn: Number(this.jwtExpiresIn), }); } catch (error) { diff --git a/extensions/authentication-jwt/src/services/user.service.ts b/extensions/authentication-jwt/src/services/user.service.ts index 45e825bef89d..b87da243ac2a 100644 --- a/extensions/authentication-jwt/src/services/user.service.ts +++ b/extensions/authentication-jwt/src/services/user.service.ts @@ -62,4 +62,16 @@ export class MyUserService implements UserService { email: user.email, }; } + + 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/tsconfig.json b/extensions/authentication-jwt/tsconfig.json index 66dfed3c71c0..2b50897809b0 100644 --- a/extensions/authentication-jwt/tsconfig.json +++ b/extensions/authentication-jwt/tsconfig.json @@ -17,6 +17,9 @@ { "path": "../../packages/boot/tsconfig.json" }, + { + "path": "../../packages/context/tsconfig.json" + }, { "path": "../../packages/core/tsconfig.json" }, diff --git a/tsconfig.json b/tsconfig.json index afa91467db5f..3cb257d9036b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -174,6 +174,9 @@ }, { "path": "packages/tsdocs/tsconfig.json" + }, + { + "path": "sandbox/lb4-example-authacl/tsconfig.json" } ], "files": []