diff --git a/README.md b/README.md index c42361b63..f52daa04a 100644 --- a/README.md +++ b/README.md @@ -1673,11 +1673,15 @@ authClient.tokenManager.on('renewed', function (key, newToken, oldToken) { // Triggered when an OAuthError is returned via the API authClient.tokenManager.on('error', function (err) { - console.log('TokenManager error:', err.message); + console.log('TokenManager error:', err); // err.name // err.message // err.errorCode // err.errorSummary + + if (err.errorCode === 'login_required') { + // Return to unauthenticated state + } }); ``` diff --git a/lib/TokenManager.js b/lib/TokenManager.js index 80fc52067..60ea9b3c1 100644 --- a/lib/TokenManager.js +++ b/lib/TokenManager.js @@ -182,7 +182,7 @@ function renew(sdk, tokenMgmtRef, storage, key) { return freshToken; }) .catch(function(err) { - if (err.name === 'OAuthError') { + if (err.name === 'OAuthError' || err.name === 'AuthSdkError') { remove(tokenMgmtRef, storage, key); emitError(tokenMgmtRef, err); } diff --git a/test/app/src/testApp.js b/test/app/src/testApp.js index 973cac02c..c5d9e473a 100644 --- a/test/app/src/testApp.js +++ b/test/app/src/testApp.js @@ -49,6 +49,12 @@ function bindFunctions(testApp, window) { function TestApp(config) { this.config = config; this.oktaAuth = new OktaAuth(config); + this.oktaAuth.tokenManager.on('error', e => { + console.error('Token manager error:', e); + if (e.errorCode === 'login_required') { + this.render(); + } + }); } export default TestApp; diff --git a/test/karma/spec/renewToken.js b/test/karma/spec/renewToken.js index 6f59cd44b..51c2f1a2f 100644 --- a/test/karma/spec/renewToken.js +++ b/test/karma/spec/renewToken.js @@ -6,7 +6,8 @@ var tokens = require('../../util/tokens'); import OktaAuth from '@okta/okta-auth-js'; import oauthUtil from '../../../lib/oauthUtil'; import pkce from '../../../lib/pkce'; -import OauthError from '../../../lib/errors/OAuthError'; +import OAuthError from '../../../lib/errors/OAuthError'; +import AuthSdkError from '../../../lib/errors/AuthSdkError'; describe('Renew token', function() { @@ -79,6 +80,51 @@ describe('Renew token', function() { }); } + it('TokenManager::renew throws an exception if token does not exist', function() { + const expectedMessage = 'The tokenManager has no token for the key: accessToken'; + return bootstrap() + .then(() => { + return sdk.tokenManager.renew('accessToken'); + }) + .catch(e => { + expect(e instanceof AuthSdkError).toBe(true); + expect(e.message).toBe(expectedMessage); + }); + }); + + it('TokenManager::renew emits an error if token::renew returns an OAuthError', function() { + const errorCode = 'login_required'; + const errorMessage = 'The client specified not to prompt, but the user is not logged in.'; + const errorCallback = jasmine.createSpy(); + return bootstrap() + .then(() => { + sdk.tokenManager.on('error', errorCallback); + sdk.tokenManager.add('accessToken', ACCCESS_TOKEN_PARSED); + spyOn(oauthUtil, 'loadFrame').and.callFake(urlStr => { + const url = new URL(urlStr); + const state = url.searchParams.get('state'); + var response = { + state, + error: errorCode, + error_description: errorMessage, + name: 'OAuthError' + }; + + // Simulate window.postMessage() from iframe + var event = new Event('message'); + event.data = response; + event.origin = ISSUER; + window.dispatchEvent(event); + }); + return sdk.tokenManager.renew('accessToken'); + }) + .catch(e => { + expect(e instanceof OAuthError).toBe(true); + expect(e.message).toBe(errorMessage); + expect(errorCallback).toHaveBeenCalledWith(e); + }); + }); + it('receives/throws error from iframe', function() { // This is the error if requesting scope='offline_access' const error = 'access_denied'; @@ -110,7 +156,7 @@ describe('Renew token', function() { }) .catch(e => { expect(oauthUtil.loadFrame).toHaveBeenCalled(); - expect(e instanceof OauthError).toBe(true); + expect(e instanceof OAuthError).toBe(true); expect(e.message).toBe(error_description); }); }); diff --git a/test/spec/tokenManager.js b/test/spec/tokenManager.js index 9ab08bfeb..351d00064 100644 --- a/test/spec/tokenManager.js +++ b/test/spec/tokenManager.js @@ -392,6 +392,38 @@ describe('TokenManager', function() { }); }); }); + + it('removes token if an AuthSdkError is thrown while renewing', function() { + return oauthUtil.setupFrame({ + authClient: setupSync(), + willFail: true, + tokenManagerAddKeys: { + 'test-accessToken': tokens.standardAccessTokenParsed, + 'test-idToken': tokens.standardIdTokenParsed + }, + tokenManagerRenewArgs: ['test-accessToken'], + postMessageSrc: { + baseUri: 'http://obviously.fake.foo', + }, + postMessageResp: { + state: oauthUtil.mockedState + } + }) + .fail(function(e) { + util.expectErrorToEqual(e, { + name: 'AuthSdkError', + message: 'The request does not match client configuration', + errorCode: 'INTERNAL', + errorSummary: 'The request does not match client configuration', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [], + }); + oauthUtil.expectTokenStorageToEqual(localStorage, { + 'test-idToken': tokens.standardIdTokenParsed + }); + }); + }); }); describe('autoRenew', function() { @@ -585,6 +617,103 @@ describe('TokenManager', function() { }); }); + it('Emits an "error" event on OAuth failure', function() { + var authClient = setupSync({ + tokenManager: { + autoRenew: true + } + }); + var errorEventCallback = jest.fn().mockImplementation(function(err) { + util.expectErrorToEqual(err, { + name: 'OAuthError', + message: 'something went wrong', + errorCode: 'sampleErrorCode', + errorSummary: 'something went wrong' + }); + }) + authClient.tokenManager.on('error', errorEventCallback); + + return oauthUtil.setupFrame({ + authClient: authClient, + autoRenew: true, + willFail: true, + fastForwardToTime: true, + autoRenewTokenKey: 'test-idToken', + time: tokens.standardIdTokenParsed.expiresAt + 1, + tokenManagerAddKeys: { + 'test-idToken': tokens.standardIdTokenParsed + }, + postMessageResp: { + error: 'sampleErrorCode', + 'error_description': 'something went wrong', + state: oauthUtil.mockedState + } + }) + .fail(function(err) { + util.expectErrorToEqual(err, { + name: 'OAuthError', + message: 'something went wrong', + errorCode: 'sampleErrorCode', + errorSummary: 'something went wrong' + }); + oauthUtil.expectTokenStorageToEqual(localStorage, {}); + + expect(errorEventCallback).toHaveBeenCalled(); + }); + }); + + it('Emits an "error" event on AuthSdkError', function() { + var authClient = setupSync({ + tokenManager: { + autoRenew: true + } + }); + var errorEventCallback = jest.fn().mockImplementation(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'The request does not match client configuration', + errorCode: 'INTERNAL', + errorSummary: 'The request does not match client configuration', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [], + }); + }) + authClient.tokenManager.on('error', errorEventCallback); + + return oauthUtil.setupFrame({ + authClient: authClient, + autoRenew: true, + willFail: true, + fastForwardToTime: true, + autoRenewTokenKey: 'test-idToken', + time: tokens.standardIdTokenParsed.expiresAt + 1, + tokenManagerAddKeys: { + 'test-idToken': tokens.standardIdTokenParsed + }, + postMessageSrc: { + baseUri: 'http://obviously.fake.foo', + }, + postMessageResp: { + state: oauthUtil.mockedState + } + }) + .fail(function(err) { + util.expectErrorToEqual(err, { + name: 'AuthSdkError', + message: 'The request does not match client configuration', + errorCode: 'INTERNAL', + errorSummary: 'The request does not match client configuration', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [], + }); + oauthUtil.expectTokenStorageToEqual(localStorage, {}); + + expect(errorEventCallback).toHaveBeenCalled(); + }); + }); + it('removes a token on OAuth failure', function() { return oauthUtil.setupFrame({ authClient: setupSync({ @@ -617,6 +746,42 @@ describe('TokenManager', function() { }); }); + it('removes a token on AuthSdkError', function() { + return oauthUtil.setupFrame({ + authClient: setupSync({ + tokenManager: { + autoRenew: true + } + }), + autoRenew: true, + willFail: true, + fastForwardToTime: true, + autoRenewTokenKey: 'test-idToken', + time: tokens.standardIdTokenParsed.expiresAt + 1, + tokenManagerAddKeys: { + 'test-idToken': tokens.standardIdTokenParsed + }, + postMessageSrc: { + baseUri: 'http://obviously.fake.foo', + }, + postMessageResp: { + state: oauthUtil.mockedState + } + }) + .fail(function(e) { + util.expectErrorToEqual(e, { + name: 'AuthSdkError', + message: 'The request does not match client configuration', + errorCode: 'INTERNAL', + errorSummary: 'The request does not match client configuration', + errorLink: 'INTERNAL', + errorId: 'INTERNAL', + errorCauses: [], + }); + oauthUtil.expectTokenStorageToEqual(localStorage, {}); + }); + }); + it('does not renew the token if the token has not expired', function() { var CURRENT_TIME = 0; var EXPIRATION_TIME = CURRENT_TIME + 10;