diff --git a/src/framework/auth/services/token/token.spec.ts b/src/framework/auth/services/token/token.spec.ts index d6d0f449a2..3609ac7759 100644 --- a/src/framework/auth/services/token/token.spec.ts +++ b/src/framework/auth/services/token/token.spec.ts @@ -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'); @@ -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); @@ -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', @@ -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.`)); @@ -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', () => { @@ -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.`)); diff --git a/src/framework/auth/services/token/token.ts b/src/framework/auth/services/token/token.ts index 8c8904247f..a24c1d733f 100644 --- a/src/framework/auth/services/token/token.ts +++ b/src/framework/auth/services/token/token.ts @@ -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; @@ -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); + } } export interface NbAuthRefreshableToken { @@ -31,29 +65,31 @@ export function nbAuthCreateToken(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); } @@ -68,9 +104,21 @@ export class NbAuthSimpleToken extends NbAuthToken { protected readonly ownerStrategyName: string, protected createdAt?: Date) { super(); + try { + this.parsePayload(); + } catch (err) { + if (!(err instanceof NbAuthTokenNotFoundError)) { + // token is present but has got a problem, including illegal + throw err; + } + } this.createdAt = this.prepareCreatedAt(createdAt); } + protected parsePayload(): any { + this.payload = null; + } + protected prepareCreatedAt(date: Date) { return date ? date : new Date(); } @@ -95,10 +143,6 @@ export class NbAuthSimpleToken extends NbAuthToken { return this.ownerStrategyName; } - getPayload(): string { - return null; - } - /** * Is non empty and valid * @returns {boolean} @@ -127,21 +171,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); } /** @@ -150,7 +192,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); @@ -216,15 +258,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; } /** @@ -264,32 +309,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(); } /** @@ -299,10 +361,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(); diff --git a/src/framework/auth/strategies/auth-strategy.ts b/src/framework/auth/strategies/auth-strategy.ts index 8890d1b021..4b4b7991fb 100644 --- a/src/framework/auth/strategies/auth-strategy.ts +++ b/src/framework/auth/strategies/auth-strategy.ts @@ -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 { @@ -20,8 +24,16 @@ export abstract class NbAuthStrategy { return getDeepFromObject(this.options, key, null); } - createToken(value: any): T { - return nbAuthCreateToken(this.getOption('token.class'), value, this.getName()); + createToken(value: any, failWhenInvalidToken?: boolean): T { + const token = nbAuthCreateToken(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 { diff --git a/src/framework/auth/strategies/dummy/dummy-strategy-options.ts b/src/framework/auth/strategies/dummy/dummy-strategy-options.ts index 72efc90e81..04b28e2405 100644 --- a/src/framework/auth/strategies/dummy/dummy-strategy-options.ts +++ b/src/framework/auth/strategies/dummy/dummy-strategy-options.ts @@ -7,7 +7,6 @@ import { NbAuthStrategyOptions } from '../auth-strategy-options'; import { NbAuthSimpleToken } from '../../services/'; export class NbDummyAuthStrategyOptions extends NbAuthStrategyOptions { - name: string; token? = { class: NbAuthSimpleToken, }; diff --git a/src/framework/auth/strategies/dummy/dummy-strategy.ts b/src/framework/auth/strategies/dummy/dummy-strategy.ts index 615b51d2f2..47af2da3c5 100644 --- a/src/framework/auth/strategies/dummy/dummy-strategy.ts +++ b/src/framework/auth/strategies/dummy/dummy-strategy.ts @@ -78,6 +78,7 @@ export class NbDummyAuthStrategy extends NbAuthStrategy { } protected createDummyResult(data?: any): NbAuthResult { + if (this.getOption('alwaysFail')) { return new NbAuthResult( false, @@ -87,13 +88,25 @@ export class NbDummyAuthStrategy extends NbAuthStrategy { ); } - return new NbAuthResult( - true, - this.createSuccessResponse(data), - '/', - [], - ['Successfully logged in.'], - this.createToken('test token'), - ); + try { + const token = this.createToken('test token', true); + return new NbAuthResult( + true, + this.createSuccessResponse(data), + '/', + [], + ['Successfully logged in.'], + token, + ); + } catch (err) { + return new NbAuthResult( + false, + this.createFailResponse(data), + null, + [err.message], + ); + } + + } } diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts index b7c2c9813f..cb07e1a2bc 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts @@ -5,6 +5,7 @@ */ import { NbAuthOAuth2Token, NbAuthTokenClass } from '../../services'; +import { NbAuthStrategyOptions } from '../auth-strategy-options'; export enum NbOAuth2ResponseType { CODE = 'code', @@ -24,8 +25,7 @@ export enum NbOAuth2ClientAuthMethod { REQUEST_BODY = 'request-body', } -export class NbOAuth2AuthStrategyOptions { - name: string; +export class NbOAuth2AuthStrategyOptions extends NbAuthStrategyOptions { baseEndpoint?: string = ''; clientId: string = ''; clientSecret?: string = ''; @@ -40,6 +40,7 @@ export class NbOAuth2AuthStrategyOptions { endpoint?: string; redirectUri?: string; responseType?: string; + requireValidToken?: boolean; // used only with NbOAuth2ResponseType.TOKEN scope?: string; state?: string; params?: { [key: string]: string }; @@ -51,16 +52,19 @@ export class NbOAuth2AuthStrategyOptions { endpoint?: string; grantType?: string; redirectUri?: string; + requireValidToken?: boolean; class: NbAuthTokenClass, } = { endpoint: 'token', grantType: NbOAuth2GrantType.AUTHORIZATION_CODE, + requireValidToken: false, class: NbAuthOAuth2Token, }; refresh?: { endpoint?: string; grantType?: string; scope?: string; + requireValidToken?: boolean; } = { endpoint: 'token', grantType: NbOAuth2GrantType.REFRESH_TOKEN, diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts index 579b6e57b1..fff6dabd2a 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts @@ -12,6 +12,7 @@ import { NB_WINDOW } from '@nebular/theme'; import { NbAuthStrategy } from '../auth-strategy'; import { + NbAuthIllegalTokenError, NbAuthRefreshableToken, NbAuthResult, NbAuthToken, @@ -58,6 +59,7 @@ import { NbAuthStrategyClass } from '../../auth.options'; * endpoint?: string; * redirectUri?: string; * responseType?: string; + * requireValidToken: false, * scope?: string; * state?: string; * params?: { [key: string]: string }; @@ -68,6 +70,7 @@ import { NbAuthStrategyClass } from '../../auth.options'; * token?: { * endpoint?: string; * grantType?: string; + * requireValidToken: false, * redirectUri?: string; * class: NbAuthTokenClass, * } = { @@ -79,6 +82,7 @@ import { NbAuthStrategyClass } from '../../auth.options'; * endpoint?: string; * grantType?: string; * scope?: string; + * requireValidToken: false, * } = { * endpoint: 'token', * grantType: NbOAuth2GrantType.REFRESH_TOKEN, @@ -122,6 +126,8 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { ); }, [NbOAuth2ResponseType.TOKEN]: () => { + const module = 'authorize'; + const requireValidToken = this.getOption(`${module}.requireValidToken`); return observableOf(this.route.snapshot.fragment).pipe( map(fragment => this.parseHashAsQueryParams(fragment)), map((params: any) => { @@ -132,9 +138,8 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { this.getOption('redirect.success'), [], this.getOption('defaultMessages'), - this.createToken(params)); + this.createToken(params, requireValidToken)); } - return new NbAuthResult( false, params, @@ -143,6 +148,21 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { [], ); }), + catchError(err => { + const errors = []; + if (err instanceof NbAuthIllegalTokenError) { + errors.push(err.message) + } else { + errors.push('Something went wrong.'); + } + return observableOf( + new NbAuthResult( + false, + err, + this.getOption('redirect.failure'), + errors, + )); + }), ); }, }; @@ -198,7 +218,9 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { } refreshToken(token: NbAuthRefreshableToken): Observable { - const url = this.getActionEndpoint('refresh'); + const module = 'refresh'; + const url = this.getActionEndpoint(module); + const requireValidToken = this.getOption(`${module}.requireValidToken`); return this.http.post(url, this.buildRefreshRequestData(token), this.buildAuthHeader()) .pipe( @@ -209,14 +231,16 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { this.getOption('redirect.success'), [], this.getOption('defaultMessages'), - this.createRefreshedToken(res, token)); + this.createRefreshedToken(res, token, requireValidToken)); }), catchError((res) => this.handleResponseError(res)), ); } passwordToken(email: string, password: string): Observable { - const url = this.getActionEndpoint('token'); + const module = 'token'; + const url = this.getActionEndpoint(module); + const requireValidToken = this.getOption(`${module}.requireValidToken`); return this.http.post(url, this.buildPasswordRequestData(email, password), this.buildAuthHeader() ) .pipe( @@ -227,7 +251,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { this.getOption('redirect.success'), [], this.getOption('defaultMessages'), - this.createToken(res)); + this.createToken(res, requireValidToken)); }), catchError((res) => this.handleResponseError(res)), ); @@ -242,7 +266,10 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { } protected requestToken(code: string) { - const url = this.getActionEndpoint('token'); + + const module = 'token'; + const url = this.getActionEndpoint(module); + const requireValidToken = this.getOption(`${module}.requireValidToken`); return this.http.post(url, this.buildCodeRequestData(code), this.buildAuthHeader()) @@ -254,7 +281,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { this.getOption('redirect.success'), [], this.getOption('defaultMessages'), - this.createToken(res)); + this.createToken(res, requireValidToken)); }), catchError((res) => this.handleResponseError(res)), ); @@ -335,9 +362,12 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { } else { errors = this.getOption('defaultErrors'); } + } else if (res instanceof NbAuthIllegalTokenError ) { + errors.push(res.message) } else { - errors.push('Something went wrong.'); - } + errors.push('Something went wrong.') + }; + return observableOf( new NbAuthResult( false, @@ -376,10 +406,10 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { }, {}) : {}; } - protected createRefreshedToken(res, existingToken: NbAuthRefreshableToken): NbAuthToken { + protected createRefreshedToken(res, existingToken: NbAuthRefreshableToken, requireValidToken: boolean): NbAuthToken { type AuthRefreshToken = NbAuthRefreshableToken & NbAuthToken; - const refreshedToken: AuthRefreshToken = this.createToken(res); + const refreshedToken: AuthRefreshToken = this.createToken(res, requireValidToken); if (!refreshedToken.getRefreshToken() && existingToken.getRefreshToken()) { refreshedToken.setRefreshToken(existingToken.getRefreshToken()); } diff --git a/src/framework/auth/strategies/password/password-strategy-options.ts b/src/framework/auth/strategies/password/password-strategy-options.ts index 45c97b9eba..663b6a0ad8 100644 --- a/src/framework/auth/strategies/password/password-strategy-options.ts +++ b/src/framework/auth/strategies/password/password-strategy-options.ts @@ -18,7 +18,7 @@ export interface NbPasswordStrategyModule { success?: string | null; failure?: string | null; }; - failWhenNoToken?: boolean, + requireValidToken?: boolean; defaultErrors?: string[]; defaultMessages?: string[]; } @@ -39,13 +39,12 @@ export interface NbPasswordStrategyMessage { } export class NbPasswordAuthStrategyOptions extends NbAuthStrategyOptions { - name: string; baseEndpoint? = '/api/auth/'; login?: boolean | NbPasswordStrategyModule = { alwaysFail: false, endpoint: 'login', method: 'post', - failWhenNoToken: true, + requireValidToken: false, redirect: { success: '/', failure: null, @@ -58,7 +57,7 @@ export class NbPasswordAuthStrategyOptions extends NbAuthStrategyOptions { rememberMe: true, endpoint: 'register', method: 'post', - failWhenNoToken: true, + requireValidToken: false, redirect: { success: '/', failure: null, @@ -101,7 +100,7 @@ export class NbPasswordAuthStrategyOptions extends NbAuthStrategyOptions { refreshToken?: boolean | NbPasswordStrategyModule = { endpoint: 'refresh-token', method: 'post', - failWhenNoToken: true, + requireValidToken: false, redirect: { success: null, failure: null, diff --git a/src/framework/auth/strategies/password/password-strategy.spec.ts b/src/framework/auth/strategies/password/password-strategy.spec.ts index 7499bcec6a..5eb6b7ba4f 100644 --- a/src/framework/auth/strategies/password/password-strategy.spec.ts +++ b/src/framework/auth/strategies/password/password-strategy.spec.ts @@ -1431,23 +1431,29 @@ describe('password-auth-strategy', () => { }); - describe('custom failWhenNoToken', () => { + describe('custom requireValidToken', () => { - it('authenticate fail as no token', (done: DoneFn) => { + it('authenticate fail as no token when requireValidToken is set', (done: DoneFn) => { + strategy.setOptions({ + name: ownerStrategyName, + login: { + requireValidToken: true, + }, + }); strategy.authenticate(loginData) .subscribe((result: NbAuthResult) => { expect(result).toBeTruthy(); expect(result.isFailure()).toBe(true); expect(result.isSuccess()).toBe(false); expect(result.getMessages()).toEqual([]); - expect(result.getErrors()[0]).toEqual('Something went wrong.'); - expect(result.getResponse()).toEqual(new Error('Could not extract token from the response.')); - + expect(result.getErrors()[0]).toEqual('Token is empty or invalid.'); done(); }); httpMock.expectOne('/api/auth/login') - .flush({}); + .flush({data: { + message: 'Successfully logged in!', + }}); }); it('authenticate does not fail even when no token', (done: DoneFn) => { @@ -1455,7 +1461,7 @@ describe('password-auth-strategy', () => { strategy.setOptions({ name: ownerStrategyName, login: { - failWhenNoToken: false, + requireValidToken: false, }, }); @@ -1474,15 +1480,20 @@ describe('password-auth-strategy', () => { .flush({}); }); - it('register fail as no token', (done: DoneFn) => { + it('register fail as no token and requireValidtoken is set', (done: DoneFn) => { + strategy.setOptions({ + name: ownerStrategyName, + register: { + requireValidToken: true, + }, + }); strategy.register(loginData) .subscribe((result: NbAuthResult) => { expect(result).toBeTruthy(); expect(result.isFailure()).toBe(true); expect(result.isSuccess()).toBe(false); expect(result.getMessages()).toEqual([]); - expect(result.getErrors()[0]).toEqual('Something went wrong.'); - expect(result.getResponse()).toEqual(new Error('Could not extract token from the response.')); + expect(result.getErrors()[0]).toEqual('Token is empty or invalid.'); done(); }); @@ -1496,7 +1507,7 @@ describe('password-auth-strategy', () => { strategy.setOptions({ name: ownerStrategyName, register: { - failWhenNoToken: false, + requireValidToken: false, }, }); @@ -1515,15 +1526,20 @@ describe('password-auth-strategy', () => { .flush({}); }); - it('refreshToken fail as no token', (done: DoneFn) => { + it('refreshToken fail as no token and requireValidToken is set', (done: DoneFn) => { + strategy.setOptions({ + name: ownerStrategyName, + refreshToken: { + requireValidToken: true, + }, + }); strategy.refreshToken(loginData) .subscribe((result: NbAuthResult) => { expect(result).toBeTruthy(); expect(result.isFailure()).toBe(true); expect(result.isSuccess()).toBe(false); expect(result.getMessages()).toEqual([]); - expect(result.getErrors()[0]).toEqual('Something went wrong.'); - expect(result.getResponse()).toEqual(new Error('Could not extract token from the response.')); + expect(result.getErrors()[0]).toEqual('Token is empty or invalid.'); done(); }); @@ -1537,7 +1553,7 @@ describe('password-auth-strategy', () => { strategy.setOptions({ name: ownerStrategyName, refreshToken: { - failWhenNoToken: false, + requireValidToken: false, }, }); diff --git a/src/framework/auth/strategies/password/password-strategy.ts b/src/framework/auth/strategies/password/password-strategy.ts index 63bc2626c6..9f2755d55b 100644 --- a/src/framework/auth/strategies/password/password-strategy.ts +++ b/src/framework/auth/strategies/password/password-strategy.ts @@ -13,6 +13,7 @@ import { NbAuthResult } from '../../services/auth-result'; import { NbAuthStrategy } from '../auth-strategy'; import { NbAuthStrategyClass } from '../../auth.options'; import { NbPasswordAuthStrategyOptions, passwordStrategyOptions } from './password-strategy-options'; +import { NbAuthIllegalTokenError } from '../../services/token/token'; /** * The most common authentication provider for email/password strategy. @@ -30,7 +31,7 @@ import { NbPasswordAuthStrategyOptions, passwordStrategyOptions } from './passwo * alwaysFail: false, * endpoint: 'login', * method: 'post', - * failWhenNoToken: true, + * requireValidToken: false, * redirect: { * success: '/', * failure: null, @@ -43,7 +44,7 @@ import { NbPasswordAuthStrategyOptions, passwordStrategyOptions } from './passwo * rememberMe: true, * endpoint: 'register', * method: 'post', - * failWhenNoToken: true, + * requireValidToken: false, * redirect: { * success: '/', * failure: null, @@ -86,7 +87,7 @@ import { NbPasswordAuthStrategyOptions, passwordStrategyOptions } from './passwo * refreshToken?: boolean | NbPasswordStrategyModule = { * endpoint: 'refresh-token', * method: 'post', - * failWhenNoToken: true, + * requireValidToken: false, * redirect: { * success: null, * failure: null, @@ -153,94 +154,70 @@ export class NbPasswordAuthStrategy extends NbAuthStrategy { } authenticate(data?: any): Observable { - const method = this.getOption('login.method'); - const url = this.getActionEndpoint('login'); + const module = 'login'; + const method = this.getOption(`${module}.method`); + const url = this.getActionEndpoint(module); + const requireValidToken = this.getOption(`${module}.requireValidToken`); return this.http.request(method, url, {body: data, observe: 'response'}) .pipe( map((res) => { - if (this.getOption('login.alwaysFail')) { + if (this.getOption(`${module}.alwaysFail`)) { throw this.createFailResponse(data); } - return res; }), - this.validateToken('login'), map((res) => { return new NbAuthResult( true, res, - this.getOption('login.redirect.success'), + this.getOption(`${module}.redirect.success`), [], - this.getOption('messages.getter')('login', res, this.options), - this.createToken(this.getOption('token.getter')('login', res, this.options))); + this.getOption('messages.getter')(module, res, this.options), + this.createToken(this.getOption('token.getter')(module, res, this.options), requireValidToken)); }), catchError((res) => { - let errors = []; - if (res instanceof HttpErrorResponse) { - errors = this.getOption('errors.getter')('login', res, this.options); - } else { - errors.push('Something went wrong.'); - } - - return observableOf( - new NbAuthResult( - false, - res, - this.getOption('login.redirect.failure'), - errors, - )); + return this.handleResponseError(res, module); }), ); } register(data?: any): Observable { - const method = this.getOption('register.method'); - const url = this.getActionEndpoint('register'); + const module = 'register'; + const method = this.getOption(`${module}.method`); + const url = this.getActionEndpoint(module); + const requireValidToken = this.getOption(`${module}.requireValidToken`); return this.http.request(method, url, {body: data, observe: 'response'}) .pipe( map((res) => { - if (this.getOption('register.alwaysFail')) { + if (this.getOption(`${module}.alwaysFail`)) { throw this.createFailResponse(data); } return res; }), - this.validateToken('register'), map((res) => { return new NbAuthResult( true, res, - this.getOption('register.redirect.success'), + this.getOption(`${module}.redirect.success`), [], - this.getOption('messages.getter')('register', res, this.options), - this.createToken(this.getOption('token.getter')('login', res, this.options))); + this.getOption('messages.getter')(module, res, this.options), + this.createToken(this.getOption('token.getter')('login', res, this.options), requireValidToken)); }), catchError((res) => { - let errors = []; - if (res instanceof HttpErrorResponse) { - errors = this.getOption('errors.getter')('register', res, this.options); - } else { - errors.push('Something went wrong.'); - } - - return observableOf( - new NbAuthResult( - false, - res, - this.getOption('register.redirect.failure'), - errors, - )); + return this.handleResponseError(res, module); }), ); } requestPassword(data?: any): Observable { - const method = this.getOption('requestPass.method'); - const url = this.getActionEndpoint('requestPass'); + const module = 'requestPass'; + const method = this.getOption(`${module}.method`); + const url = this.getActionEndpoint(module); return this.http.request(method, url, {body: data, observe: 'response'}) .pipe( map((res) => { - if (this.getOption('requestPass.alwaysFail')) { + if (this.getOption(`${module}.alwaysFail`)) { throw this.createFailResponse(); } @@ -250,39 +227,27 @@ export class NbPasswordAuthStrategy extends NbAuthStrategy { return new NbAuthResult( true, res, - this.getOption('requestPass.redirect.success'), + this.getOption(`${module}.redirect.success`), [], - this.getOption('messages.getter')('requestPass', res, this.options)); + this.getOption('messages.getter')(module, res, this.options)); }), catchError((res) => { - let errors = []; - if (res instanceof HttpErrorResponse) { - errors = this.getOption('errors.getter')('requestPass', res, this.options); - } else { - errors.push('Something went wrong.'); - } - - return observableOf( - new NbAuthResult( - false, - res, - this.getOption('requestPass.redirect.failure'), - errors, - )); + return this.handleResponseError(res, module); }), ); } resetPassword(data: any = {}): Observable { - const tokenKey = this.getOption('resetPass.resetPasswordTokenKey'); - data[tokenKey] = this.route.snapshot.queryParams[tokenKey]; - const method = this.getOption('resetPass.method'); - const url = this.getActionEndpoint('resetPass'); + const module = 'resetPass'; + const method = this.getOption(`${module}.method`); + const url = this.getActionEndpoint(module); + const tokenKey = this.getOption(`${module}.resetPasswordTokenKey`); + data[tokenKey] = this.route.snapshot.queryParams[tokenKey]; return this.http.request(method, url, {body: data, observe: 'response'}) .pipe( map((res) => { - if (this.getOption('resetPass.alwaysFail')) { + if (this.getOption(`${module}.alwaysFail`)) { throw this.createFailResponse(); } @@ -292,33 +257,21 @@ export class NbPasswordAuthStrategy extends NbAuthStrategy { return new NbAuthResult( true, res, - this.getOption('resetPass.redirect.success'), + this.getOption(`${module}.redirect.success`), [], - this.getOption('messages.getter')('resetPass', res, this.options)); + this.getOption('messages.getter')(module, res, this.options)); }), catchError((res) => { - let errors = []; - if (res instanceof HttpErrorResponse) { - errors = this.getOption('errors.getter')('resetPass', res, this.options); - } else { - errors.push('Something went wrong.'); - } - - return observableOf( - new NbAuthResult( - false, - res, - this.getOption('resetPass.redirect.failure'), - errors, - )); + return this.handleResponseError(res, module); }), ); } logout(): Observable { - const method = this.getOption('logout.method'); - const url = this.getActionEndpoint('logout'); + const module = 'logout'; + const method = this.getOption(`${module}.method`); + const url = this.getActionEndpoint(module); return observableOf({}) .pipe( @@ -329,7 +282,7 @@ export class NbPasswordAuthStrategy extends NbAuthStrategy { return this.http.request(method, url, {observe: 'response'}); }), map((res) => { - if (this.getOption('logout.alwaysFail')) { + if (this.getOption(`${module}.alwaysFail`)) { throw this.createFailResponse(); } @@ -339,84 +292,63 @@ export class NbPasswordAuthStrategy extends NbAuthStrategy { return new NbAuthResult( true, res, - this.getOption('logout.redirect.success'), + this.getOption(`${module}.redirect.success`), [], - this.getOption('messages.getter')('logout', res, this.options)); + this.getOption('messages.getter')(module, res, this.options)); }), catchError((res) => { - let errors = []; - if (res instanceof HttpErrorResponse) { - errors = this.getOption('errors.getter')('logout', res, this.options); - } else { - errors.push('Something went wrong.'); - } - - return observableOf( - new NbAuthResult( - false, - res, - this.getOption('logout.redirect.failure'), - errors, - )); + return this.handleResponseError(res, module); }), ); } refreshToken(data?: any): Observable { - const method = this.getOption('refreshToken.method'); - const url = this.getActionEndpoint('refreshToken'); + const module = 'refreshToken'; + const method = this.getOption(`${module}.method`); + const url = this.getActionEndpoint(module); + const requireValidToken = this.getOption(`${module}.requireValidToken`); return this.http.request(method, url, {body: data, observe: 'response'}) .pipe( map((res) => { - if (this.getOption('refreshToken.alwaysFail')) { + if (this.getOption(`${module}.alwaysFail`)) { throw this.createFailResponse(data); } return res; }), - this.validateToken('refreshToken'), map((res) => { return new NbAuthResult( true, res, - this.getOption('refreshToken.redirect.success'), + this.getOption(`${module}.redirect.success`), [], - this.getOption('messages.getter')('refreshToken', res, this.options), - this.createToken(this.getOption('token.getter')('login', res, this.options))); + this.getOption('messages.getter')(module, res, this.options), + this.createToken(this.getOption('token.getter')(module, res, this.options), requireValidToken)); }), catchError((res) => { - let errors = []; - if (res instanceof HttpErrorResponse) { - errors = this.getOption('errors.getter')('refreshToken', res, this.options); - } else { - errors.push('Something went wrong.'); - } - - return observableOf( - new NbAuthResult( - false, - res, - this.getOption('refreshToken.redirect.failure'), - errors, - )); + return this.handleResponseError(res, module); }), ); } - protected validateToken(module: string): any { - return map((res) => { - const token = this.getOption('token.getter')(module, res, this.options); - if (!token && this.getOption(`${module}.failWhenNoToken`)) { - const key = this.getOption('token.key'); - console.warn(`NbPasswordAuthStrategy: - Token is not provided under '${key}' key - with getter '${this.getOption('token.getter')}', check your auth configuration.`); - - throw new Error('Could not extract token from the response.'); - } - return res; - }); + protected handleResponseError(res: any, module: string): Observable { + let errors = []; + if (res instanceof HttpErrorResponse) { + errors = this.getOption('errors.getter')(module, res, this.options); + } else if (res instanceof NbAuthIllegalTokenError) { + errors.push(res.message) + } else { + errors.push('Something went wrong.'); + } + return observableOf( + new NbAuthResult( + false, + res, + this.getOption(`${module}.redirect.failure`), + errors, + )); } + } diff --git a/src/playground/auth/auth.module.ts b/src/playground/auth/auth.module.ts index d222a7cd1b..84ce008cea 100644 --- a/src/playground/auth/auth.module.ts +++ b/src/playground/auth/auth.module.ts @@ -74,10 +74,12 @@ import { NbAuthGuard } from './auth-guard.service'; NbPasswordAuthStrategy.setup({ name: 'email', - token: { class: NbAuthJWTToken, }, + login: { + requireValidToken: false, + }, baseEndpoint: 'http://localhost:4400/api/auth/', logout: { redirect: { diff --git a/src/playground/oauth2-password/oauth2-password-login.component.ts b/src/playground/oauth2-password/oauth2-password-login.component.ts index 993e546ba4..6c0100cb7b 100644 --- a/src/playground/oauth2-password/oauth2-password-login.component.ts +++ b/src/playground/oauth2-password/oauth2-password-login.component.ts @@ -6,12 +6,11 @@ import { Component, Inject } from '@angular/core'; import { - NbAuthOAuth2Token, NbAuthResult, NbAuthService, NB_AUTH_OPTIONS, nbAuthCreateToken, - NbAuthJWTToken, + NbAuthJWTToken, NbAuthToken, } from '@nebular/auth'; import { Router } from '@angular/router'; import { getDeepFromObject } from '../../framework/auth/helpers'; @@ -22,7 +21,7 @@ import { getDeepFromObject } from '../../framework/auth/helpers'; -

{{getClaims(token.getValue()).email | json}} is currently authenticated

+

You are currently authenticated

Current User Access Token: {{ token.getValue() | json }}

Current User Access Token Payload : {{getClaims(token.getValue()) | json}}

@@ -83,7 +82,7 @@ import { getDeepFromObject } from '../../framework/auth/helpers'; }) export class NbOAuth2PasswordLoginComponent { - token: NbAuthOAuth2Token; + token: NbAuthToken; redirectDelay: number = 0; showMessages: any = {}; strategy: string = ''; @@ -98,22 +97,23 @@ export class NbOAuth2PasswordLoginComponent { this.redirectDelay = this.getConfigValue('forms.login.redirectDelay'); this.showMessages = this.getConfigValue('forms.login.showMessages'); this.strategy = this.getConfigValue('forms.login.strategy'); - this.authService.onTokenChange() + /**this.authService.onTokenChange() .subscribe((token: NbAuthOAuth2Token) => { this.token = null; if (token && token.isValid()) { this.token = token; } - }); + }); **/ } - login(): void { + login(): void { this.errors = this.messages = []; this.submitted = true; this.authService.authenticate(this.strategy, this.user).subscribe((result: NbAuthResult) => { this.submitted = false; + this.token = result.getToken(); if (result.isSuccess()) { this.messages = result.getMessages(); } else { @@ -133,6 +133,7 @@ export class NbOAuth2PasswordLoginComponent { logout() { this.authService.logout('password') .subscribe((authResult: NbAuthResult) => { + this.token = null; }); } @@ -141,6 +142,9 @@ export class NbOAuth2PasswordLoginComponent { } getClaims(rawToken: string): string { + if (!rawToken) { + return null; + } return nbAuthCreateToken(NbAuthJWTToken, rawToken, this.strategy).getPayload(); } } diff --git a/src/playground/oauth2-password/oauth2-password.module.ts b/src/playground/oauth2-password/oauth2-password.module.ts index c650f75d98..bdbd2dc69e 100644 --- a/src/playground/oauth2-password/oauth2-password.module.ts +++ b/src/playground/oauth2-password/oauth2-password.module.ts @@ -16,13 +16,11 @@ import { } from '@nebular/theme'; import { - NbAuthModule, + NbAuthModule, NbAuthOAuth2JWTToken, NbOAuth2AuthStrategy, NbOAuth2ClientAuthMethod, NbOAuth2GrantType, } from '@nebular/auth'; import { NbOAuth2PasswordLoginComponent } from './oauth2-password-login.component'; -import { NbAuthOAuth2Token } from '@nebular/auth'; - @NgModule({ @@ -59,7 +57,8 @@ import { NbAuthOAuth2Token } from '@nebular/auth'; token: { endpoint: 'token', grantType: NbOAuth2GrantType.PASSWORD, - class: NbAuthOAuth2Token, + class: NbAuthOAuth2JWTToken, + requireValidToken: true, }, redirect: { success: '/oauth2-password',