Skip to content

Commit

Permalink
feat(authentication-jwt): implementing refresh token
Browse files Browse the repository at this point in the history
Refresh token implementation through interceptor
  • Loading branch information
madaky committed May 27, 2020
1 parent 697042a commit ac5091a
Show file tree
Hide file tree
Showing 19 changed files with 536 additions and 10 deletions.
76 changes: 74 additions & 2 deletions extensions/authentication-jwt/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions extensions/authentication-jwt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: '[email protected]', 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -95,4 +102,70 @@ export class UserController {
async whoAmI(): Promise<string> {
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<TokenObject> {
// 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};
}
}
12 changes: 10 additions & 2 deletions extensions/authentication-jwt/src/__tests__/unit/jwt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '../../';

Expand All @@ -19,9 +19,17 @@ describe('token service', () => {
id: '1',
name: 'test',
};
type Setter<T> = (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> = 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);
Expand Down
3 changes: 3 additions & 0 deletions extensions/authentication-jwt/src/interceptors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './refresh-token-generate.interceptor';

export * from './refresh-token-grant.interceptor';
Original file line number Diff line number Diff line change
@@ -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<Interceptor> {
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<UserProfile>,
) {}

/**
* 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<InvocationResult>,
) {
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}`,
);
}
}
}
Loading

0 comments on commit ac5091a

Please sign in to comment.