Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/auth : created NbAuthOauth2JWTToken class for handling Oauth2 tokens encapsulating jwt access_token #583

Merged
merged 50 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
cef7a1d
Added password grant_type option
alain-charles Jun 29, 2018
9f0dbab
Added password grant_type option implementation
alain-charles Jun 29, 2018
44d7d2e
Playground sample for oAuth2 password grant-type
alain-charles Jun 29, 2018
f57b737
Added route to oAuth2 password grant-type sample
alain-charles Jun 29, 2018
ed4628e
Addes angular-jwt for decoding jwt token (used in playground only)
alain-charles Jun 29, 2018
d7f01df
Added /api/auth/token endpoint (oAuth2 password grant-type playground)
alain-charles Jun 29, 2018
af52e88
Patched code for passing ci
alain-charles Jun 29, 2018
554f7dc
code optimization for passing ci
alain-charles Jun 29, 2018
fa0548f
Merge branch 'master' into master
nnixaa Jun 29, 2018
b72adae
Changes request by Dmitry (first review)
alain-charles Jun 29, 2018
733efc0
Generalized code in oauth2-strategy
alain-charles Jun 29, 2018
57fab69
Merge remote-tracking branch 'origin/master'
alain-charles Jun 29, 2018
c50df3a
Generalized code in oauth2-strategy
alain-charles Jun 29, 2018
dda5436
Added unit tests for oAuth-strategy password authentication
alain-charles Jul 2, 2018
ef0be25
Merge branch 'master' into master
alain-charles Jul 3, 2018
e180dc3
Merge branch 'master' into master
nnixaa Jul 4, 2018
6aa1fdb
Cleaned code according to nnixaa second review
alain-charles Jul 4, 2018
96c7344
Merge branch 'master' of https://github.com/alain-charles/nebular
alain-charles Jul 4, 2018
cf82c42
Removed package-lock.json from git repo
alain-charles Jul 4, 2018
dcff930
package-lock.json to revert
alain-charles Jul 4, 2018
315662f
reverted package-lock.json
alain-charles Jul 4, 2018
57a0230
Added new grant_type 'PASSWORD' in the block comment for it to be ins…
alain-charles Jul 4, 2018
d69d633
Merge remote-tracking branch 'upstream/master'
alain-charles Jul 5, 2018
1be8d91
Add the refreshtoken request management in NbJwtInterceptor and NbAut…
alain-charles Jul 13, 2018
4b67825
Merge branch 'master' of https://github.com/alain-charles/nebular
alain-charles Jul 19, 2018
deea3e2
Merge branch 'master' into temp
alain-charles Jul 19, 2018
acd0241
The token now contains ownerStrategyName, with is a back link to the …
alain-charles Jul 19, 2018
25250f0
feature/auth:
alain-charles Jul 19, 2018
c0fdd31
feature/auth:
alain-charles Jul 19, 2018
bd564b3
feature/auth:
alain-charles Jul 20, 2018
44ed61a
Merge branch 'master' into master
nnixaa Jul 20, 2018
9c16387
feature/auth
alain-charles Jul 24, 2018
2dd26c4
Merge remote-tracking branch 'origin/master'
alain-charles Jul 24, 2018
b284377
Merge branch 'master' into master
alain-charles Jul 24, 2018
e462c7d
feature/auth
alain-charles Jul 24, 2018
a3ee10b
Merge branch 'master' into master
nnixaa Jul 24, 2018
b38ae55
Merge branch 'master' into master
nnixaa Jul 25, 2018
62a2aff
feature/auth
alain-charles Jul 25, 2018
66ca493
Merge remote-tracking branch 'origin/master'
alain-charles Jul 25, 2018
3bd5a50
feature/auth
alain-charles Jul 25, 2018
398c058
feature/auth
alain-charles Jul 25, 2018
ee706d3
Merge branch 'master' into master
alain-charles Jul 26, 2018
5eb5821
feature/auth
alain-charles Jul 26, 2018
1a08225
Merge remote-tracking branch 'origin/master'
alain-charles Jul 26, 2018
1d697bc
feature/auth
alain-charles Jul 27, 2018
1c25956
Merge remote-tracking branch 'upstream/master'
alain-charles Jul 27, 2018
ff0e8d3
Merge remote-tracking branch 'upstream/master'
alain-charles Jul 27, 2018
e7af99e
Optimized getAccessTokenPayload() calls
alain-charles Jul 28, 2018
95632b1
Optimized getAccessTokenPayload() calls
alain-charles Jul 28, 2018
bc4001f
Merge branch 'master' into master
nnixaa Jul 30, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so in case when OAuth2 token is JWT, the iat fields is a part of JWT access token, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly. Redundant with expires_in of the 'containing' token

}
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();
}
}
}