Skip to content

Commit aed2099

Browse files
alain-charlesnnixaa
authored andcommitted
feat(auth): add new NbAuthOAuth2JWTToken (#583)
The token presumes that `access_token` is a JWT token itself.
1 parent a7b8ff4 commit aed2099

File tree

2 files changed

+208
-33
lines changed

2 files changed

+208
-33
lines changed

src/framework/auth/services/token/token.spec.ts

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Licensed under the MIT License. See License.txt in the project root for license information.
55
*/
66

7-
import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken } from './token';
7+
import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken, NbAuthOAuth2JWTToken } from './token';
88

99

1010
describe('auth token', () => {
@@ -38,23 +38,39 @@ describe('auth token', () => {
3838
invalidJWTToken.getPayload();
3939
})
4040
.toThrow(new Error(
41-
`The token ${invalidJWTToken.getValue()} is not valid JWT token and must consist of three parts.`));
41+
`The payload ${invalidJWTToken.getValue()} is not valid JWT payload and must consist of three parts.`));
4242
});
4343

4444
it('getPayload, not valid JWT token, cannot be decoded', () => {
4545
expect(() => {
4646
emptyJWTToken.getPayload();
4747
})
4848
.toThrow(new Error(
49-
`The token ${emptyJWTToken.getValue()} is not valid JWT token and cannot be decoded.`));
49+
`The payload ${emptyJWTToken.getValue()} is not valid JWT payload and cannot be decoded.`));
5050
});
5151

5252
it('getPayload, not valid base64 in JWT token, cannot be decoded', () => {
5353
expect(() => {
5454
invalidBase64JWTToken.getPayload();
5555
})
5656
.toThrow(new Error(
57-
`The token ${invalidBase64JWTToken.getValue()} is not valid JWT token and cannot be parsed.`));
57+
`The payload ${invalidBase64JWTToken.getValue()} is not valid JWT payload and cannot be parsed.`));
58+
});
59+
60+
it('getCreatedAt success : now for simpleToken', () => {
61+
// we consider dates are the same if differing from minus than 10 ms
62+
expect(simpleToken.getCreatedAt().getTime() - now.getTime() < 10);
63+
});
64+
65+
it('getCreatedAt success : exp for validJWTToken', () => {
66+
const date = new Date();
67+
date.setTime(1532350800000)
68+
expect(validJWTToken.getCreatedAt()).toEqual(date);
69+
});
70+
71+
it('getCreatedAt success : now for noIatJWTToken', () => {
72+
// we consider dates are the same if differing from minus than 10 ms
73+
expect(noIatJWTToken.getCreatedAt().getTime() - now.getTime() < 10);
5874
});
5975

6076
it('getCreatedAt success : now for simpleToken', () => {
@@ -206,4 +222,114 @@ describe('auth token', () => {
206222
expect(NbAuthOAuth2Token.NAME).toEqual(validToken.getName());
207223
});
208224
});
225+
226+
describe('NbAuthOAuth2JWTToken', () => {
227+
228+
const exp = 2532350800;
229+
const iat = 1532350800;
230+
const expires_in = 1000000000;
231+
232+
const accessTokenPayload = {
233+
'iss': 'cerema.fr',
234+
'iat': 1532350800,
235+
'exp': 2532350800,
236+
'sub': 'Alain CHARLES',
237+
'admin': true,
238+
};
239+
240+
const validPayload = {
241+
// tslint:disable-next-line
242+
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsImV4cCI6MjUzMjM1MDgwMCwic3ViIjoiQWxhaW4gQ0hBUkxFUyIsImFkbWluIjp0cnVlfQ.Rgkgb4KvxY2wp2niXIyLJNJeapFp9z3tCF-zK6Omc8c',
243+
expires_in: 1000000000,
244+
refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA',
245+
token_type: 'bearer',
246+
example_parameter: 'example_value',
247+
};
248+
249+
const noExpButIatPayload = {
250+
// tslint:disable-next-line
251+
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsInN1YiI6IkFsYWluIENIQVJMRVMiLCJhZG1pbiI6dHJ1ZX0.heHVXkHexwqbPCPUAvkJlXO6tvxzxTKf4iP0OWBbp7Y',
252+
expires_in: expires_in,
253+
refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA',
254+
token_type: 'bearer',
255+
example_parameter: 'example_value',
256+
};
257+
258+
const noExpNoIatPayload = {
259+
// tslint:disable-next-line
260+
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJzdWIiOiJBbGFpbiBDSEFSTEVTIiwiYWRtaW4iOnRydWV9.LKZggkN-r_5hnEcCg5GzbSqZz5_SUHEB1Bf9Sy1qJd4',
261+
expires_in: expires_in,
262+
refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA',
263+
token_type: 'bearer',
264+
example_parameter: 'example_value',
265+
};
266+
267+
const permanentPayload = {
268+
// tslint:disable-next-line
269+
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJzdWIiOiJBbGFpbiBDSEFSTEVTIiwiYWRtaW4iOnRydWV9.LKZggkN-r_5hnEcCg5GzbSqZz5_SUHEB1Bf9Sy1qJd4',
270+
token_type: 'bearer',
271+
example_parameter: 'example_value',
272+
};
273+
274+
const validToken = new NbAuthOAuth2JWTToken(validPayload, 'strategy');
275+
let noExpButIatToken = new NbAuthOAuth2JWTToken(noExpButIatPayload, 'strategy');
276+
const emptyToken = new NbAuthOAuth2JWTToken({}, 'strategy');
277+
const permanentToken = new NbAuthOAuth2JWTToken(permanentPayload, 'strategy');
278+
279+
it('getPayload success', () => {
280+
expect(validToken.getPayload()).toEqual(validPayload);
281+
});
282+
283+
it('getAccessTokenPayload success', () => {
284+
expect(validToken.getAccessTokenPayload()).toEqual(accessTokenPayload);
285+
});
286+
287+
it('getPayload, not valid token, cannot be decoded', () => {
288+
expect(() => {
289+
emptyToken.getPayload();
290+
})
291+
.toThrow(new Error(
292+
`Cannot extract payload from an empty token.`));
293+
});
294+
295+
it('getCreatedAt success for valid token', () => {
296+
const date = new Date(0);
297+
date.setUTCSeconds(iat);
298+
expect(validToken.getCreatedAt()).toEqual(date);
299+
});
300+
301+
it('getCreatedAt success for no iat token', () => {
302+
noExpButIatToken = new NbAuthOAuth2JWTToken(noExpButIatPayload, 'strategy');
303+
const date = new Date();
304+
expect(noExpButIatToken.getTokenExpDate().getTime() - date.getTime() < 10);
305+
});
306+
307+
it('getExpDate success when exp is set', () => {
308+
const date = new Date(0);
309+
date.setUTCSeconds(exp);
310+
expect(validToken.getTokenExpDate()).toEqual(date);
311+
});
312+
313+
it('getExpDate success when exp is not set but iat and expires_in are set', () => {
314+
const date = new Date(0);
315+
date.setUTCSeconds(iat + expires_in);
316+
expect(noExpButIatToken.getTokenExpDate()).toEqual(date);
317+
});
318+
319+
it('getExpDate success when only expires_in is set', () => {
320+
const NoExpNoIatToken = new NbAuthOAuth2JWTToken(noExpNoIatPayload, 'strategy');
321+
const date = new Date();
322+
date.setTime(date.getTime() + expires_in * 1000);
323+
expect(NoExpNoIatToken.getTokenExpDate().getTime() - date.getTime() < 10);
324+
});
325+
326+
it('getTokenExpDate is empty', () => {
327+
expect(permanentToken.getTokenExpDate()).toBeNull();
328+
});
329+
330+
it('name', () => {
331+
expect(NbAuthOAuth2JWTToken.NAME).toEqual(validToken.getName());
332+
});
333+
});
334+
209335
});

src/framework/auth/services/token/token.ts

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@ export function nbAuthCreateToken(tokenClass: NbAuthTokenClass,
3030
return new tokenClass(token, ownerStrategyName, createdAt);
3131
}
3232

33+
export function decodeJwtPayload(payload: string): string {
34+
35+
if (!payload) {
36+
throw new Error('Cannot extract payload from an empty token.');
37+
}
38+
39+
const parts = payload.split('.');
40+
41+
if (parts.length !== 3) {
42+
throw new Error(`The payload ${payload} is not valid JWT payload and must consist of three parts.`);
43+
}
44+
45+
let decoded;
46+
try {
47+
decoded = urlBase64Decode(parts[1]);
48+
} catch (e) {
49+
throw new Error(`The payload ${payload} is not valid JWT payload and cannot be parsed.`);
50+
}
51+
52+
if (!decoded) {
53+
throw new Error(`The payload ${payload} is not valid JWT payload and cannot be decoded.`);
54+
}
55+
56+
return JSON.parse(decoded);
57+
}
58+
3359
/**
3460
* Wrapper for simple (text) token
3561
*/
@@ -45,7 +71,6 @@ export class NbAuthSimpleToken extends NbAuthToken {
4571
}
4672

4773
protected prepareCreatedAt(date: Date) {
48-
// For simple tokens, if not set the creation date is 'now'
4974
return date ? date : new Date();
5075
}
5176

@@ -101,13 +126,12 @@ export class NbAuthJWTToken extends NbAuthSimpleToken {
101126
* for JWT token, the iat (issued at) field of the token payload contains the creation Date
102127
*/
103128
protected prepareCreatedAt(date: Date) {
104-
date = super.prepareCreatedAt(date);
105-
let decoded = null;
106-
try { // needed as getPayload() throws error and we want the token to be created in any case
129+
let decoded;
130+
try {
107131
decoded = this.getPayload();
108132
}
109133
finally {
110-
return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : date;
134+
return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date);
111135
}
112136
}
113137

@@ -116,29 +140,7 @@ export class NbAuthJWTToken extends NbAuthSimpleToken {
116140
* @returns any
117141
*/
118142
getPayload(): any {
119-
120-
if (!this.token) {
121-
throw new Error('Cannot extract payload from an empty token.');
122-
}
123-
124-
const parts = this.token.split('.');
125-
126-
if (parts.length !== 3) {
127-
throw new Error(`The token ${this.token} is not valid JWT token and must consist of three parts.`);
128-
}
129-
130-
let decoded;
131-
try {
132-
decoded = urlBase64Decode(parts[1]);
133-
} catch (e) {
134-
throw new Error(`The token ${this.token} is not valid JWT token and cannot be parsed.`);
135-
}
136-
137-
if (!decoded) {
138-
throw new Error(`The token ${this.token} is not valid JWT token and cannot be decoded.`);
139-
}
140-
141-
return JSON.parse(decoded);
143+
return decodeJwtPayload(this.token);
142144
}
143145

144146
/**
@@ -174,7 +176,7 @@ const prepareOAuth2Token = (data) => {
174176
};
175177

176178
/**
177-
* Wrapper for OAuth2 token
179+
* Wrapper for OAuth2 token whose access_token is a JWT Token
178180
*/
179181
export class NbAuthOAuth2Token extends NbAuthSimpleToken {
180182

@@ -251,3 +253,50 @@ export class NbAuthOAuth2Token extends NbAuthSimpleToken {
251253
return JSON.stringify(this.token);
252254
}
253255
}
256+
257+
/**
258+
* Wrapper for OAuth2 token
259+
*/
260+
export class NbAuthOAuth2JWTToken extends NbAuthOAuth2Token {
261+
262+
static NAME = 'nb:auth:oauth2:jwt:token';
263+
264+
/**
265+
* for Oauth2 JWT token, the iat (issued at) field of the access_token payload
266+
*/
267+
protected prepareCreatedAt(date: Date) {
268+
let decoded;
269+
try {
270+
decoded = this.getAccessTokenPayload();
271+
}
272+
finally {
273+
return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date);
274+
}
275+
}
276+
277+
278+
/**
279+
* Returns access token payload
280+
* @returns any
281+
*/
282+
getAccessTokenPayload(): any {
283+
return decodeJwtPayload(this.getValue())
284+
}
285+
286+
/**
287+
* Returns expiration date :
288+
* - exp if set,
289+
* - super.getExpDate() otherwise
290+
* @returns Date
291+
*/
292+
getTokenExpDate(): Date {
293+
const accessTokenPayload = this.getAccessTokenPayload();
294+
if (accessTokenPayload.hasOwnProperty('exp')) {
295+
const date = new Date(0);
296+
date.setUTCSeconds(accessTokenPayload.exp);
297+
return date;
298+
} else {
299+
return super.getTokenExpDate();
300+
}
301+
}
302+
}

0 commit comments

Comments
 (0)