Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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