From dffbd594b3deffa55e016a3b460588ca7704cfdc Mon Sep 17 00:00:00 2001 From: Alain CHARLES <40032406+alain-charles@users.noreply.github.com> Date: Tue, 7 Aug 2018 14:50:48 +0200 Subject: [PATCH] feat(auth): use existing refreshToken if it is not repeated by the backend refresh endpoint (#593) --- src/framework/auth/auth.options.ts | 4 +- src/framework/auth/services/auth.service.ts | 2 +- src/framework/auth/services/token/token.ts | 15 +++- .../auth/strategies/auth-strategy.ts | 4 +- .../strategies/oauth2/oauth2-strategy.spec.ts | 70 +++++++++++++++++++ .../auth/strategies/oauth2/oauth2-strategy.ts | 18 ++++- 6 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/framework/auth/auth.options.ts b/src/framework/auth/auth.options.ts index 3d661ab965..630466f94a 100644 --- a/src/framework/auth/auth.options.ts +++ b/src/framework/auth/auth.options.ts @@ -1,6 +1,6 @@ import { InjectionToken } from '@angular/core'; import { NbAuthStrategy, NbAuthStrategyOptions } from './strategies'; -import { NbAuthTokenClass } from './services'; +import { NbAuthToken, NbAuthTokenClass } from './services/token/token'; export type NbAuthStrategyClass = new (...params: any[]) => NbAuthStrategy; @@ -87,5 +87,5 @@ export const defaultAuthOptions: any = { export const NB_AUTH_OPTIONS = new InjectionToken('Nebular Auth Options'); export const NB_AUTH_USER_OPTIONS = new InjectionToken('Nebular User Auth Options'); export const NB_AUTH_STRATEGIES = new InjectionToken('Nebular Auth Strategies'); -export const NB_AUTH_TOKENS = new InjectionToken('Nebular Auth Tokens'); +export const NB_AUTH_TOKENS = new InjectionToken[]>('Nebular Auth Tokens'); export const NB_AUTH_INTERCEPTOR_HEADER = new InjectionToken('Nebular Simple Interceptor Header'); diff --git a/src/framework/auth/services/auth.service.ts b/src/framework/auth/services/auth.service.ts index 3e19fe892b..c65958507c 100644 --- a/src/framework/auth/services/auth.service.ts +++ b/src/framework/auth/services/auth.service.ts @@ -162,7 +162,7 @@ export class NbAuthService { * @returns {Observable} */ refreshToken(strategyName: string, data?: any): Observable { - return this.getStrategy(strategyName).refreshToken() + return this.getStrategy(strategyName).refreshToken(data) .pipe( switchMap((result: NbAuthResult) => { return this.processResultToken(result); diff --git a/src/framework/auth/services/token/token.ts b/src/framework/auth/services/token/token.ts index 94431826c6..8c8904247f 100644 --- a/src/framework/auth/services/token/token.ts +++ b/src/framework/auth/services/token/token.ts @@ -16,14 +16,15 @@ export abstract class NbAuthToken { export interface NbAuthRefreshableToken { getRefreshToken(): string; + setRefreshToken(refreshToken: string); } -export interface NbAuthTokenClass { +export interface NbAuthTokenClass { NAME: string; - new (raw: any, ownerStrategyName: string, createdAt: Date): NbAuthToken; + new (raw: any, strategyName: string, expDate?: Date): T; } -export function nbAuthCreateToken(tokenClass: NbAuthTokenClass, +export function nbAuthCreateToken(tokenClass: NbAuthTokenClass, token: any, ownerStrategyName: string, createdAt?: Date) { @@ -206,6 +207,14 @@ export class NbAuthOAuth2Token extends NbAuthSimpleToken { return this.token.refresh_token; } + /** + * put refreshToken in the token payload + * @param refreshToken + */ + setRefreshToken(refreshToken: string) { + this.token.refresh_token = refreshToken; + } + /** * Returns token payload * @returns any diff --git a/src/framework/auth/strategies/auth-strategy.ts b/src/framework/auth/strategies/auth-strategy.ts index d662fb6bf5..8890d1b021 100644 --- a/src/framework/auth/strategies/auth-strategy.ts +++ b/src/framework/auth/strategies/auth-strategy.ts @@ -20,8 +20,8 @@ export abstract class NbAuthStrategy { return getDeepFromObject(this.options, key, null); } - createToken(value: any): NbAuthToken { - return nbAuthCreateToken(this.getOption('token.class'), value, this.getName()); + createToken(value: any): T { + return nbAuthCreateToken(this.getOption('token.class'), value, this.getName()); } getName(): string { diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts index a48186ad07..d5537c981e 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts @@ -37,6 +37,26 @@ describe('oauth2-auth-strategy', () => { example_parameter: 'example_value', }; + const tokenWithoutRefreshTokenResponse = { + access_token: '8uoloUIg765fHGF9jknjksdn9', + expires_in: 3600, + example_parameter: 'example_refresh_value', + } + + const refreshedTokenPayload = { + access_token: '8uoloUIg765fHGF9jknjksdn9', + expires_in: 3600, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + example_parameter: 'example_refresh_value', + } + + const refreshedTokenResponse = { + access_token: '8uoloUIg765fHGF9jknjksdn9', + expires_in: 3600, + refresh_token: 'dfsjkgkdh989JHJHJDSHJns', + example_parameter: 'example_refresh_value', + } + const tokenErrorResponse = { error: 'unauthorized_client', error_description: 'unauthorized', @@ -44,6 +64,9 @@ describe('oauth2-auth-strategy', () => { }; const successToken = nbAuthCreateToken(NbAuthOAuth2Token, tokenSuccessResponse, 'strategy') as NbAuthOAuth2Token; + // tslint:disable-next-line + const refreshedToken = nbAuthCreateToken(NbAuthOAuth2Token, refreshedTokenPayload, 'strategy') as NbAuthOAuth2Token; + const refreshedTokenWithRefreshToken = nbAuthCreateToken(NbAuthOAuth2Token, refreshedTokenResponse, 'strategy') as NbAuthOAuth2Token; beforeEach(() => { @@ -241,6 +264,53 @@ describe('oauth2-auth-strategy', () => { ).flush(tokenSuccessResponse); }); + it('handle refresh token and inserts existing refresh_token if needed', (done: DoneFn) => { + strategy.setOptions(basicOptions); + strategy.refreshToken(successToken) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(refreshedToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(refreshedToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/token' + && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN + && req.body['refresh_token'] === successToken.getRefreshToken() + && !req.body['scope'], + ).flush(tokenWithoutRefreshTokenResponse); + }); + + it('Handle refresh-token and leaves refresh_token unchanged if present', (done: DoneFn) => { + strategy.setOptions(basicOptions); + strategy.refreshToken(successToken) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(refreshedTokenWithRefreshToken.getValue()); + expect(result.getToken().getOwnerStrategyName()). + toEqual(refreshedTokenWithRefreshToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/token' + && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN + && req.body['refresh_token'] === successToken.getRefreshToken() + && !req.body['scope'], + ).flush(refreshedTokenResponse); + }); + it('handle error token refresh response', (done: DoneFn) => { strategy.refreshToken(successToken) diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts index 51916c40e4..579b6e57b1 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts @@ -11,7 +11,11 @@ import { switchMap, map, catchError } from 'rxjs/operators'; import { NB_WINDOW } from '@nebular/theme'; import { NbAuthStrategy } from '../auth-strategy'; -import { NbAuthRefreshableToken, NbAuthResult } from '../../services/'; +import { + NbAuthRefreshableToken, + NbAuthResult, + NbAuthToken, +} from '../../services/'; import { NbOAuth2AuthStrategyOptions, NbOAuth2ResponseType, @@ -205,7 +209,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { this.getOption('redirect.success'), [], this.getOption('defaultMessages'), - this.createToken(res)); + this.createRefreshedToken(res, token)); }), catchError((res) => this.handleResponseError(res)), ); @@ -372,6 +376,16 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { }, {}) : {}; } + protected createRefreshedToken(res, existingToken: NbAuthRefreshableToken): NbAuthToken { + type AuthRefreshToken = NbAuthRefreshableToken & NbAuthToken; + + const refreshedToken: AuthRefreshToken = this.createToken(res); + if (!refreshedToken.getRefreshToken() && existingToken.getRefreshToken()) { + refreshedToken.setRefreshToken(existingToken.getRefreshToken()); + } + return refreshedToken; + } + register(data?: any): Observable { throw new Error('`register` is not supported by `NbOAuth2AuthStrategy`, use `authenticate`.'); }