-
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): implementing refresh token
Refresh token implementation through interceptor
- Loading branch information
Showing
19 changed files
with
536 additions
and
10 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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}`, | ||
); | ||
} | ||
} | ||
} |
Oops, something went wrong.