Skip to content

Commit

Permalink
clears refreshToken from storage when expired
Browse files Browse the repository at this point in the history
OKTA-495545
<<<Jenkins Check-In of Tested SHA: 20376d9 for [email protected]>>>
Artifact: okta-auth-js
Files changed count: 9
PR Link: #1222
  • Loading branch information
jaredperreault-okta authored and eng-prod-CI-bot-okta committed Jun 1, 2022
1 parent 23f1bfe commit 2a2825d
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- [#1225](https://github.com/okta/okta-auth-js/pull/1225) `oktaAuth.start`/`oktaAuth.stop` now return a `Promise`, ensures services have started/stopped before resolving

### Fixes

- [#1222](https://github.com/okta/okta-auth-js/pull/1222) Invalid (or expired) refresh tokens are now removed from storage when invalid token error occurs

## 6.5.3

- [#1224](https://github.com/okta/okta-auth-js/pull/1224) Fixes missing `relatesTo` type from `NextStep`
Expand Down
5 changes: 5 additions & 0 deletions lib/TokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,11 @@ export class TokenManager implements TokenManagerInterface {
this.storage.setStorage(tokenStorage);
}

removeRefreshToken () {
const key = this.getStorageKeyByType('refreshToken') || REFRESH_TOKEN_STORAGE_KEY;
this.remove(key);
}

addPendingRemoveFlags() {
const tokens = this.getTokensSync();
Object.keys(tokens).forEach(key => {
Expand Down
5 changes: 5 additions & 0 deletions lib/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ function isAuthApiError(obj: any): obj is AuthApiError {
return (obj instanceof AuthApiError);
}

function isOAuthError(obj: any): obj is OAuthError {
return (obj instanceof OAuthError);
}

export {
isAuthApiError,
isOAuthError,
AuthApiError,
AuthPollStopError,
AuthSdkError,
Expand Down
34 changes: 22 additions & 12 deletions lib/oidc/renewTokensWithRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { isSameRefreshToken } from './util/refreshToken';
import { OktaAuthOIDCInterface, TokenParams, RefreshToken, Tokens } from '../types';
import { handleOAuthResponse } from './handleOAuthResponse';
import { postRefreshToken } from './endpoints/token';
import { isRefreshTokenInvalidError } from './util/errors';

export async function renewTokensWithRefresh(
sdk: OktaAuthOIDCInterface,
Expand All @@ -27,18 +28,27 @@ export async function renewTokensWithRefresh(
throw new AuthSdkError('A clientId must be specified in the OktaAuth constructor to renew tokens');
}

const renewTokenParams: TokenParams = Object.assign({}, tokenParams, {
clientId,
});
const tokenResponse = await postRefreshToken(sdk, renewTokenParams, refreshTokenObject);
const urls = getOAuthUrls(sdk, tokenParams);
const { tokens } = await handleOAuthResponse(sdk, renewTokenParams, tokenResponse, urls);
try {
const renewTokenParams: TokenParams = Object.assign({}, tokenParams, {
clientId,
});
const tokenResponse = await postRefreshToken(sdk, renewTokenParams, refreshTokenObject);
const urls = getOAuthUrls(sdk, tokenParams);
const { tokens } = await handleOAuthResponse(sdk, renewTokenParams, tokenResponse, urls);

// Support rotating refresh tokens
const { refreshToken } = tokens;
if (refreshToken && !isSameRefreshToken(refreshToken, refreshTokenObject)) {
sdk.tokenManager.updateRefreshToken(refreshToken);
}
// Support rotating refresh tokens
const { refreshToken } = tokens;
if (refreshToken && !isSameRefreshToken(refreshToken, refreshTokenObject)) {
sdk.tokenManager.updateRefreshToken(refreshToken);
}

return tokens;
return tokens;
}
catch (err) {
if (isRefreshTokenInvalidError(err)) {
// if the refresh token is invalid, remove it from storage
sdk.tokenManager.removeRefreshToken();
}
throw err;
}
}
9 changes: 8 additions & 1 deletion lib/oidc/util/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


import { OktaAuthOptionsInterface } from '../../types';
import { OAuthError, AuthApiError } from '../../errors';
import { OAuthError, AuthApiError, isOAuthError } from '../../errors';

export function isInteractionRequiredError(error: Error) {
if (error.name !== 'OAuthError') {
Expand All @@ -32,3 +32,10 @@ export function isAuthorizationCodeError(sdk: OktaAuthOptionsInterface, error: E
const responseJSON = errorResponse?.responseJSON as Record<string, unknown>;
return sdk.options.pkce && (responseJSON?.error as string === 'invalid_grant');
}

export function isRefreshTokenInvalidError(error: unknown): boolean {
// error: {"error":"invalid_grant","error_description":"The refresh token is invalid or expired."}
return isOAuthError(error) &&
error.errorCode === 'invalid_grant' &&
error.errorSummary === 'The refresh token is invalid or expired.';
}
1 change: 1 addition & 0 deletions lib/types/TokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export interface TokenManagerInterface {
getStorageKeyByType(type: TokenType): string;
add(key: any, token: Token): void;
updateRefreshToken(token: RefreshToken);
removeRefreshToken(): void;
}
9 changes: 9 additions & 0 deletions test/spec/TokenManager/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,13 @@ describe('TokenManager', function() {
expect(client.tokenManager.remove).toHaveBeenNthCalledWith(2, 'accessToken');
});
});

describe('removeRefreshToken', () => {
it('clears refresh token', () => {
setupSync({}, false);
jest.spyOn(client.tokenManager, 'remove');
client.tokenManager.removeRefreshToken();
expect(client.tokenManager.remove).toHaveBeenNthCalledWith(1, 'refreshToken');
});
});
});
36 changes: 36 additions & 0 deletions test/spec/oidc/renewTokensWithRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import tokens from '@okta/test.support/tokens';
import * as tokenEndpoint from '../../../lib/oidc/endpoints/token';
import * as renewTokensWithRefreshTokenModule from '../../../lib/oidc/renewTokensWithRefresh';
import * as getWithoutPromptModule from '../../../lib/oidc/getWithoutPrompt';
import { OAuthError } from '../../../lib/errors';
import oauthUtil from '@okta/test.support/oauthUtil';
import util from '@okta/test.support/util';

Expand Down Expand Up @@ -143,4 +144,39 @@ describe('renewTokensWithRefresh', function () {
refreshToken = await authInstance.tokenManager.get('refreshToken2');
expect(refreshToken).toEqual(tokens.standardRefreshToken2Parsed);
});

describe('error handling', () => {
describe('refreshToken is invalid (or expired)', () => {
beforeEach(() => {
const refreshTokenExpiredError = new OAuthError('invalid_grant', 'The refresh token is invalid or expired.');
jest.spyOn(tokenEndpoint, 'postRefreshToken').mockRejectedValue(refreshTokenExpiredError);
testContext.refreshTokenExpiredError = refreshTokenExpiredError;
});

it('refreshToken is removed after token invalid error is returned', async () => {
const { authInstance, renewTokenSpy, refreshTokenExpiredError } = testContext;
jest.spyOn(authInstance.tokenManager, 'remove');

authInstance.tokenManager.add('refreshToken', tokens.standardRefreshTokenParsed);
await expect(authInstance.token.renewTokens()).rejects.toBe(refreshTokenExpiredError);
expect(renewTokenSpy).toHaveBeenCalled();
expect(authInstance.tokenManager.remove).toHaveBeenCalledWith('refreshToken');
await expect(await authInstance.tokenManager.get('refreshToken')).toBeUndefined();
});

it('refreshToken is NOT removed after non-token invalid error is returned', async () => {
const error = new Error('something happened');
jest.spyOn(tokenEndpoint, 'postRefreshToken').mockRejectedValue(error);

const { authInstance, renewTokenSpy } = testContext;
jest.spyOn(authInstance.tokenManager, 'remove');

authInstance.tokenManager.add('refreshToken', tokens.standardRefreshTokenParsed);
await expect(authInstance.token.renewTokens()).rejects.toBe(error);
expect(renewTokenSpy).toHaveBeenCalled();
expect(authInstance.tokenManager.remove).not.toHaveBeenCalledWith('refreshToken');
await expect(await authInstance.tokenManager.get('refreshToken')).toEqual(tokens.standardRefreshTokenParsed);
});
});
});
});
21 changes: 20 additions & 1 deletion test/spec/oidc/util/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


import { OAuthError } from '../../../../lib/errors';
import { isInteractionRequiredError } from '../../../../lib/oidc/util';
import { isInteractionRequiredError, isRefreshTokenInvalidError } from '../../../../lib/oidc/util';

describe('oidc/util/errors', () => {

Expand All @@ -32,4 +32,23 @@ describe('oidc/util/errors', () => {
expect(isInteractionRequiredError(error)).toBe(false);
});
});

describe('isRedirectTokenInvalidError', () => {
// error: {"error":"invalid_grant","error_description":"The refresh token is invalid or expired."}

it('returns true for OAuthError objects with expected fields', () => {
const error = new OAuthError('invalid_grant', 'The refresh token is invalid or expired.');
expect(isRefreshTokenInvalidError(error)).toBe(true);
});

it('returns false for OAuthError objects without expected fields', () => {
const error = new OAuthError('something', 'description not matter');
expect(isRefreshTokenInvalidError(error)).toBe(false);
});

it('returns false for non OAuthError objects', () => {
const error = new Error('something');
expect(isRefreshTokenInvalidError(error as unknown)).toBe(false);
});
});
});

0 comments on commit 2a2825d

Please sign in to comment.