diff --git a/src/framework/auth/services/token/token.spec.ts b/src/framework/auth/services/token/token.spec.ts index f88962cfcf..d6d0f449a2 100644 --- a/src/framework/auth/services/token/token.spec.ts +++ b/src/framework/auth/services/token/token.spec.ts @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken } from './token'; +import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken, NbAuthOAuth2JWTToken } from './token'; describe('auth token', () => { @@ -38,7 +38,7 @@ describe('auth token', () => { invalidJWTToken.getPayload(); }) .toThrow(new Error( - `The token ${invalidJWTToken.getValue()} is not valid JWT token and must consist of three parts.`)); + `The payload ${invalidJWTToken.getValue()} is not valid JWT payload and must consist of three parts.`)); }); it('getPayload, not valid JWT token, cannot be decoded', () => { @@ -46,7 +46,7 @@ describe('auth token', () => { emptyJWTToken.getPayload(); }) .toThrow(new Error( - `The token ${emptyJWTToken.getValue()} is not valid JWT token and cannot be decoded.`)); + `The payload ${emptyJWTToken.getValue()} is not valid JWT payload and cannot be decoded.`)); }); it('getPayload, not valid base64 in JWT token, cannot be decoded', () => { @@ -54,7 +54,23 @@ describe('auth token', () => { invalidBase64JWTToken.getPayload(); }) .toThrow(new Error( - `The token ${invalidBase64JWTToken.getValue()} is not valid JWT token and cannot be parsed.`)); + `The payload ${invalidBase64JWTToken.getValue()} is not valid JWT payload and cannot be parsed.`)); + }); + + it('getCreatedAt success : now for simpleToken', () => { + // we consider dates are the same if differing from minus than 10 ms + expect(simpleToken.getCreatedAt().getTime() - now.getTime() < 10); + }); + + it('getCreatedAt success : exp for validJWTToken', () => { + const date = new Date(); + date.setTime(1532350800000) + expect(validJWTToken.getCreatedAt()).toEqual(date); + }); + + it('getCreatedAt success : now for noIatJWTToken', () => { + // we consider dates are the same if differing from minus than 10 ms + expect(noIatJWTToken.getCreatedAt().getTime() - now.getTime() < 10); }); it('getCreatedAt success : now for simpleToken', () => { @@ -206,4 +222,114 @@ describe('auth token', () => { expect(NbAuthOAuth2Token.NAME).toEqual(validToken.getName()); }); }); + + describe('NbAuthOAuth2JWTToken', () => { + + const exp = 2532350800; + const iat = 1532350800; + const expires_in = 1000000000; + + const accessTokenPayload = { + 'iss': 'cerema.fr', + 'iat': 1532350800, + 'exp': 2532350800, + 'sub': 'Alain CHARLES', + 'admin': true, + }; + + const validPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsImV4cCI6MjUzMjM1MDgwMCwic3ViIjoiQWxhaW4gQ0hBUkxFUyIsImFkbWluIjp0cnVlfQ.Rgkgb4KvxY2wp2niXIyLJNJeapFp9z3tCF-zK6Omc8c', + expires_in: 1000000000, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const noExpButIatPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsInN1YiI6IkFsYWluIENIQVJMRVMiLCJhZG1pbiI6dHJ1ZX0.heHVXkHexwqbPCPUAvkJlXO6tvxzxTKf4iP0OWBbp7Y', + expires_in: expires_in, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const noExpNoIatPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJzdWIiOiJBbGFpbiBDSEFSTEVTIiwiYWRtaW4iOnRydWV9.LKZggkN-r_5hnEcCg5GzbSqZz5_SUHEB1Bf9Sy1qJd4', + expires_in: expires_in, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const permanentPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJzdWIiOiJBbGFpbiBDSEFSTEVTIiwiYWRtaW4iOnRydWV9.LKZggkN-r_5hnEcCg5GzbSqZz5_SUHEB1Bf9Sy1qJd4', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const validToken = new NbAuthOAuth2JWTToken(validPayload, 'strategy'); + let noExpButIatToken = new NbAuthOAuth2JWTToken(noExpButIatPayload, 'strategy'); + const emptyToken = new NbAuthOAuth2JWTToken({}, 'strategy'); + const permanentToken = new NbAuthOAuth2JWTToken(permanentPayload, 'strategy'); + + it('getPayload success', () => { + expect(validToken.getPayload()).toEqual(validPayload); + }); + + it('getAccessTokenPayload success', () => { + expect(validToken.getAccessTokenPayload()).toEqual(accessTokenPayload); + }); + + it('getPayload, not valid token, cannot be decoded', () => { + expect(() => { + emptyToken.getPayload(); + }) + .toThrow(new Error( + `Cannot extract payload from an empty token.`)); + }); + + it('getCreatedAt success for valid token', () => { + const date = new Date(0); + date.setUTCSeconds(iat); + expect(validToken.getCreatedAt()).toEqual(date); + }); + + it('getCreatedAt success for no iat token', () => { + noExpButIatToken = new NbAuthOAuth2JWTToken(noExpButIatPayload, 'strategy'); + const date = new Date(); + expect(noExpButIatToken.getTokenExpDate().getTime() - date.getTime() < 10); + }); + + it('getExpDate success when exp is set', () => { + const date = new Date(0); + date.setUTCSeconds(exp); + expect(validToken.getTokenExpDate()).toEqual(date); + }); + + it('getExpDate success when exp is not set but iat and expires_in are set', () => { + const date = new Date(0); + date.setUTCSeconds(iat + expires_in); + expect(noExpButIatToken.getTokenExpDate()).toEqual(date); + }); + + it('getExpDate success when only expires_in is set', () => { + const NoExpNoIatToken = new NbAuthOAuth2JWTToken(noExpNoIatPayload, 'strategy'); + const date = new Date(); + date.setTime(date.getTime() + expires_in * 1000); + expect(NoExpNoIatToken.getTokenExpDate().getTime() - date.getTime() < 10); + }); + + it('getTokenExpDate is empty', () => { + expect(permanentToken.getTokenExpDate()).toBeNull(); + }); + + it('name', () => { + expect(NbAuthOAuth2JWTToken.NAME).toEqual(validToken.getName()); + }); + }); + }); diff --git a/src/framework/auth/services/token/token.ts b/src/framework/auth/services/token/token.ts index ea02db6e3b..94431826c6 100644 --- a/src/framework/auth/services/token/token.ts +++ b/src/framework/auth/services/token/token.ts @@ -30,6 +30,32 @@ export function nbAuthCreateToken(tokenClass: NbAuthTokenClass, return new tokenClass(token, ownerStrategyName, createdAt); } +export function decodeJwtPayload(payload: string): string { + + if (!payload) { + throw new Error('Cannot extract payload from an empty token.'); + } + + const parts = payload.split('.'); + + if (parts.length !== 3) { + throw new Error(`The payload ${payload} is not valid JWT payload and must consist of three parts.`); + } + + let decoded; + try { + decoded = urlBase64Decode(parts[1]); + } catch (e) { + throw new Error(`The payload ${payload} is not valid JWT payload and cannot be parsed.`); + } + + if (!decoded) { + throw new Error(`The payload ${payload} is not valid JWT payload and cannot be decoded.`); + } + + return JSON.parse(decoded); +} + /** * Wrapper for simple (text) token */ @@ -45,7 +71,6 @@ export class NbAuthSimpleToken extends NbAuthToken { } protected prepareCreatedAt(date: Date) { - // For simple tokens, if not set the creation date is 'now' return date ? date : new Date(); } @@ -101,13 +126,12 @@ export class NbAuthJWTToken extends NbAuthSimpleToken { * for JWT token, the iat (issued at) field of the token payload contains the creation Date */ protected prepareCreatedAt(date: Date) { - date = super.prepareCreatedAt(date); - let decoded = null; - try { // needed as getPayload() throws error and we want the token to be created in any case + let decoded; + try { decoded = this.getPayload(); } finally { - return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : date; + return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date); } } @@ -116,29 +140,7 @@ export class NbAuthJWTToken extends NbAuthSimpleToken { * @returns any */ getPayload(): any { - - if (!this.token) { - throw new Error('Cannot extract payload from an empty token.'); - } - - const parts = this.token.split('.'); - - if (parts.length !== 3) { - throw new Error(`The token ${this.token} is not valid JWT token and must consist of three parts.`); - } - - let decoded; - try { - decoded = urlBase64Decode(parts[1]); - } catch (e) { - throw new Error(`The token ${this.token} is not valid JWT token and cannot be parsed.`); - } - - if (!decoded) { - throw new Error(`The token ${this.token} is not valid JWT token and cannot be decoded.`); - } - - return JSON.parse(decoded); + return decodeJwtPayload(this.token); } /** @@ -174,7 +176,7 @@ const prepareOAuth2Token = (data) => { }; /** - * Wrapper for OAuth2 token + * Wrapper for OAuth2 token whose access_token is a JWT Token */ export class NbAuthOAuth2Token extends NbAuthSimpleToken { @@ -251,3 +253,50 @@ export class NbAuthOAuth2Token extends NbAuthSimpleToken { return JSON.stringify(this.token); } } + +/** + * Wrapper for OAuth2 token + */ +export class NbAuthOAuth2JWTToken extends NbAuthOAuth2Token { + + static NAME = 'nb:auth:oauth2:jwt:token'; + + /** + * for Oauth2 JWT token, the iat (issued at) field of the access_token payload + */ + protected prepareCreatedAt(date: Date) { + let decoded; + try { + decoded = this.getAccessTokenPayload(); + } + finally { + return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date); + } + } + + + /** + * Returns access token payload + * @returns any + */ + getAccessTokenPayload(): any { + return decodeJwtPayload(this.getValue()) + } + + /** + * Returns expiration date : + * - exp if set, + * - super.getExpDate() otherwise + * @returns Date + */ + getTokenExpDate(): Date { + const accessTokenPayload = this.getAccessTokenPayload(); + if (accessTokenPayload.hasOwnProperty('exp')) { + const date = new Date(0); + date.setUTCSeconds(accessTokenPayload.exp); + return date; + } else { + return super.getTokenExpDate(); + } + } +}