-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(authentication-jwt): implementation of refresh token
feature refresh token implemented through interceptors
- Loading branch information
Showing
17 changed files
with
465 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
84 changes: 84 additions & 0 deletions
84
extensions/authentication-jwt/src/interceptors/refresh-token-generate.interceptor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`, | ||
); | ||
} | ||
} | ||
} |
97 changes: 97 additions & 0 deletions
97
extensions/authentication-jwt/src/interceptors/refresh-token-grant.interceptor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import {TokenService} from '@loopback/authentication'; | ||
import { | ||
inject, | ||
Interceptor, | ||
InvocationContext, | ||
InvocationResult, | ||
Provider, | ||
ValueOrPromise, | ||
} from '@loopback/context'; | ||
import {repository} from '@loopback/repository/'; | ||
import {HttpErrors} from '@loopback/rest'; | ||
import {UserProfile} from '@loopback/security'; | ||
import {promisify} from 'util'; | ||
import { | ||
RefreshTokenInterceptorBindings, | ||
UserServiceBindings, | ||
TokenServiceBindings, | ||
} from '../keys'; | ||
import {MyUserService} from '../services'; | ||
import {RefreshTokenRepository} from '../repositories'; | ||
|
||
const jwt = require('jsonwebtoken'); | ||
const verifyAsync = promisify(jwt.verify); | ||
/** | ||
* This class will be bound to the application as an `Interceptor` during | ||
* `boot` | ||
*/ | ||
export class RefreshTokenGrantInterceptor implements Provider<Interceptor> { | ||
constructor( | ||
@inject(RefreshTokenInterceptorBindings.REFRESH_SECRET) | ||
private refreshSecret: string, | ||
@inject(UserServiceBindings.USER_SERVICE) public userService: MyUserService, | ||
@repository(RefreshTokenRepository) | ||
public refreshTokenRepository: RefreshTokenRepository, | ||
@inject(TokenServiceBindings.TOKEN_SERVICE) public jwtService: TokenService, | ||
) {} | ||
/** | ||
* 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 refreshToken = invocationCtx.args[0].refreshToken; | ||
|
||
if (!refreshToken) { | ||
throw new HttpErrors.Unauthorized( | ||
`Error verifying token : 'refresh token' is null`, | ||
); | ||
} | ||
|
||
await verifyAsync(refreshToken, this.refreshSecret); | ||
const userRefreshData = await this.refreshTokenRepository.findOne({ | ||
where: {refreshToken: refreshToken}, | ||
}); | ||
|
||
if (!userRefreshData) { | ||
throw new HttpErrors.Unauthorized( | ||
`Error verifying token : Invalid Token`, | ||
); | ||
} | ||
const user = await this.userService.findUserById( | ||
userRefreshData.userId.toString(), | ||
); | ||
const userProfile: UserProfile = this.userService.convertToUserProfile( | ||
user, | ||
); | ||
// create a JSON Web Token based on the user profile | ||
const token = await this.jwtService.generateToken(userProfile); | ||
result = { | ||
accessToken: token, | ||
}; | ||
return result; | ||
} catch (error) { | ||
// Add error handling logic here | ||
throw new HttpErrors.Unauthorized( | ||
`Error verifying token : ${error.message}`, | ||
); | ||
} | ||
} | ||
} |
Oops, something went wrong.