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..5ff1bc376d28 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,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..4e788ba59de9 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..50f2d18166d3 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,17 @@ 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, + TokenObject, +} from '../../../'; import {Credentials} from '../../../services/user.service'; const CredentialsSchema = { @@ -95,4 +102,70 @@ 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: { + accessToken: { + type: 'string', + }, + refreshToken: { + 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 = { + accessToken: await this.jwtService.generateToken(userProfile), + }; + return token; + } + + @post('/refresh', { + responses: { + '200': { + description: 'Token', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + refreshToken: { + 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..65fc807f8cde 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,17 @@ 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..0bc5fc772fe3 --- /dev/null +++ b/extensions/authentication-jwt/src/interceptors/refresh-token-generate.interceptor.ts @@ -0,0 +1,84 @@ +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, { + refreshToken: refreshToken, + }); + await this.refreshTokenRepository.create({ + userId: currentUser.id, + refreshToken: result.refreshToken, + }); + 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..e6214920c15f --- /dev/null +++ b/extensions/authentication-jwt/src/interceptors/refresh-token-grant.interceptor.ts @@ -0,0 +1,97 @@ +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].refreshToken; + + 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 = { + accessToken: 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..08d473bdbe76 100644 --- a/extensions/authentication-jwt/src/jwt-authentication-component.ts +++ b/extensions/authentication-jwt/src/jwt-authentication-component.ts @@ -16,12 +16,23 @@ 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 +52,27 @@ 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..a6bed25fd941 100644 --- a/extensions/authentication-jwt/src/keys.ts +++ b/extensions/authentication-jwt/src/keys.ts @@ -34,3 +34,55 @@ 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 = { + refreshToken: string; +}; + +export const RefreshGrantSchema = { + type: 'object', + required: ['refreshToken'], + properties: { + refreshToken: { + type: 'string', + }, + }, +}; +export const RefreshGrantRequestBody = { + description: 'Reissuing Acess Token', + required: true, + content: { + 'application/json': {schema: RefreshGrantSchema}, + }, +}; + +export type TokenObject = { + accessToken: string; + expiresIn?: string | undefined; + refreshToken?: string | undefined; +}; 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..fbd92176e265 --- /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..2928a3115ee9 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, diff --git a/extensions/authentication-jwt/src/services/user.service.ts b/extensions/authentication-jwt/src/services/user.service.ts index 45e825bef89d..9a3d04327bf0 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 deleted file mode 100644 index afa91467db5f..000000000000 --- a/tsconfig.json +++ /dev/null @@ -1,180 +0,0 @@ -{ - "extends": "./packages/build/config/tsconfig.common.json", - "compilerOptions": { - "composite": true - }, - "references": [ - { - "path": "acceptance/extension-logging-fluentd/tsconfig.json" - }, - { - "path": "acceptance/repository-cloudant/tsconfig.json" - }, - { - "path": "acceptance/repository-mongodb/tsconfig.json" - }, - { - "path": "acceptance/repository-mysql/tsconfig.json" - }, - { - "path": "acceptance/repository-postgresql/tsconfig.json" - }, - { - "path": "benchmark/tsconfig.json" - }, - { - "path": "examples/access-control-migration/tsconfig.json" - }, - { - "path": "examples/context/tsconfig.json" - }, - { - "path": "examples/express-composition/tsconfig.json" - }, - { - "path": "examples/file-transfer/tsconfig.json" - }, - { - "path": "examples/greeter-extension/tsconfig.json" - }, - { - "path": "examples/greeting-app/tsconfig.json" - }, - { - "path": "examples/hello-world/tsconfig.json" - }, - { - "path": "examples/lb3-application/tsconfig.json" - }, - { - "path": "examples/log-extension/tsconfig.json" - }, - { - "path": "examples/metrics-prometheus/tsconfig.json" - }, - { - "path": "examples/multi-tenancy/tsconfig.json" - }, - { - "path": "examples/passport-login/tsconfig.json" - }, - { - "path": "examples/rest-crud/tsconfig.json" - }, - { - "path": "examples/rpc-server/tsconfig.json" - }, - { - "path": "examples/soap-calculator/tsconfig.json" - }, - { - "path": "examples/todo-jwt/tsconfig.json" - }, - { - "path": "examples/todo-list/tsconfig.json" - }, - { - "path": "examples/todo/tsconfig.json" - }, - { - "path": "examples/validation-app/tsconfig.json" - }, - { - "path": "extensions/apiconnect/tsconfig.json" - }, - { - "path": "extensions/authentication-jwt/tsconfig.json" - }, - { - "path": "extensions/authentication-passport/tsconfig.json" - }, - { - "path": "extensions/context-explorer/tsconfig.json" - }, - { - "path": "extensions/cron/tsconfig.json" - }, - { - "path": "extensions/health/tsconfig.json" - }, - { - "path": "extensions/logging/tsconfig.json" - }, - { - "path": "extensions/metrics/tsconfig.json" - }, - { - "path": "fixtures/mock-oauth2-provider/tsconfig.json" - }, - { - "path": "packages/authentication/tsconfig.json" - }, - { - "path": "packages/authorization/tsconfig.json" - }, - { - "path": "packages/boot/tsconfig.json" - }, - { - "path": "packages/booter-lb3app/tsconfig.json" - }, - { - "path": "packages/context/tsconfig.json" - }, - { - "path": "packages/core/tsconfig.json" - }, - { - "path": "packages/express/tsconfig.json" - }, - { - "path": "packages/http-caching-proxy/tsconfig.json" - }, - { - "path": "packages/http-server/tsconfig.json" - }, - { - "path": "packages/metadata/tsconfig.json" - }, - { - "path": "packages/model-api-builder/tsconfig.json" - }, - { - "path": "packages/openapi-spec-builder/tsconfig.json" - }, - { - "path": "packages/openapi-v3/tsconfig.json" - }, - { - "path": "packages/repository-json-schema/tsconfig.json" - }, - { - "path": "packages/repository-tests/tsconfig.json" - }, - { - "path": "packages/repository/tsconfig.json" - }, - { - "path": "packages/rest-crud/tsconfig.json" - }, - { - "path": "packages/rest-explorer/tsconfig.json" - }, - { - "path": "packages/rest/tsconfig.json" - }, - { - "path": "packages/security/tsconfig.json" - }, - { - "path": "packages/service-proxy/tsconfig.json" - }, - { - "path": "packages/testlab/tsconfig.json" - }, - { - "path": "packages/tsdocs/tsconfig.json" - } - ], - "files": [] -}