Skip to content

Commit

Permalink
feat(auth): add new NbAuthOAuth2JWTToken (#583)
Browse files Browse the repository at this point in the history
The token presumes that `access_token` is a JWT token itself.
  • Loading branch information
alain-charles authored and nnixaa committed Jul 30, 2018
1 parent a7b8ff4 commit aed2099
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 33 deletions.
134 changes: 130 additions & 4 deletions src/framework/auth/services/token/token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -38,23 +38,39 @@ 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', () => {
expect(() => {
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', () => {
expect(() => {
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', () => {
Expand Down Expand Up @@ -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());
});
});

});
107 changes: 78 additions & 29 deletions src/framework/auth/services/token/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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();
}
}
}

0 comments on commit aed2099

Please sign in to comment.