From ddaa14e9e33fd07677bbbd86993b9c550aaaf452 Mon Sep 17 00:00:00 2001 From: Bassam Date: Wed, 1 Nov 2017 18:31:51 -0700 Subject: [PATCH] fix(auth): fixes various Auth bugs. Fixes verifyAssertion unrecoverable errors when returnIdpCredential is set to true. In this case, the error code is returned along with the credential in the errorMessage without any ID token/refresh token. Catch, suppress/handle when localStorage is null or when `localStorage.getItem` throws a security error due to access being disabled by the browser for whatever reason. --- packages/auth/src/authstorage.js | 17 ++- packages/auth/src/rpchandler.js | 34 ++++++ packages/auth/src/storage/indexeddb.js | 6 +- packages/auth/src/storage/localstorage.js | 15 ++- packages/auth/src/storage/sessionstorage.js | 15 ++- packages/auth/test/authstorage_test.js | 72 +++++++++-- packages/auth/test/rpchandler_test.js | 129 ++++++++++++++++++++ 7 files changed, 268 insertions(+), 20 deletions(-) diff --git a/packages/auth/src/authstorage.js b/packages/auth/src/authstorage.js index a0d95871fc3..405b7b8e276 100644 --- a/packages/auth/src/authstorage.js +++ b/packages/auth/src/authstorage.js @@ -137,13 +137,16 @@ fireauth.authStorage.Key; * some mobile browsers. A localStorage change in the foreground window * will not be detected in the background window via the storage event. * This was detected in iOS 7.x mobile browsers. + * @param {boolean} webStorageSupported Whether browser web storage is + * supported. * @constructor @struct @final */ fireauth.authStorage.Manager = function( namespace, separator, safariLocalStorageNotSynced, - runsInBackground) { + runsInBackground, + webStorageSupported) { /** @const @private {string} Storage namespace. */ this.namespace_ = namespace; /** @const @private {string} Storage namespace key separator. */ @@ -159,6 +162,8 @@ fireauth.authStorage.Manager = function( * mobile browsers. */ this.runsInBackground_ = runsInBackground; + /** @const @private {boolean} Whether browser web storage is supported. */ + this.webStorageSupported_ = webStorageSupported; /** * @const @private {!Object.>} The storage event @@ -223,7 +228,8 @@ fireauth.authStorage.Manager.getInstance = function() { fireauth.authStorage.NAMESPACE_, fireauth.authStorage.SEPARATOR_, fireauth.util.isSafariLocalStorageNotSynced(), - fireauth.util.runsInBackground()); + fireauth.util.runsInBackground(), + fireauth.util.isWebStorageSupported()); } return fireauth.authStorage.Manager.instance_; }; @@ -337,8 +343,7 @@ fireauth.authStorage.Manager.prototype.addListener = function(dataKey, id, listener) { var key = this.getKeyName_(dataKey, id); // Initialize local map for current key if web storage is supported. - if (typeof goog.global['localStorage'] !== 'undefined' && - typeof goog.global['localStorage']['getItem'] === 'function') { + if (this.webStorageSupported_) { this.localMap_[key] = goog.global['localStorage']['getItem'](key); } if (goog.object.isEmpty(this.listeners_)) { @@ -401,7 +406,9 @@ fireauth.authStorage.Manager.prototype.startListeners_ = function() { if (!this.runsInBackground_ && // Add an exception for IE11 and Edge browsers, we should stick to // indexedDB in that case. - !fireauth.util.isLocalStorageNotSynchronized()) { + !fireauth.util.isLocalStorageNotSynchronized() && + // Confirm browser web storage is supported as polling relies on it. + this.webStorageSupported_) { this.startManualListeners_(); } }; diff --git a/packages/auth/src/rpchandler.js b/packages/auth/src/rpchandler.js index e064c4f0438..94d23a78ed7 100644 --- a/packages/auth/src/rpchandler.js +++ b/packages/auth/src/rpchandler.js @@ -1499,6 +1499,11 @@ fireauth.RpcHandler.validateVerifyAssertionForExistingResponse_ = fireauth.RpcHandler.ServerError.USER_NOT_FOUND) { // This corresponds to user-not-found. throw new fireauth.AuthError(fireauth.authenum.Error.USER_DELETED); + } else if (response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE]) { + // Construct developer facing error message from server code in errorMessage + // field. + throw fireauth.RpcHandler.getDeveloperErrorFromCode_( + response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE]); } // Need confirmation should not be returned when do not create new user flag // is set. @@ -1543,6 +1548,11 @@ fireauth.RpcHandler.validateVerifyAssertionResponse_ = function(response) { // owner of the account and then link to the returned credential here. response['code'] = fireauth.authenum.Error.EMAIL_EXISTS; error = fireauth.AuthErrorWithCredential.fromPlainObject(response); + } else if (response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE]) { + // Construct developer facing error message from server code in errorMessage + // field. + error = fireauth.RpcHandler.getDeveloperErrorFromCode_( + response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE]); } // If error found, throw it. if (error) { @@ -1979,6 +1989,30 @@ fireauth.RpcHandler.hasError_ = function(resp) { }; +/** + * Returns the developer facing error corresponding to the server code provided. + * @param {string} serverErrorCode The server error message. + * @return {!fireauth.AuthError} The corresponding error object. + * @private + */ +fireauth.RpcHandler.getDeveloperErrorFromCode_ = function(serverErrorCode) { + // Encapsulate the server error code in a typical server error response with + // the code populated within. This will convert the response to a developer + // facing one. + return fireauth.RpcHandler.getDeveloperError_({ + 'error': { + 'errors': [ + { + 'message': serverErrorCode + } + ], + 'code': 400, + 'message': serverErrorCode + } + }); +}; + + /** * Converts a server response with errors to a developer-facing AuthError. * @param {!Object} response The server response. diff --git a/packages/auth/src/storage/indexeddb.js b/packages/auth/src/storage/indexeddb.js index cc3f008eabb..d86be3c6645 100644 --- a/packages/auth/src/storage/indexeddb.js +++ b/packages/auth/src/storage/indexeddb.js @@ -220,7 +220,11 @@ fireauth.storage.IndexedDB.prototype.initializeDbAndRun_ = * @return {boolean} Whether indexedDB is available or not. */ fireauth.storage.IndexedDB.isAvailable = function() { - return !!window.indexedDB; + try { + return !!goog.global['indexedDB']; + } catch (e) { + return false; + } }; diff --git a/packages/auth/src/storage/localstorage.js b/packages/auth/src/storage/localstorage.js index bee8103d15b..61bf55cea12 100644 --- a/packages/auth/src/storage/localstorage.js +++ b/packages/auth/src/storage/localstorage.js @@ -55,7 +55,20 @@ fireauth.storage.LocalStorage = function() { /** @return {?Storage|undefined} The global localStorage instance. */ fireauth.storage.LocalStorage.getGlobalStorage = function() { - return goog.global['localStorage']; + try { + var storage = goog.global['localStorage']; + // Try editing web storage. If an error is thrown, it may be disabled. + var key = fireauth.util.generateEventId(); + if (storage) { + storage['setItem'](key, '1'); + storage['removeItem'](key); + } + return storage; + } catch (e) { + // In some cases, browsers with web storage disabled throw an error simply + // on access. + return null; + } }; diff --git a/packages/auth/src/storage/sessionstorage.js b/packages/auth/src/storage/sessionstorage.js index c1e02225e92..c566e55c5ae 100644 --- a/packages/auth/src/storage/sessionstorage.js +++ b/packages/auth/src/storage/sessionstorage.js @@ -54,7 +54,20 @@ fireauth.storage.SessionStorage = function() { /** @return {?Storage|undefined} The global sessionStorage instance. */ fireauth.storage.SessionStorage.getGlobalStorage = function() { - return goog.global['sessionStorage']; + try { + var storage = goog.global['sessionStorage']; + // Try editing web storage. If an error is thrown, it may be disabled. + var key = fireauth.util.generateEventId(); + if (storage) { + storage['setItem'](key, '1'); + storage['removeItem'](key); + } + return storage; + } catch (e) { + // In some cases, browsers with web storage disabled throw an error simply + // on access. + return null; + } }; diff --git a/packages/auth/test/authstorage_test.js b/packages/auth/test/authstorage_test.js index 78f52e0e360..683a6f1ae84 100644 --- a/packages/auth/test/authstorage_test.js +++ b/packages/auth/test/authstorage_test.js @@ -77,7 +77,7 @@ function tearDown() { * synchronized manager instance used for testing. */ function getDefaultManagerInstance() { - return new fireauth.authStorage.Manager('firebase', ':', false, true); + return new fireauth.authStorage.Manager('firebase', ':', false, true, true); } @@ -404,7 +404,8 @@ function testGetSet_persistentStorage_noId() { function testAddRemoveListeners_localStorage() { - var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var manager = + new fireauth.authStorage.Manager('name', ':', false, true, true); var listener1 = goog.testing.recordFunction(); var listener2 = goog.testing.recordFunction(); var listener3 = goog.testing.recordFunction(); @@ -470,7 +471,8 @@ function testAddRemoveListeners_localStorage() { function testAddRemoveListeners_localStorage_nullKey() { - var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var manager = + new fireauth.authStorage.Manager('name', ':', false, true, true); var listener1 = goog.testing.recordFunction(); var listener2 = goog.testing.recordFunction(); var listener3 = goog.testing.recordFunction(); @@ -522,7 +524,8 @@ function testAddRemoveListeners_localStorage_ie10() { function() { return true; }); - var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var manager = + new fireauth.authStorage.Manager('name', ':', false, true, true); var listener1 = goog.testing.recordFunction(); var listener2 = goog.testing.recordFunction(); var listener3 = goog.testing.recordFunction(); @@ -637,7 +640,8 @@ function testAddRemoveListeners_indexeddb() { function() { return mockIndexeddb; }); - var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var manager = + new fireauth.authStorage.Manager('name', ':', false, true, true); var listener1 = goog.testing.recordFunction(); var listener2 = goog.testing.recordFunction(); var listener3 = goog.testing.recordFunction(); @@ -714,7 +718,8 @@ function testAddRemoveListeners_indexeddb_cannotRunInBackground() { return mockIndexeddb; }); // Cannot run in the background. - var manager = new fireauth.authStorage.Manager('name', ':', false, false); + var manager = + new fireauth.authStorage.Manager('name', ':', false, false, true); var listener1 = goog.testing.recordFunction(); var listener2 = goog.testing.recordFunction(); var listener3 = goog.testing.recordFunction(); @@ -757,7 +762,7 @@ function testAddRemoveListeners_indexeddb_cannotRunInBackground() { function testSafariLocalStorageSync_newEvent() { var manager = - new fireauth.authStorage.Manager('firebase', ':', true, true); + new fireauth.authStorage.Manager('firebase', ':', true, true, true); // Simulate Safari bug. stubs.replace( fireauth.util, @@ -797,7 +802,7 @@ function testSafariLocalStorageSync_cannotRunInBackground() { // Realistically only storage event should trigger here. // Test when new data is added to storage. var manager = - new fireauth.authStorage.Manager('firebase', ':', true, false); + new fireauth.authStorage.Manager('firebase', ':', true, false, true); // Simulate Safari bug. stubs.replace( fireauth.util, @@ -837,7 +842,7 @@ function testSafariLocalStorageSync_deletedEvent() { // Realistically only storage event should trigger here. // Test when old data is deleted from storage. var manager = - new fireauth.authStorage.Manager('firebase', ':', true, true); + new fireauth.authStorage.Manager('firebase', ':', true, true, true); var key1 = {'name': 'authEvent', 'persistent': true}; // Simulate Safari bug. stubs.replace( @@ -879,7 +884,7 @@ function testRunsInBackground_storageEventMode() { var key = {name: 'authEvent', persistent: 'local'}; var storageKey = 'firebase:authEvent:appId1'; var manager = new fireauth.authStorage.Manager( - 'firebase', ':', false, false); + 'firebase', ':', false, false, true); var listener1 = goog.testing.recordFunction(); var expectedEvent = { type: 'signInViaPopup', @@ -923,6 +928,49 @@ function testRunsInBackground_storageEventMode() { } +function testRunsInBackground_webStorageNotSupported() { + // Test when browser does not run in the background and web storage is not + // supported. Polling should not be turned on. + var key = {name: 'authEvent', persistent: 'local'}; + var storageKey = 'firebase:authEvent:appId1'; + // Simulate manager doesn't support web storage and can't run in the + // background. Normally when a browser can't run in the background, polling is + // enabled. + var manager = new fireauth.authStorage.Manager( + 'firebase', ':', false, false, false); + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + + // Add listener. + manager.addListener(key, appId, listener1); + // Test that polling function is not set by updating localStorage with some + // data. This should not happen realistically when web storage is disabled. + window.localStorage.setItem(storageKey, JSON.stringify(expectedEvent)); + // Run clock. + clock.tick(1000); + // Listener should not trigger. + assertEquals(0, listener1.getCallCount()); + // Clear storage. + window.localStorage.clear(); + // Run clock. + clock.tick(1000); + // Listener should not trigger. + assertEquals(0, listener1.getCallCount()); + // Save Auth event and confirm listener not triggered. + // This normally simulates polling. + window.localStorage.setItem(storageKey, JSON.stringify(expectedEvent)); + // Run clock. + clock.tick(1000); + // Listener should not trigger. + assertEquals(0, listener1.getCallCount()); +} + + function testRunsInBackground_pollingMode() { // Test when browser does not run in the background while another tab is in // foreground. @@ -932,7 +980,7 @@ function testRunsInBackground_pollingMode() { var key = {name: 'authEvent', persistent: 'local'}; var storageKey = 'firebase:authEvent:appId1'; var manager = new fireauth.authStorage.Manager( - 'firebase', ':', false, false); + 'firebase', ':', false, false, true); var listener1 = goog.testing.recordFunction(); var expectedEvent = { type: 'signInViaPopup', @@ -984,7 +1032,7 @@ function testRunsInBackground_currentTabChangesIgnored() { var key = {name: 'authEvent', persistent: 'local'}; var storageKey = 'firebase:authEvent:appId1'; var manager = new fireauth.authStorage.Manager( - 'firebase', ':', false, false); + 'firebase', ':', false, false, true); var listener1 = goog.testing.recordFunction(); var expectedEvent = { type: 'signInViaPopup', diff --git a/packages/auth/test/rpchandler_test.js b/packages/auth/test/rpchandler_test.js index e3a81ee35d9..1b8b61b6ae9 100644 --- a/packages/auth/test/rpchandler_test.js +++ b/packages/auth/test/rpchandler_test.js @@ -2435,6 +2435,48 @@ function testVerifyAssertion_success() { } +/** + * Tests verifyAssertion RPC call with no recovery errorMessage. + */ +function testVerifyAssertion_returnIdpCredential_noRecoveryError() { + // Simulate server response containing unrecoverable errorMessage. + var serverResponse = { + 'federatedId': 'FEDERATED_ID', + 'providerId': 'google.com', + 'email': 'user@example.com', + 'emailVerified': true, + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE', + 'errorMessage': 'USER_DISABLED' + }; + // Expected error thrown. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyAssertion({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + /** * Tests verifyAssertion RPC call with no sessionId passed. */ @@ -2489,6 +2531,50 @@ function testVerifyAssertionForLinking_success() { } +/** + * Tests verifyAssertion for linking RPC call with no recovery errorMessage. + */ +function testVerifyAssertionForLinking_returnIdpCredential_noRecoveryError() { + // Simulate server response containing unrecoverable errorMessage. + var serverResponse = { + 'federatedId': 'FEDERATED_ID', + 'providerId': 'google.com', + 'email': 'user@example.com', + 'emailVerified': true, + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE', + 'errorMessage': 'USER_DISABLED' + }; + // Expected error thrown. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyAssertionForLinking({ + 'idToken': 'ID_TOKEN', + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + /** * Tests verifyAssertion for linking RPC call with no idToken passed. */ @@ -2823,6 +2909,49 @@ function testVerifyAssertionForExisting_success() { } +/** + * Tests verifyAssertion for existing RPC call with no recovery errorMessage. + */ +function testVerifyAssertionForExisting_returnIdpCredential_noRecoveryError() { + // Simulate server response containing unrecoverable errorMessage. + var serverResponse = { + 'federatedId': 'FEDERATED_ID', + 'providerId': 'google.com', + 'email': 'user@example.com', + 'emailVerified': true, + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE', + 'errorMessage': 'USER_DISABLED' + }; + // Expected error thrown. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + 'autoCreate': false, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyAssertionForExisting({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + /** * Tests verifyAssertionForExisting RPC call with no sessionId passed. */