Skip to content

Commit

Permalink
feat(loopback4-example-shopping): add refreshtoken - refactoring
Browse files Browse the repository at this point in the history
Signed-off-by: derdeka <[email protected]>
  • Loading branch information
derdeka committed Feb 10, 2020
1 parent 5f892de commit 487e699
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 34 deletions.
8 changes: 4 additions & 4 deletions packages/shopping/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class UserController {
@inject(TokenServiceBindings.TOKEN_SERVICE)
public jwtService: TokenService,
@inject(RefreshtokenServiceBindings.REFRESHTOKEN_SERVICE)
public refreshtokenService: RefreshtokenService<User>,
public refreshtokenService: RefreshtokenService,
@inject(UserServiceBindings.USER_SERVICE)
public userService: UserService<User, Credentials>,
) {}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
);
Expand Down
12 changes: 9 additions & 3 deletions packages/shopping/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export namespace UserServiceBindings {
}

export namespace RefreshtokenServiceBindings {
export const REFRESHTOKEN_SERVICE = BindingKey.create<
RefreshtokenService<User>
>('services.refreshtoken.service');
export const REFRESHTOKEN_ETERNAL_ALLOWED = BindingKey.create<boolean>(
'services.refreshtoken.eternal_allowed',
);
export const REFRESHTOKEN_EXPIRES_IN = BindingKey.create<number>(
'services.refreshtoken.expires_in',
);
export const REFRESHTOKEN_SERVICE = BindingKey.create<RefreshtokenService>(
'services.refreshtoken.service',
);
}
79 changes: 52 additions & 27 deletions packages/shopping/src/services/refreshtoken.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<U> {
generateRefreshtoken(user: User): Promise<string>;
verifyRefreshtoken(
refreshtoken: string,
userProfile: UserProfile,
): Promise<void>;
revokeRefreshtoken(
refreshtoken: string,
userProfile: UserProfile,
): Promise<void>;
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<UserProfile>;
/**
* Revokes a given token (if supported by token system)
*/
revokeToken(token: string, userProfile?: UserProfile): Promise<void>;
}

export class MyRefreshtokenService implements RefreshtokenService<User> {
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<string> {
async generateToken(userProfile: UserProfile): Promise<string> {
// 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<void> {
userProfile?: UserProfile,
): Promise<UserProfile> {
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<void> {
Expand Down

0 comments on commit 487e699

Please sign in to comment.