Skip to content

Commit

Permalink
feat(auth): use existing refreshToken if it is not repeated by the ba…
Browse files Browse the repository at this point in the history
…ckend refresh endpoint (#593)
  • Loading branch information
alain-charles authored and nnixaa committed Aug 7, 2018
1 parent 8547527 commit dffbd59
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 10 deletions.
4 changes: 2 additions & 2 deletions src/framework/auth/auth.options.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -87,5 +87,5 @@ export const defaultAuthOptions: any = {
export const NB_AUTH_OPTIONS = new InjectionToken<NbAuthOptions>('Nebular Auth Options');
export const NB_AUTH_USER_OPTIONS = new InjectionToken<NbAuthOptions>('Nebular User Auth Options');
export const NB_AUTH_STRATEGIES = new InjectionToken<NbAuthStrategies>('Nebular Auth Strategies');
export const NB_AUTH_TOKENS = new InjectionToken<NbAuthTokenClass[]>('Nebular Auth Tokens');
export const NB_AUTH_TOKENS = new InjectionToken<NbAuthTokenClass<NbAuthToken>[]>('Nebular Auth Tokens');
export const NB_AUTH_INTERCEPTOR_HEADER = new InjectionToken<NbAuthStrategies>('Nebular Simple Interceptor Header');
2 changes: 1 addition & 1 deletion src/framework/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export class NbAuthService {
* @returns {Observable<NbAuthResult>}
*/
refreshToken(strategyName: string, data?: any): Observable<NbAuthResult> {
return this.getStrategy(strategyName).refreshToken()
return this.getStrategy(strategyName).refreshToken(data)
.pipe(
switchMap((result: NbAuthResult) => {
return this.processResultToken(result);
Expand Down
15 changes: 12 additions & 3 deletions src/framework/auth/services/token/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ export abstract class NbAuthToken {

export interface NbAuthRefreshableToken {
getRefreshToken(): string;
setRefreshToken(refreshToken: string);
}

export interface NbAuthTokenClass {
export interface NbAuthTokenClass<T = NbAuthToken> {
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<T extends NbAuthToken>(tokenClass: NbAuthTokenClass<T>,
token: any,
ownerStrategyName: string,
createdAt?: Date) {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/framework/auth/strategies/auth-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends NbAuthToken>(value: any): T {
return nbAuthCreateToken<T>(this.getOption('token.class'), value, this.getName());
}

getName(): string {
Expand Down
70 changes: 70 additions & 0 deletions src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,36 @@ 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',
error_uri: 'some',
};

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(() => {
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 16 additions & 2 deletions src/framework/auth/strategies/oauth2/oauth2-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)),
);
Expand Down Expand Up @@ -372,6 +376,16 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
}, {}) : {};
}

protected createRefreshedToken(res, existingToken: NbAuthRefreshableToken): NbAuthToken {
type AuthRefreshToken = NbAuthRefreshableToken & NbAuthToken;

const refreshedToken: AuthRefreshToken = this.createToken<AuthRefreshToken>(res);
if (!refreshedToken.getRefreshToken() && existingToken.getRefreshToken()) {
refreshedToken.setRefreshToken(existingToken.getRefreshToken());
}
return refreshedToken;
}

register(data?: any): Observable<NbAuthResult> {
throw new Error('`register` is not supported by `NbOAuth2AuthStrategy`, use `authenticate`.');
}
Expand Down

0 comments on commit dffbd59

Please sign in to comment.