Skip to content
Closed
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
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: '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 @@ -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