Skip to content

Commit

Permalink
feat(authentication-jwt): implementation of refresh token
Browse files Browse the repository at this point in the history
feature refresh token implementation through Service

chore(authentication-jwt): readme updated

readme.md to use refresh token and extra configurations

chore(context): readmemd refactor

Apply suggestions from code review

Co-authored-by: Diana Lau <dhmlau@ca.ibm.com>

feat: implemented awthentication-jwt refreshtoken services

Signed-off-by: Madaky <17172989+madaky@users.noreply.github.com>
  • Loading branch information
madaky authored and jannyHou committed Aug 27, 2020
1 parent 9881650 commit 7074182
Show file tree
Hide file tree
Showing 16 changed files with 532 additions and 11 deletions.
84 changes: 81 additions & 3 deletions extensions/authentication-jwt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class MySequence implements SequenceHandler {
</details>

- mount jwt component in application

- bind datasource to user service and refresh token
<details>
<summary markdown="span"><strong>Check The Code</strong></summary>

Expand Down Expand Up @@ -116,8 +116,10 @@ export class TestApplication extends BootMixin(
this.component(AuthenticationComponent);
// Mount jwt component
this.component(JWTAuthenticationComponent);
// Bind datasource
// Bind datasource for user
this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME);
// Bind datasource for refresh token
this.dataSource(DbDataSource, RefreshTokenBindings.DATASOURCE_NAME);

this.component(RestExplorerComponent);
this.projectRoot = __dirname;
Expand All @@ -140,7 +142,24 @@ login, then decorate endpoints with `@authentication('jwt')` to inject the
logged in user's profile.

This module contains an example application in the `fixtures` folder. It has a
controller with endpoints `/login` and `/whoAmI`.
controller with endpoints `/login`, `/refreshlogin`, `/refresh` and `/whoAmI`.

Before using the below snippet do not forget to inject below repositories and
bindingings
in your controller's constructor

```ts
@inject(TokenServiceBindings.TOKEN_SERVICE)
public jwtService: TokenService,
@inject(UserServiceBindings.USER_SERVICE)
public userService: UserService<User, Credentials>,
@inject(SecurityBindings.USER, {optional: true})
private user: UserProfile,
@inject(UserServiceBindings.USER_REPOSITORY)
public userRepository: UserRepository,
@inject(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE)
public refreshService: RefreshTokenService,
```

The code snippet for login function:

Expand Down Expand Up @@ -170,6 +189,44 @@ The code snippet for whoAmI function:
}
```

### Endpoints with refresh token

To add refresh token mechanism in your app, you can follow below example code at
the endpoint.

1. `To generate refresh token` : to generate the refresh token and access token
when user logins to your app with provided credentials.

```ts
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,
);
const accessToken = await this.jwtService.generateToken(userProfile);
const tokens = await this.refreshService.generateToken(
userProfile,
accessToken,
);
return tokens;
}
```

2. `To refresh the token`: to generate the access token by the refresh token
obtained from the the last login endpoint.

```ts
async refresh(
@requestBody(RefreshGrantRequestBody) refreshGrant: RefreshGrant,
): Promise<TokenObject> {
return this.refreshService.refreshToken(refreshGrant.refreshToken);
}
```

The complete file is in
[user.controller.ts](https://github.com/strongloop/loopback-next/tree/master/extensions/authentication-jwt/src/__tests__/fixtures/controllers/user.controller.ts)

Expand Down Expand Up @@ -298,6 +355,27 @@ provide your own `User` model and repository.
}
```

### Extra configurations

1. To change the token secret in your application.ts

```
// for jwt access token
this.bind(TokenServiceBindings.TOKEN_SECRET).to("<yourSecret>");
// for refresh token
this.bind(RefreshTokenServiceBindings.TOKEN_SECRET).to("<yourSecret>");
```

2. To change token expiration. to learn more about expiration time here at
[Ziet/ms](https://github.com/zeit/ms)

```
// for jwt access token expiration
this.bind(TokenServiceBindings.TOKEN_EXPIRES_IN).to("<Expiration Time in sec>");
// for refresh token expiration
this.bind(RefreshTokenServiceBindings.TOKEN_EXPIRES_IN).to("<Expiration Time in sec>");
```

## Future Work

The security specification is currently manually added in the application file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ describe('jwt authentication', () => {
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 +51,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {RestApplication} from '@loopback/rest';
import {RestExplorerComponent} from '@loopback/rest-explorer';
import {ServiceMixin} from '@loopback/service-proxy';
import path from 'path';
import {JWTAuthenticationComponent, UserServiceBindings} from '../../';
import {
JWTAuthenticationComponent,
UserServiceBindings,
RefreshTokenServiceBindings,
} from '../../';
import {DbDataSource} from './datasources/db.datasource';
import {MySequence} from './sequence';

Expand All @@ -34,6 +38,8 @@ export class TestApplication extends BootMixin(
this.component(JWTAuthenticationComponent);
// Bind datasource
this.dataSource(DbDataSource, UserServiceBindings.DATASOURCE_NAME);
//Bind datasource for refreshtoken table
this.dataSource(DbDataSource, RefreshTokenServiceBindings.DATASOURCE_NAME);

this.component(RestExplorerComponent);
this.projectRoot = __dirname;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,43 @@ import {model, property} from '@loopback/repository';
import {get, post, requestBody} from '@loopback/rest';
import {SecurityBindings, securityId, UserProfile} from '@loopback/security';
import {genSalt, hash} from 'bcryptjs';
import {TokenServiceBindings, User, UserServiceBindings} from '../../../';
import {
RefreshTokenServiceBindings,
TokenObject,
TokenServiceBindings,
User,
UserServiceBindings,
} from '../../../';
import {UserRepository} from '../../../repositories';
import {Credentials} from '../../../services/user.service';
import {RefreshTokenService} from '../../../types';

// Describes the type of grant object taken in by method "refresh"
type RefreshGrant = {
refreshToken: string;
};

// Describes the schema of grant object
const RefreshGrantSchema = {
type: 'object',
required: ['refreshToken'],
properties: {
refreshToken: {
type: 'string',
},
},
};

// Describes the request body of grant object
const RefreshGrantRequestBody = {
description: 'Reissuing Acess Token',
required: true,
content: {
'application/json': {schema: RefreshGrantSchema},
},
};

// Describe the schema of user credentials
const CredentialsSchema = {
type: 'object',
required: ['email', 'password'],
Expand Down Expand Up @@ -59,6 +92,8 @@ export class UserController {
private user: UserProfile,
@inject(UserServiceBindings.USER_REPOSITORY)
public userRepository: UserRepository,
@inject(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE)
public refreshService: RefreshTokenService,
) {}

@post('/users/signup', {
Expand Down Expand Up @@ -94,6 +129,11 @@ export class UserController {
return savedUser;
}

/**
* A login function that returns an access token. After login, include the token
* in the next requests to verify your identity.
* @param credentials User email and password
*/
@post('/users/login', {
responses: {
'200': {
Expand Down Expand Up @@ -142,4 +182,71 @@ export class UserController {
async whoAmI(): Promise<string> {
return this.user[securityId];
}
/**
* A login function that returns refresh token and access token.
* @param credentials User email and password
*/
@post('/users/refresh-login', {
responses: {
'200': {
description: 'Token',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
accessToken: {
type: 'string',
},
refreshToken: {
type: 'string',
},
},
},
},
},
},
},
})
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,
);
const accessToken = await this.jwtService.generateToken(userProfile);
const tokens = await this.refreshService.generateToken(
userProfile,
accessToken,
);
return tokens;
}

@post('/refresh', {
responses: {
'200': {
description: 'Token',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
accessToken: {
type: 'object',
},
},
},
},
},
},
},
})
async refresh(
@requestBody(RefreshGrantRequestBody) refreshGrant: RefreshGrant,
): Promise<TokenObject> {
return this.refreshService.refreshToken(refreshGrant.refreshToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ describe('token service', () => {
id: '1',
name: 'test',
};

const TOKEN_SECRET_VALUE = 'myjwts3cr3t';
const TOKEN_EXPIRES_IN_VALUE = '60';

const jwtService = new JWTService(TOKEN_SECRET_VALUE, TOKEN_EXPIRES_IN_VALUE);

it('token service generateToken() succeeds', async () => {
Expand All @@ -31,7 +33,6 @@ describe('token service', () => {
it('token service verifyToken() succeeds', async () => {
const token = await jwtService.generateToken(USER_PROFILE);
const userProfileFromToken = await jwtService.verifyToken(token);

expect(userProfileFromToken).to.deepEqual(DECODED_USER_PROFILE);
});

Expand Down
1 change: 1 addition & 0 deletions extensions/authentication-jwt/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './keys';
export * from './models';
export * from './repositories';
export * from './services';
export * from './types';
29 changes: 27 additions & 2 deletions extensions/authentication-jwt/src/jwt-authentication-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ import {
inject,
} from '@loopback/core';
import {
RefreshTokenConstants,
RefreshTokenServiceBindings,
TokenServiceBindings,
TokenServiceConstants,
UserServiceBindings,
} from './keys';
import {UserCredentialsRepository, UserRepository} from './repositories';
import {MyUserService} from './services';
import {
RefreshTokenRepository,
UserCredentialsRepository,
UserRepository,
} from './repositories';
import {MyUserService, RefreshtokenService} from './services';
import {JWTAuthenticationStrategy} from './services/jwt.auth.strategy';
import {JWTService} from './services/jwt.service';
import {SecuritySpecEnhancer} from './services/security.spec.enhancer';
Expand All @@ -41,6 +47,25 @@ export class JWTAuthenticationComponent implements Component {
UserCredentialsRepository,
),
createBindingFromClass(SecuritySpecEnhancer),
///refresh bindings
Binding.bind(RefreshTokenServiceBindings.REFRESH_TOKEN_SERVICE).toClass(
RefreshtokenService,
),

// Refresh token bindings
Binding.bind(RefreshTokenServiceBindings.REFRESH_SECRET).to(
RefreshTokenConstants.REFRESH_SECRET_VALUE,
),
Binding.bind(RefreshTokenServiceBindings.REFRESH_EXPIRES_IN).to(
RefreshTokenConstants.REFRESH_EXPIRES_IN_VALUE,
),
Binding.bind(RefreshTokenServiceBindings.REFRESH_ISSUER).to(
RefreshTokenConstants.REFRESH_ISSUER_VALUE,
),
//refresh token repository binding
Binding.bind(RefreshTokenServiceBindings.REFRESH_REPOSITORY).toClass(
RefreshTokenRepository,
),
];
constructor(@inject(CoreBindings.APPLICATION_INSTANCE) app: Application) {
registerAuthenticationStrategy(app, JWTAuthenticationStrategy);
Expand Down
Loading

0 comments on commit 7074182

Please sign in to comment.