diff --git a/packages/shopping/src/controllers/user.controller.ts b/packages/shopping/src/controllers/user.controller.ts index 8f9b8675d..58bce8efe 100644 --- a/packages/shopping/src/controllers/user.controller.ts +++ b/packages/shopping/src/controllers/user.controller.ts @@ -63,7 +63,7 @@ export class UserController { @inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService, @inject(RefreshtokenServiceBindings.REFRESHTOKEN_SERVICE) - public refreshtokenService: RefreshtokenService, + public refreshtokenService: RefreshtokenService, @inject(UserServiceBindings.USER_SERVICE) public userService: UserService, ) {} @@ -266,8 +266,8 @@ export class UserController { const token = await this.jwtService.generateToken(userProfile); // create a refreshtoken - const refreshtoken = await this.refreshtokenService.generateRefreshtoken( - user, + const refreshtoken = await this.refreshtokenService.generateToken( + userProfile, ); return { @@ -303,7 +303,7 @@ export class UserController { @requestBody(RefreshTokenRequestBody) body: {refreshtoken: string}, ): Promise<{token: string}> { // check if the provided refreshtoken is valid, throws error if invalid - await this.refreshtokenService.verifyRefreshtoken( + await this.refreshtokenService.verifyToken( body.refreshtoken, currentUserProfile, ); diff --git a/packages/shopping/src/keys.ts b/packages/shopping/src/keys.ts index ea5df1041..3a0c44758 100644 --- a/packages/shopping/src/keys.ts +++ b/packages/shopping/src/keys.ts @@ -41,7 +41,13 @@ export namespace UserServiceBindings { } export namespace RefreshtokenServiceBindings { - export const REFRESHTOKEN_SERVICE = BindingKey.create< - RefreshtokenService - >('services.refreshtoken.service'); + export const REFRESHTOKEN_ETERNAL_ALLOWED = BindingKey.create( + 'services.refreshtoken.eternal_allowed', + ); + export const REFRESHTOKEN_EXPIRES_IN = BindingKey.create( + 'services.refreshtoken.expires_in', + ); + export const REFRESHTOKEN_SERVICE = BindingKey.create( + 'services.refreshtoken.service', + ); } diff --git a/packages/shopping/src/services/refreshtoken.service.ts b/packages/shopping/src/services/refreshtoken.service.ts index aa905f53d..32f0da223 100644 --- a/packages/shopping/src/services/refreshtoken.service.ts +++ b/packages/shopping/src/services/refreshtoken.service.ts @@ -2,57 +2,82 @@ // Node module: @loopback/authentication // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {UserRefreshtokenRepository} from '../repositories/user-refreshtoken.repository'; -import {User} from '../models/user.model'; +import {TokenService} from '@loopback/authentication'; +import {inject} from '@loopback/core'; import {repository} from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; import {UserProfile, securityId} from '@loopback/security'; +import {UserRefreshtokenRepository} from '../repositories/user-refreshtoken.repository'; +import {RefreshtokenServiceBindings} from '../keys'; -export interface RefreshtokenService { - generateRefreshtoken(user: User): Promise; - verifyRefreshtoken( - refreshtoken: string, - userProfile: UserProfile, - ): Promise; - revokeRefreshtoken( - refreshtoken: string, - userProfile: UserProfile, - ): Promise; +export interface RefreshtokenService extends TokenService { + /** + * Verifies the validity of a token string and returns a user profile + * + * TODO(derdeka) move optional parameter userProfile to TokenService? + */ + verifyToken(token: string, userProfile?: UserProfile): Promise; + /** + * Revokes a given token (if supported by token system) + */ + revokeToken(token: string, userProfile?: UserProfile): Promise; } -export class MyRefreshtokenService implements RefreshtokenService { +export class MyRefreshtokenService implements RefreshtokenService { constructor( @repository(UserRefreshtokenRepository) public userRefreshtokenRepository: UserRefreshtokenRepository, + @inject(RefreshtokenServiceBindings.REFRESHTOKEN_ETERNAL_ALLOWED, { + optional: true, + }) + private refreshtokenEternalAllowed: boolean = false, + @inject(RefreshtokenServiceBindings.REFRESHTOKEN_EXPIRES_IN, { + optional: true, + }) + private refreshtokenExpiresIn: number = 60 * 60 * 24, ) {} - async generateRefreshtoken(user: User): Promise { + async generateToken(userProfile: UserProfile): Promise { + // TODO(derdeka) objectId as refreshtoken is a bad idea const userRefreshtoken = await this.userRefreshtokenRepository.create({ creation: new Date(), - // TODO(derdeka) inject ttl setting - ttl: 60 * 60 * 6, - userId: user.id, + ttl: this.refreshtokenExpiresIn, + userId: userProfile[securityId], }); return userRefreshtoken.id; } - async verifyRefreshtoken( + async verifyToken( refreshtoken: string, - userProfile: UserProfile, - ): Promise { + userProfile?: UserProfile, + ): Promise { try { - // TODO(derdeka) check ttl and creation date - await this.userRefreshtokenRepository.findById(refreshtoken, { - where: { - userId: userProfile[securityId], + if (!userProfile || !userProfile[securityId]) { + throw new HttpErrors.Unauthorized('Invalid refreshToken'); + } + const {creation, ttl} = await this.userRefreshtokenRepository.findById( + refreshtoken, + { + where: { + userId: userProfile[securityId], + }, }, - }); + ); + const isEternalToken = ttl === -1; + const elapsedSeconds = (Date.now() - creation.getTime()) / 1000; + const isValid = isEternalToken + ? this.refreshtokenEternalAllowed + : elapsedSeconds < ttl; + if (!isValid) { + throw new HttpErrors.Unauthorized('Invalid refreshToken'); + } + return userProfile; } catch (e) { - throw new HttpErrors.Unauthorized('Invalid accessToken'); + throw new HttpErrors.Unauthorized('Invalid refreshToken'); } } - async revokeRefreshtoken( + async revokeToken( refreshtoken: string, userProfile: UserProfile, ): Promise {