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 : Rejects malformed JWTToken and requireValidToken Option #597

Merged
merged 72 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
72 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
e06a45c
Feat/auth
alain-charles Jul 31, 2018
7c6fc43
Merge remote-tracking branch 'upstream/master' into feat/auth-Automat…
alain-charles Jul 31, 2018
88c7106
Moved code from tokenservice to strategy.
alain-charles Jul 31, 2018
76dda02
Added setRefreshToken(token)
alain-charles Jul 31, 2018
a75f75b
Merge remote-tracking branch 'upstream/master' into feat/auth_require…
alain-charles Aug 1, 2018
ab75598
feat/auth
alain-charles Aug 2, 2018
2ad04ba
feat/auth
alain-charles Aug 2, 2018
cfe6b87
Merge remote-tracking branch 'upstream/master' into feat/auth_require…
alain-charles Aug 2, 2018
ad90f20
feat/auth
alain-charles Aug 2, 2018
66cf701
Merge remote-tracking branch 'upstream/master' into feat/auth_require…
alain-charles Aug 7, 2018
9d35278
Removed not released failWhenNoToken from password strategy as requir…
alain-charles Aug 7, 2018
432b2cc
Corrected import paths
alain-charles Aug 7, 2018
01e2c2f
Merge remote-tracking branch 'upstream/master' into feat/auth_require…
alain-charles Aug 7, 2018
a22d3ef
Merge branch 'master' into feat/auth_requireValidToken
nnixaa Aug 7, 2018
ffef510
Merge branch 'master' into feat/auth_requireValidToken
nnixaa Aug 8, 2018
03e1fae
Payload(s) are now parsed during token construction
alain-charles Aug 9, 2018
cb1bfed
Merge remote-tracking branch 'upstream/master' into feat/auth_require…
alain-charles Aug 9, 2018
b6a9db6
Merge remote-tracking branch 'origin/feat/auth_requireValidToken' int…
alain-charles Aug 9, 2018
4b57287
Inverted error test in parsePayload
alain-charles Aug 10, 2018
27d74a7
Merge branch 'master' into feat/auth_requireValidToken
nnixaa Aug 13, 2018
9a8d41f
Merge remote-tracking branch 'upstream/master' into feat/auth_require…
alain-charles Aug 14, 2018
ae46531
Merge remote-tracking branch 'origin/feat/auth_requireValidToken' int…
alain-charles Aug 14, 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
44 changes: 19 additions & 25 deletions src/framework/auth/services/token/token.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ describe('auth token', () => {
// tslint:disable
const simpleToken = new NbAuthSimpleToken('token','strategy');
const validJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsImV4cCI6MjUzMjM1MDgwMCwic3ViIjoiQWxhaW4gQ0hBUkxFUyIsImFkbWluIjp0cnVlfQ.Rgkgb4KvxY2wp2niXIyLJNJeapFp9z3tCF-zK6Omc8c', 'strategy');
const emptyJWTToken = new NbAuthJWTToken('..', 'strategy');
const invalidBase64JWTToken = new NbAuthJWTToken('h%2BHY.h%2BHY.h%2BHY','strategy');

const invalidJWTToken = new NbAuthJWTToken('.','strategy');

const noIatJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJleHAiOjE1MzI0MzcyMDAsInN1YiI6IkFsYWluIENIQVJMRVMiLCJhZG1pbiI6dHJ1ZX0.cfwQlKo6xomXkE-U-SOqse2GjdxncOuhdd1VWIOiYzA', 'strategy');

Expand All @@ -27,36 +23,37 @@ describe('auth token', () => {

// tslint:enable

it('getPayload success', () => {
expect(validJWTToken.getPayload())
// tslint:disable-next-line
.toEqual(JSON.parse('{"iss":"cerema.fr","iat":1532350800,"exp":2532350800,"sub":"Alain CHARLES","admin":true}'));
});

it('getPayload, not valid JWT token, must consist of three parts', () => {
it('JWT Token constructor, not valid JWT token, must consist of three parts', () => {
expect(() => {
invalidJWTToken.getPayload();
new NbAuthJWTToken('.', 'strategy');
})
.toThrow(new Error(
`The payload ${invalidJWTToken.getValue()} is not valid JWT payload and must consist of three parts.`));
`The payload . is not valid JWT payload and must consist of three parts.`));
});

it('getPayload, not valid JWT token, cannot be decoded', () => {
it('JWT Token constructor,, not valid JWT token, cannot be decoded', () => {
expect(() => {
emptyJWTToken.getPayload();
new NbAuthJWTToken('..', 'strategy');
})
.toThrow(new Error(
`The payload ${emptyJWTToken.getValue()} is not valid JWT payload and cannot be decoded.`));
`The payload .. is not valid JWT payload and cannot be decoded.`));
});

it('getPayload, not valid base64 in JWT token, cannot be decoded', () => {
expect(() => {
invalidBase64JWTToken.getPayload();
new NbAuthJWTToken('h%2BHY.h%2BHY.h%2BHY', 'strategy');
})
.toThrow(new Error(
`The payload ${invalidBase64JWTToken.getValue()} is not valid JWT payload and cannot be parsed.`));
`The payload h%2BHY.h%2BHY.h%2BHY is not valid JWT payload and cannot be parsed.`));
});

it('getPayload success', () => {
expect(validJWTToken.getPayload())
// tslint:disable-next-line
.toEqual(JSON.parse('{"iss":"cerema.fr","iat":1532350800,"exp":2532350800,"sub":"Alain CHARLES","admin":true}'));
});


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);
Expand Down Expand Up @@ -166,8 +163,6 @@ describe('auth token', () => {

let validToken = new NbAuthOAuth2Token(token, 'strategy');

const emptyToken = new NbAuthOAuth2Token({}, 'strategy');

const noExpToken = new NbAuthOAuth2Token({
access_token: '2YotnFZFEjr1zCsicMWpAA',
refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA',
Expand All @@ -178,9 +173,9 @@ describe('auth token', () => {
expect(validToken.getPayload()).toEqual(token);
});

it('getPayload, not valid token, cannot be decoded', () => {
it('empty token constructor, not valid token, cannot be decoded', () => {
expect(() => {
emptyToken.getPayload();
new NbAuthOAuth2Token({}, 'strategy');
})
.toThrow(new Error(
`Cannot extract payload from an empty token.`));
Expand Down Expand Up @@ -273,7 +268,6 @@ describe('auth token', () => {

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', () => {
Expand All @@ -284,9 +278,9 @@ describe('auth token', () => {
expect(validToken.getAccessTokenPayload()).toEqual(accessTokenPayload);
});

it('getPayload, not valid token, cannot be decoded', () => {
it('empty token constructor, not valid token, cannot be decoded', () => {
expect(() => {
emptyToken.getPayload();
new NbAuthOAuth2JWTToken({}, 'strategy');
})
.toThrow(new Error(
`Cannot extract payload from an empty token.`));
Expand Down
146 changes: 103 additions & 43 deletions src/framework/auth/services/token/token.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { urlBase64Decode } from '../../helpers';

export abstract class NbAuthToken {

protected payload: any = null;

abstract getValue(): string;
abstract isValid(): boolean;
abstract getPayload(): string;
// the strategy name used to acquire this token (needed for refreshing token)
abstract getOwnerStrategyName(): string;
abstract getCreatedAt(): Date;
Expand All @@ -12,6 +14,38 @@ export abstract class NbAuthToken {
getName(): string {
return (this.constructor as NbAuthTokenClass).NAME;
}

getPayload(): any {
return this.payload;
}
}

export class NbAuthTokenNotFoundError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

export class NbAuthIllegalTokenError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

export class NbAuthEmptyTokenError extends NbAuthIllegalTokenError {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

export class NbAuthIllegalJWTTokenError extends NbAuthIllegalTokenError {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

we also need another error then, as we cannot use InvalidJWTTokenError everywhere, since not every token is JWT token.
probably like this
NbAuthInvalidTokenError
NbAuthInvalidJWTTokenError

export interface NbAuthRefreshableToken {
Expand All @@ -31,29 +65,31 @@ export function nbAuthCreateToken<T extends NbAuthToken>(tokenClass: NbAuthToken
return new tokenClass(token, ownerStrategyName, createdAt);
}

export function decodeJwtPayload(payload: string): string {
export function decodeJwtPayload(payload: string): any {

if (!payload) {
throw new Error('Cannot extract payload from an empty token.');
if (payload.length === 0) {
throw new NbAuthEmptyTokenError('Cannot extract from an empty payload.');
}

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.`);
throw new NbAuthIllegalJWTTokenError(
`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.`);
throw new NbAuthIllegalJWTTokenError(
`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.`);
throw new NbAuthIllegalJWTTokenError(
`The payload ${payload} is not valid JWT payload and cannot be decoded.`);
}

return JSON.parse(decoded);
}

Expand All @@ -68,9 +104,20 @@ export class NbAuthSimpleToken extends NbAuthToken {
protected readonly ownerStrategyName: string,
protected createdAt?: Date) {
super();
try {
this.parsePayload();
} catch (err) {
if (err instanceof NbAuthIllegalTokenError) { // token is present but illegal (empty or malformed), we reject it
throw err;
}
}
this.createdAt = this.prepareCreatedAt(createdAt);
}

protected parsePayload(): any {
this.payload = null;
}

protected prepareCreatedAt(date: Date) {
return date ? date : new Date();
}
Expand All @@ -95,10 +142,6 @@ export class NbAuthSimpleToken extends NbAuthToken {
return this.ownerStrategyName;
}

getPayload(): string {
return null;
}

/**
* Is non empty and valid
* @returns {boolean}
Expand Down Expand Up @@ -127,21 +170,19 @@ 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) {
let decoded;
try {
decoded = this.getPayload();
}
finally {
const decoded = this.getPayload();
return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date);
}
}

/**
* Returns payload object
* @returns any
*/
getPayload(): any {
return decodeJwtPayload(this.token);
protected parsePayload(): void {
if (!this.token) {
throw new NbAuthTokenNotFoundError('Token not found. ')
}
this.payload = decodeJwtPayload(this.token);
}

/**
Expand All @@ -150,7 +191,7 @@ export class NbAuthJWTToken extends NbAuthSimpleToken {
*/
getTokenExpDate(): Date {
const decoded = this.getPayload();
if (!decoded.hasOwnProperty('exp')) {
if (decoded && !decoded.hasOwnProperty('exp')) {
return null;
}
const date = new Date(0);
Expand Down Expand Up @@ -216,15 +257,18 @@ export class NbAuthOAuth2Token extends NbAuthSimpleToken {
}

/**
* Returns token payload
* Parses token payload
* @returns any
*/
getPayload(): any {
if (!this.token || !Object.keys(this.token).length) {
throw new Error('Cannot extract payload from an empty token.');
protected parsePayload(): void {
if (!this.token) {
throw new NbAuthTokenNotFoundError('Token not found.')
} else {
if (!Object.keys(this.token).length) {
throw new NbAuthEmptyTokenError('Cannot extract payload from an empty token.');
}
}

return this.token;
this.payload = this.token;
}

/**
Expand Down Expand Up @@ -264,32 +308,49 @@ export class NbAuthOAuth2Token extends NbAuthSimpleToken {
}

/**
* Wrapper for OAuth2 token
* Wrapper for OAuth2 token embedding JWT tokens
*/
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);
}
protected accessTokenPayload: any;

protected parsePayload(): void {
super.parsePayload();
this.parseAccessTokenPayload();
}

protected parseAccessTokenPayload(): any {
const accessToken = this.getValue();
if (!accessToken) {
throw new NbAuthTokenNotFoundError('access_token key not found.')
}
this.accessTokenPayload = decodeJwtPayload(accessToken);
}

/**
* Returns access token payload
* @returns any
*/
getAccessTokenPayload(): any {
return decodeJwtPayload(this.getValue())
return this.accessTokenPayload;
}

/**
* for Oauth2 JWT token, the iat (issued at) field of the access_token payload
*/
protected prepareCreatedAt(date: Date) {
const payload = this.accessTokenPayload;
return payload && payload.iat ? new Date(Number(payload.iat) * 1000) : super.prepareCreatedAt(date);
}

/**
* Is token valid
* @returns {boolean}
*/
isValid(): boolean {
return this.accessTokenPayload && super.isValid();
}

/**
Expand All @@ -299,10 +360,9 @@ export class NbAuthOAuth2JWTToken extends NbAuthOAuth2Token {
* @returns Date
*/
getTokenExpDate(): Date {
const accessTokenPayload = this.getAccessTokenPayload();
if (accessTokenPayload.hasOwnProperty('exp')) {
if (this.accessTokenPayload && this.accessTokenPayload.hasOwnProperty('exp')) {
const date = new Date(0);
date.setUTCSeconds(accessTokenPayload.exp);
date.setUTCSeconds(this.accessTokenPayload.exp);
return date;
} else {
return super.getTokenExpDate();
Expand Down
18 changes: 15 additions & 3 deletions src/framework/auth/strategies/auth-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Observable } from 'rxjs';
import { NbAuthResult } from '../services/auth-result';
import { NbAuthStrategyOptions } from './auth-strategy-options';
import { deepExtend, getDeepFromObject } from '../helpers';
import { NbAuthToken, nbAuthCreateToken } from '../services/token/token';
import {
NbAuthToken,
nbAuthCreateToken,
NbAuthIllegalTokenError,
} from '../services/token/token';

export abstract class NbAuthStrategy {

Expand All @@ -20,8 +24,16 @@ export abstract class NbAuthStrategy {
return getDeepFromObject(this.options, key, null);
}

createToken<T extends NbAuthToken>(value: any): T {
return nbAuthCreateToken<T>(this.getOption('token.class'), value, this.getName());
createToken<T extends NbAuthToken>(value: any, failWhenInvalidToken?: boolean): T {
const token = nbAuthCreateToken<T>(this.getOption('token.class'), value, this.getName());
// At this point, nbAuthCreateToken failed with NbAuthIllegalTokenError which MUST be intercepted by strategies
// Or token is created. It MAY be created even if backend did not return any token, in this case it is !Valid
if (failWhenInvalidToken && !token.isValid()) {
// If we require a valid token (i.e. isValid), then we MUST throw NbAuthIllegalTokenError so that the strategies
// intercept it
throw new NbAuthIllegalTokenError('Token is empty or invalid.');
}
return token;
}

getName(): string {
Expand Down
Loading