diff --git a/.changeset/large-books-call.md b/.changeset/large-books-call.md new file mode 100644 index 00000000000..6e09825eb58 --- /dev/null +++ b/.changeset/large-books-call.md @@ -0,0 +1,5 @@ +--- +"@firebase/auth": patch +--- + +Update auth token logic to rely on device clock time instead of server time. This fixes an issue seen when a device's clock is skewed by a lot: https://github.com/firebase/firebase-js-sdk/issues/3222 diff --git a/packages/auth/src/authuser.js b/packages/auth/src/authuser.js index 37638bd763d..f81b5f1e2e2 100644 --- a/packages/auth/src/authuser.js +++ b/packages/auth/src/authuser.js @@ -508,7 +508,7 @@ fireauth.AuthUser.prototype.initializeProactiveRefreshUtility_ = function() { function() { // Get time until expiration minus the refresh offset. var waitInterval = - self.stsTokenManager_.getExpirationTime() - goog.now() - + self.stsTokenManager_.getExpirationTime() - Date.now() - fireauth.TokenRefreshTime.OFFSET_DURATION; // Set to zero if wait interval is negative. return waitInterval > 0 ? waitInterval : 0; @@ -2436,6 +2436,12 @@ fireauth.AuthUser.fromPlainObject = function(user) { // Refresh token could be expired. stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN] = user['stsTokenManager']['refreshToken'] || null; + const expirationTime = user['stsTokenManager']['expirationTime']; + if (expirationTime) { + stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField + .EXPIRES_IN] = + (expirationTime - Date.now()) / 1000; + } } else { // Token response is a required field. return null; diff --git a/packages/auth/src/idtoken.js b/packages/auth/src/idtoken.js index 830142e6fd9..27abe52c28a 100644 --- a/packages/auth/src/idtoken.js +++ b/packages/auth/src/idtoken.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,7 @@ goog.require('goog.crypt.base64'); * @constructor */ fireauth.IdToken = function(tokenString) { - var token = fireauth.IdToken.parseIdTokenClaims(tokenString); + const token = fireauth.IdToken.parseIdTokenClaims(tokenString); if (!(token && token['sub'] && token['iss'] && token['aud'] && token['exp'])) { throw new Error('Invalid JWT'); @@ -45,7 +45,7 @@ fireauth.IdToken = function(tokenString) { this.exp_ = token['exp']; /** @const @private {string} The local user ID of the token. */ this.localId_ = token['sub']; - var now = goog.now() / 1000; + const now = Date.now() / 1000; /** @const @private {number} The issue time in seconds of the token. */ this.iat_ = token['iat'] || (now > this.exp_ ? this.exp_ : now); /** @const @private {?string} The email address of the token. */ @@ -111,12 +111,24 @@ fireauth.IdToken.prototype.getEmail = function() { }; -/** @return {number} The expire time in seconds. */ +/** + * @deprecated Use client side clock to calculate when the token expires. + * @return {number} The expire time in seconds. + */ fireauth.IdToken.prototype.getExp = function() { return this.exp_; }; +/** + * @return {number} The difference in seconds between when the token was + * issued and when it expires. + */ +fireauth.IdToken.prototype.getExpiresIn = function() { + return this.exp_ - this.iat_; +}; + + /** @return {?string} The ID of the identity provider. */ fireauth.IdToken.prototype.getProviderId = function() { return this.providerId_; @@ -165,9 +177,12 @@ fireauth.IdToken.prototype.isVerified = function() { }; -/** @return {boolean} Whether token is expired. */ +/** + * @deprecated Use client side clock to calculate when the token expires. + * @return {boolean} Whether token is expired. + */ fireauth.IdToken.prototype.isExpired = function() { - var now = Math.floor(goog.now() / 1000); + const now = Math.floor(Date.now() / 1000); // It is expired if token expiration time is less than current time. return this.getExp() <= now; }; @@ -218,18 +233,18 @@ fireauth.IdToken.parseIdTokenClaims = function(tokenString) { return null; } // Token format is .. - var fields = tokenString.split('.'); + const fields = tokenString.split('.'); if (fields.length != 3) { return null; } - var jsonInfo = fields[1]; + let jsonInfo = fields[1]; // Google base64 library does not handle padding. - var padLen = (4 - jsonInfo.length % 4) % 4; - for (var i = 0; i < padLen; i++) { + const padLen = (4 - jsonInfo.length % 4) % 4; + for (let i = 0; i < padLen; i++) { jsonInfo += '.'; } try { - var token = JSON.parse(goog.crypt.base64.decodeString(jsonInfo, true)); + const token = JSON.parse(goog.crypt.base64.decodeString(jsonInfo, true)); return /** @type {?Object} */ (token); } catch (e) {} return null; diff --git a/packages/auth/src/token.js b/packages/auth/src/token.js index a634b0ef63e..9cfbb703d55 100644 --- a/packages/auth/src/token.js +++ b/packages/auth/src/token.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,8 @@ fireauth.StsTokenManager = function(rpcHandler) { this.refreshToken_ = null; /** @private {?fireauth.IdToken} The STS ID token. */ this.accessToken_ = null; + /** @private {number} The expiration time of the token in epoch millis. */ + this.expiresAt_ = Date.now(); }; @@ -73,13 +75,14 @@ fireauth.StsTokenManager.prototype.toPlainObject = function() { * plain object provided using the RPC handler provided. */ fireauth.StsTokenManager.fromPlainObject = function(rpcHandler, obj) { - var stsTokenManager = null; + let stsTokenManager = null; if (obj && obj['apiKey']) { // These should be always equals and must be enforced in internal use. goog.asserts.assert(obj['apiKey'] == rpcHandler.getApiKey()); stsTokenManager = new fireauth.StsTokenManager(rpcHandler); stsTokenManager.setRefreshToken(obj['refreshToken']); stsTokenManager.setAccessToken(obj['accessToken']); + stsTokenManager.setExpiresAt(obj['expirationTime']); } return stsTokenManager; }; @@ -118,6 +121,30 @@ fireauth.StsTokenManager.prototype.setAccessToken = function(accessToken) { this.accessToken_ = fireauth.IdToken.parse(accessToken || ''); }; +/** + * To account for client side clock skew, we try to set the expiration time + * using the local clock by adding the server TTL. If not provided, expiresAt + * will be set from the accessToken by taking the difference between the exp + * and iat fields. + * + * @param {number=} expiresIn The expiration TTL in seconds. + */ +fireauth.StsTokenManager.prototype.setExpiresIn = function(expiresIn) { + expiresIn = typeof expiresIn !== 'undefined' ? expiresIn : + this.accessToken_ ? this.accessToken_.getExpiresIn() : + 0; + this.expiresAt_ = Date.now() + expiresIn * 1000; +} + +/** + * Allow setting expiresAt directly when we know the time is already in the + * local clock. + * + * @param {number} expiresAt The expiration time in epoch millis. + */ +fireauth.StsTokenManager.prototype.setExpiresAt = function(expiresAt) { + this.expiresAt_ = expiresAt; +} /** * @return {?string} The refresh token. @@ -131,7 +158,7 @@ fireauth.StsTokenManager.prototype.getRefreshToken = function() { * @return {number} The STS access token expiration time in milliseconds. */ fireauth.StsTokenManager.prototype.getExpirationTime = function() { - return (this.accessToken_ && this.accessToken_.getExp() * 1000) || 0; + return this.expiresAt_; }; @@ -148,7 +175,7 @@ fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER = 30 * 1000; * @private */ fireauth.StsTokenManager.prototype.isExpired_ = function() { - return goog.now() > + return Date.now() > this.getExpirationTime() - fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER; }; @@ -161,11 +188,14 @@ fireauth.StsTokenManager.prototype.isExpired_ = function() { * @return {string} The STS access token. */ fireauth.StsTokenManager.prototype.parseServerResponse = function(response) { - var idToken = response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]; - var refreshToken = - response[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN]; + const idToken = response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]; this.setAccessToken(idToken); - this.setRefreshToken(refreshToken); + this.setRefreshToken( + response[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN]); + // Not all IDP server responses come with expiresIn, some like MFA omit it. + const expiresIn = response[fireauth.RpcHandler.AuthServerField.EXPIRES_IN]; + this.setExpiresIn( + typeof expiresIn !== 'undefined' ? Number(expiresIn) : undefined); return idToken; }; @@ -175,7 +205,7 @@ fireauth.StsTokenManager.prototype.parseServerResponse = function(response) { * @return {!Object} */ fireauth.StsTokenManager.prototype.toServerResponse = function() { - var stsTokenManagerResponse = {}; + const stsTokenManagerResponse = {}; stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.ID_TOKEN] = this.accessToken_ && this.accessToken_.toString(); // Refresh token could be expired. @@ -192,6 +222,7 @@ fireauth.StsTokenManager.prototype.toServerResponse = function() { fireauth.StsTokenManager.prototype.copy = function(tokenManagerToCopy) { this.accessToken_ = tokenManagerToCopy.accessToken_; this.refreshToken_ = tokenManagerToCopy.refreshToken_; + this.expiresAt_ = tokenManagerToCopy.expiresAt_; }; @@ -201,7 +232,7 @@ fireauth.StsTokenManager.prototype.copy = function(tokenManagerToCopy) { * @private */ fireauth.StsTokenManager.prototype.exchangeRefreshToken_ = function() { - var data = { + const data = { 'grant_type': 'refresh_token', 'refresh_token': this.refreshToken_ }; @@ -216,27 +247,32 @@ fireauth.StsTokenManager.prototype.exchangeRefreshToken_ = function() { * @private */ fireauth.StsTokenManager.prototype.requestToken_ = function(data) { - var self = this; // Send RPC request to STS token endpoint. - return this.rpcHandler_.requestStsToken(data).then(function(resp) { - var response = /** @type {!fireauth.StsTokenManager.ResponseData} */ (resp); - self.accessToken_ = fireauth.IdToken.parse( - response[fireauth.RpcHandler.StsServerField.ACCESS_TOKEN]); - self.refreshToken_ = - response[fireauth.RpcHandler.StsServerField.REFRESH_TOKEN]; - return /** @type {!fireauth.StsTokenManager.Response} */ ({ - 'accessToken': self.accessToken_.toString(), - 'refreshToken': self.refreshToken_ - }); - }).thenCatch(function(error) { - // Refresh token expired or user deleted. In this case, reset refresh token - // to prevent sending the request again to the STS server unless - // the token is manually updated, perhaps via successful reauthentication. - if (error['code'] == 'auth/user-token-expired') { - self.refreshToken_ = null; - } - throw error; - }); + return this.rpcHandler_.requestStsToken(data) + .then((resp) => { + const response = + /** @type {!fireauth.StsTokenManager.ResponseData} */ (resp); + this.accessToken_ = fireauth.IdToken.parse( + response[fireauth.RpcHandler.StsServerField.ACCESS_TOKEN]); + this.refreshToken_ = + response[fireauth.RpcHandler.StsServerField.REFRESH_TOKEN]; + this.setExpiresIn( + response[fireauth.RpcHandler.StsServerField.EXPIRES_IN]); + return /** @type {!fireauth.StsTokenManager.Response} */ ({ + 'accessToken': this.accessToken_.toString(), + 'refreshToken': this.refreshToken_ + }); + }) + .thenCatch((error) => { + // Refresh token expired or user deleted. In this case, reset refresh + // token to prevent sending the request again to the STS server unless + // the token is manually updated, perhaps via successful + // reauthentication. + if (error['code'] == 'auth/user-token-expired') { + this.refreshToken_ = null; + } + throw error; + }); }; @@ -251,12 +287,11 @@ fireauth.StsTokenManager.prototype.isRefreshTokenExpired = function() { * Otherwise the existing ID token or refresh token is exchanged for a new one. * If there is no user signed in, returns null. * - * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. + * @param {boolean=} forceRefresh Whether to force refresh token exchange. * @return {!goog.Promise} */ -fireauth.StsTokenManager.prototype.getToken = function(opt_forceRefresh) { - var self = this; - var forceRefresh = !!opt_forceRefresh; +fireauth.StsTokenManager.prototype.getToken = function(forceRefresh) { + forceRefresh = !!forceRefresh; // Refresh token is expired. if (this.isRefreshTokenExpired()) { return goog.Promise.reject( @@ -265,8 +300,8 @@ fireauth.StsTokenManager.prototype.getToken = function(opt_forceRefresh) { if (!forceRefresh && this.accessToken_ && !this.isExpired_()) { // Cached STS access token not expired, return it. return /** @type {!goog.Promise} */ (goog.Promise.resolve({ - 'accessToken': self.accessToken_.toString(), - 'refreshToken': self.refreshToken_ + 'accessToken': this.accessToken_.toString(), + 'refreshToken': this.refreshToken_ })); } else if (this.refreshToken_) { // Expired but refresh token available, exchange refresh token for STS diff --git a/packages/auth/test/auth_test.js b/packages/auth/test/auth_test.js index b5d4a24652b..701df7fcc3b 100644 --- a/packages/auth/test/auth_test.js +++ b/packages/auth/test/auth_test.js @@ -138,7 +138,7 @@ var expectedAdditionalUserInfo; var expectedGoogleCredential; var expectedSamlTokenResponseWithIdPData; var expectedSamlAdditionalUserInfo; -var now = goog.now(); +var now = Date.now(); var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); var mockControl; @@ -205,7 +205,7 @@ function setUp() { return goog.Promise.resolve(); }); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -244,15 +244,18 @@ function setUp() { }; expectedTokenResponse = { 'idToken': jwt1, - 'refreshToken': 'REFRESH_TOKEN' + 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600' }; expectedTokenResponse2 = { 'idToken': jwt2, - 'refreshToken': 'REFRESH_TOKEN2' + 'refreshToken': 'REFRESH_TOKEN2', + 'expiresIn': '3600' }; expectedTokenResponse3 = { 'idToken': jwt3, - 'refreshToken': 'REFRESH_TOKEN3' + 'refreshToken': 'REFRESH_TOKEN3', + 'expiresIn': '3600' }; expectedTokenResponse4 = { // Sample ID token with provider password and email user@example.com. @@ -275,11 +278,13 @@ function setUp() { 'sign_in_provider': 'password' } }), - 'refreshToken': 'REFRESH_TOKEN4' + 'refreshToken': 'REFRESH_TOKEN4', + 'expiresIn': '3600' }; expectedTokenResponseWithIdPData = { 'idToken': jwt1, 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600', // Credential returned. 'providerId': 'google.com', 'oauthAccessToken': 'googleAccessToken', @@ -306,6 +311,7 @@ function setUp() { expectedSamlTokenResponseWithIdPData = { 'idToken': jwt1, 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600', 'providerId': 'saml.provider', // Additional user info data. 'rawUserInfo': '{"kind":"plus#person","displayName":"John Doe","na' + @@ -388,6 +394,7 @@ function setUp() { token = new fireauth.StsTokenManager(rpcHandler); token.setRefreshToken('refreshToken'); token.setAccessToken(jwt1); + token.setExpiresIn(3600); ignoreArgument = goog.testing.mockmatchers.ignoreArgument; mockControl = new goog.testing.MockControl(); mockControl.$resetAll(); @@ -576,7 +583,7 @@ function testToJson_withUser() { return goog.Promise.resolve(); }); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -2510,7 +2517,7 @@ function testAuth_initState_signedInStatus() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -2612,7 +2619,7 @@ function testAuth_initState_signedInStatus_withEmulator() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function () { return now; @@ -2735,7 +2742,7 @@ function testAuth_initState_signedInStatus_differentAuthDomain() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -2799,7 +2806,7 @@ function testAuth_initState_signedInStatus_withRedirectUser() { // Assume origin is a valid one. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -2929,7 +2936,7 @@ function testAuth_initState_signedInStatus_withRedirectUser_sameEventId() { // Assume origin is a valid one. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3059,7 +3066,7 @@ function testAuth_initState_signedInStatus_deletedUser() { var expectedError = new fireauth.AuthError( fireauth.authenum.Error.TOKEN_EXPIRED); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3131,7 +3138,7 @@ function testAuth_initState_signedInStatus_offline() { var expectedError = new fireauth.AuthError( fireauth.authenum.Error.NETWORK_REQUEST_FAILED); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3221,7 +3228,7 @@ function testAuth_initState_signedOutStatus() { }); }); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3276,7 +3283,7 @@ function testAuth_syncAuthChanges_sameUser() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3411,7 +3418,7 @@ function testAuth_syncAuthChanges_newSignIn() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3537,7 +3544,7 @@ function testAuth_syncAuthChanges_newSignIn_differentAuthDomain() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -3605,7 +3612,7 @@ function testAuth_syncAuthChanges_newSignOut() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -4122,7 +4129,7 @@ function testAuth_signInWithIdTokenResponse_newUser() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -4211,7 +4218,7 @@ function testAuth_signInWithIdTokenResponse_sameUser() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -4344,7 +4351,7 @@ function testAuth_signInWithIdTokenResponse_newUserDifferentFromCurrent() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -4463,7 +4470,7 @@ function testAuth_signInWithIdTokenResponse_newUserDifferentFromCurrent() { // first. stubs.reset(); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -4497,7 +4504,7 @@ function testAuth_signInWithIdTokenResponse_withEmulator() { // Simulate current origin is whitelisted. simulateWhitelistedOrigin(); stubs.replace( - goog, + Date, 'now', function () { return now; @@ -9283,7 +9290,7 @@ function testAuth_redirectedLoggedOutUser_differentAuthDomain() { simulateWhitelistedOrigin(); initializeMockStorage(); stubs.replace( - goog, + Date, 'now', function() { return now; diff --git a/packages/auth/test/authuser_test.js b/packages/auth/test/authuser_test.js index a47a0234d05..1d62921368e 100644 --- a/packages/auth/test/authuser_test.js +++ b/packages/auth/test/authuser_test.js @@ -144,7 +144,7 @@ var expectedTokenResponseWithIdPData; var expectedAdditionalUserInfo; var expectedGoogleCredential; var expectedReauthenticateTokenResponse; -var now = goog.now(); +var now = Date.now(); var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); @@ -232,7 +232,7 @@ function setUp() { return false; }); stubs.replace( - goog, + Date, 'now', function() { return now; @@ -333,7 +333,8 @@ function setUp() { token.setAccessToken(jwt); tokenResponse = { 'idToken': jwt, - 'refreshToken': 'refreshToken' + 'refreshToken': 'refreshToken', + 'expiresIn': '4800' }; // accountInfo in the format of a getAccountInfo response. @@ -4618,7 +4619,7 @@ function testUser_toPlainObject() { 'apiKey': 'apiKey1', 'refreshToken': 'refreshToken', 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'redirectEventId': '5678', 'lastLoginAt': lastLoginAt, @@ -4676,7 +4677,7 @@ function testUser_toPlainObject_noMetadata() { 'apiKey': 'apiKey1', 'refreshToken': 'refreshToken', 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'redirectEventId': '5678', 'tenantId': null, @@ -4732,7 +4733,7 @@ function testUser_toPlainObject_enrolledFactors() { 'apiKey': 'apiKey1', 'refreshToken': 'refreshToken', 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'redirectEventId': '5678', 'tenantId': null, @@ -4812,7 +4813,7 @@ function testUser_fromPlainObject() { 'apiKey': 'apiKey1', 'refreshToken': 'refreshToken', 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'redirectEventId': '5678', 'lastLoginAt': lastLoginAt, @@ -4881,7 +4882,7 @@ function testUser_fromPlainObject_noMetadata() { 'apiKey': 'apiKey1', 'refreshToken': 'refreshToken', 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'redirectEventId': '5678', 'tenantId': null @@ -4936,7 +4937,7 @@ function testUser_fromPlainObject_tokenExpired() { // Expired refresh token. 'refreshToken': null, 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'lastLoginAt': lastLoginAt, 'createdAt': createdAt @@ -5011,7 +5012,7 @@ function testUser_fromPlainObject_enrolledFactors() { 'apiKey': 'apiKey1', 'refreshToken': 'refreshToken', 'accessToken': jwt, - 'expirationTime': now + 3600 * 1000 + 'expirationTime': now + 4800 * 1000 }, 'redirectEventId': '5678', 'lastLoginAt': lastLoginAt, @@ -12741,7 +12742,7 @@ function testUser_proactiveRefresh_startAndStop() { fireauth.authenum.Error.USER_DISABLED))); // Confirm getWaitDuration returns expected value. assertEquals( - 3600 * 1000 - fireauth.TokenRefreshTime.OFFSET_DURATION, + 4800 * 1000 - fireauth.TokenRefreshTime.OFFSET_DURATION, getWaitDuration()); return proactiveRefreshInstance; }).$once(); diff --git a/packages/auth/test/idtoken_test.js b/packages/auth/test/idtoken_test.js index 70d21f96efc..ffc14a14de9 100644 --- a/packages/auth/test/idtoken_test.js +++ b/packages/auth/test/idtoken_test.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,23 @@ goog.provide('fireauth.IdTokenTest'); goog.require('fireauth.IdToken'); +goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.jsunit'); goog.setTestOnly('fireauth.IdTokenTest'); +const stubs = new goog.testing.PropertyReplacer(); +const now = Date.now(); + +function setUp() { + stubs.replace(Date, 'now', function() { + return now; + }); +} + +function tearDown() { + stubs.reset(); +} // exp: 1326439044 // sub: "679" @@ -166,6 +179,7 @@ var tokenMultiTenant = 'HEAD.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5j' + * @param {!fireauth.IdToken} token The ID token to assert. * @param {?string} email The expected email. * @param {number} exp The expected expiration field. + * @param {number} iat The token issuance time field. * @param {?string} providerId The expected provider ID. * @param {?string} displayName The expected display name. * @param {?string} photoURL The expected photo URL. @@ -180,6 +194,7 @@ function assertToken( token, email, exp, + iat, providerId, displayName, photoURL, @@ -190,7 +205,8 @@ function assertToken( phoneNumber, tenantId) { assertEquals(email, token.getEmail()); - assertEquals(exp, token.getExp()); + assertEquals(exp, token.getExp()) + assertEquals(exp - iat, token.getExpiresIn());; assertEquals(providerId, token.getProviderId()); assertEquals(displayName, token.getDisplayName()); assertEquals(photoURL, token.getPhotoUrl()); @@ -209,11 +225,12 @@ function testParse_invalid() { function testParse_anonymous() { - var token = fireauth.IdToken.parse(tokenAnonymous); + const token = fireauth.IdToken.parse(tokenAnonymous); assertToken( token, null, 1326446190, + 1326446190, null, null, null, @@ -228,11 +245,12 @@ function testParse_anonymous() { function testParse_tenantId() { - var token = fireauth.IdToken.parse(tokenMultiTenant); + const token = fireauth.IdToken.parse(tokenMultiTenant); assertToken( token, 'testuser@gmail.com', 1522780575, + 1522776807, 'password', null, null, @@ -246,11 +264,12 @@ function testParse_tenantId() { function testParse_needPadding() { - var token = fireauth.IdToken.parse(tokenGmail); + const token = fireauth.IdToken.parse(tokenGmail); assertToken( token, 'test123456@gmail.com', 1326439044, + 1326439044, 'gmail.com', null, null, @@ -266,11 +285,12 @@ function testParse_needPadding() { function testParse_noPadding() { - var token = fireauth.IdToken.parse(tokenYahoo); + const token = fireauth.IdToken.parse(tokenYahoo); assertToken( token, 'user123@yahoo.com', 1326446190, + 1326446190, 'yahoo.com', null, null, @@ -287,11 +307,12 @@ function testParse_noPadding() { function testParse_unexpired() { // This token will expire in year 2047. - var token = fireauth.IdToken.parse(tokenGoogleWithFederatedId); + const token = fireauth.IdToken.parse(tokenGoogleWithFederatedId); assertToken( token, 'testuser@gmail.com', 2442455688, + 1441246088, 'google.com', 'John Doe', 'https://lh5.googleusercontent.com/1458474/photo.jpg', @@ -309,11 +330,12 @@ function testParse_unexpired() { function testParse_phoneAndFirebaseProviderId() { - var token = fireauth.IdToken.parse(tokenPhone); + const token = fireauth.IdToken.parse(tokenPhone); assertToken( token, 'user@example.com', 1506053883, + 1506050283, 'phone', null, null, @@ -339,7 +361,7 @@ function testParseIdTokenClaims_null() { function testParseIdTokenClaims() { - var tokenJSON = fireauth.IdToken.parseIdTokenClaims( + const tokenJSON = fireauth.IdToken.parseIdTokenClaims( tokenGoogleWithFederatedId); assertObjectEquals( { @@ -359,7 +381,7 @@ function testParseIdTokenClaims() { function testParseIdTokenClaims_customClaims() { - var tokenJSON = fireauth.IdToken.parseIdTokenClaims(tokenCustomClaim); + const tokenJSON = fireauth.IdToken.parseIdTokenClaims(tokenCustomClaim); assertObjectEquals( { 'iss': 'https://securetoken.google.com/projectId', diff --git a/packages/auth/test/storageusermanager_test.js b/packages/auth/test/storageusermanager_test.js index f0601cd2437..4b52c049c99 100644 --- a/packages/auth/test/storageusermanager_test.js +++ b/packages/auth/test/storageusermanager_test.js @@ -31,7 +31,6 @@ goog.require('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.events'); goog.require('goog.events.EventType'); -goog.require('goog.testing.MockClock'); goog.require('goog.testing.PropertyReplacer'); goog.require('goog.testing.events'); goog.require('goog.testing.events.Event'); @@ -45,7 +44,6 @@ var config = { apiKey: 'apiKey1' }; var appId = 'appId1'; -var clock; var expectedUser; var expectedUserWithAuthDomain; var stubs = new goog.testing.PropertyReplacer(); @@ -53,7 +51,8 @@ var testUser; var testUser2; var mockLocalStorage; var mockSessionStorage; -var now = new Date(); +const now = Date.now(); +const nowDate = new Date(now); function setUp() { @@ -70,7 +69,9 @@ function setUp() { function() { return false; }); - clock = new goog.testing.MockClock(true); + stubs.replace(Date, 'now', function() { + return now; + }); window.localStorage.clear(); window.sessionStorage.clear(); var config = { @@ -88,14 +89,14 @@ function setUp() { { 'uid': 'ENROLLMENT_UID1', 'displayName': 'Work phone number', - 'enrollmentTime': now.toUTCString(), + 'enrollmentTime': nowDate.toUTCString(), 'factorId': fireauth.constants.SecondFactorType.PHONE, 'phoneNumber': '+16505551234' }, { 'uid': 'ENROLLMENT_UID2', 'displayName': 'Spouse phone number', - 'enrollmentTime': now.toUTCString(), + 'enrollmentTime': nowDate.toUTCString(), 'factorId': fireauth.constants.SecondFactorType.PHONE, 'phoneNumber': '+16505556789' } @@ -111,7 +112,8 @@ function setUp() { }; var tokenResponse = { 'idToken': fireauth.common.testHelper.createMockJwt(), - 'refreshToken': 'refreshToken' + 'refreshToken': 'refreshToken', + 'expiresIn': '3600' }; testUser = new fireauth.AuthUser(config, tokenResponse, accountInfo); testUser2 = new fireauth.AuthUser(config, tokenResponse, accountInfo2); @@ -131,7 +133,6 @@ function tearDown() { if (testUser2) { testUser2.destroy(); } - goog.dispose(clock); } @@ -169,14 +170,14 @@ function testGetSetRemoveCurrentUser() { { 'uid': 'ENROLLMENT_UID1', 'displayName': 'Work phone number', - 'enrollmentTime': now.toUTCString(), + 'enrollmentTime': nowDate.toUTCString(), 'factorId': fireauth.constants.SecondFactorType.PHONE, 'phoneNumber': '+16505551234' }, { 'uid': 'ENROLLMENT_UID2', 'displayName': 'Spouse phone number', - 'enrollmentTime': now.toUTCString(), + 'enrollmentTime': nowDate.toUTCString(), 'factorId': fireauth.constants.SecondFactorType.PHONE, 'phoneNumber': '+16505556789' } @@ -185,7 +186,8 @@ function testGetSetRemoveCurrentUser() { }; var tokenResponse = { 'idToken': fireauth.common.testHelper.createMockJwt(), - 'refreshToken': 'refreshToken' + 'refreshToken': 'refreshToken', + 'expiresIn': '3600' }; expectedUser = new fireauth.AuthUser(config, tokenResponse, accountInfo); // Expected user with authDomain. diff --git a/packages/auth/test/testhelper.js b/packages/auth/test/testhelper.js index 4a477070809..89a421f6e2f 100644 --- a/packages/auth/test/testhelper.js +++ b/packages/auth/test/testhelper.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -267,22 +267,22 @@ fireauth.common.testHelper.installMockStorages = * @return {string} The mock JWT. */ fireauth.common.testHelper.createMockJwt = - function(opt_payload, opt_expirationTime) { + function(payload, expirationTime) { // JWT time units should not have decimals but to make testing easier, // we will allow it. - var now = goog.now() / 1000; - var basePayload = { + const now = Date.now() / 1000; + const basePayload = { 'iss': 'https://securetoken.google.com/projectId', 'aud': 'projectId', 'sub': '12345678', 'auth_time': now, 'iat': now, - 'exp': typeof opt_expirationTime === 'undefined' ? - now + 3600 : opt_expirationTime / 1000 + 'exp': typeof expirationTime === 'undefined' ? + now + 3600 : expirationTime / 1000 }; // Extend base payload. - goog.object.extend(basePayload, opt_payload || {}); - var encodedPayload = + Object.assign(basePayload, payload || {}); + const encodedPayload = goog.crypt.base64.encodeString(JSON.stringify(basePayload), goog.crypt.base64.Alphabet.WEBSAFE); // Remove any trailing or leading dots from the payload component. diff --git a/packages/auth/test/token_test.js b/packages/auth/test/token_test.js index f0b48c35914..e9ff013460b 100644 --- a/packages/auth/test/token_test.js +++ b/packages/auth/test/token_test.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,22 +34,18 @@ goog.require('goog.testing.jsunit'); goog.setTestOnly('fireauth.StsTokenManagerTest'); -var token = null; -var rpcHandler = null; -var stubs = new goog.testing.PropertyReplacer(); -var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); -var now; +let token = null; +let rpcHandler = null; +const stubs = new goog.testing.PropertyReplacer(); +const asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +const now = Date.now(); function setUp() { - now = goog.now(); - // Override goog.now(). - stubs.replace( - goog, - 'now', - function() { - return now; - }); + // Override now. + stubs.replace(Date, 'now', () => { + return now; + }); // Initialize RPC handler and token. rpcHandler = new fireauth.RpcHandler( 'apiKey', @@ -77,12 +73,12 @@ function tearDown() { * returned response. * @param {?Object|string} expectedData The expected body data. * @param {?Object} xhrResponse The returned response when no error is returned. - * @param {?fireauth.AuthError=} opt_error The specific error returned. + * @param {?fireauth.AuthError=} error The specific error returned. */ function assertRpcHandler( expectedData, xhrResponse, - opt_error) { + error) { stubs.replace( fireauth.RpcHandler.prototype, 'requestStsToken', @@ -93,8 +89,8 @@ function assertRpcHandler( if (xhrResponse) { // Return expected response. resolve(xhrResponse); - } else if (opt_error) { - reject(opt_error); + } else if (error) { + reject(error); } else { reject( new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); @@ -117,65 +113,83 @@ function assertErrorEquals(expected, actual) { function testStsTokenManager_gettersSetters() { - var expirationTime = goog.now() + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); + const expirationTime = now + 3600 * 1000; + const jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); assertEquals('refreshToken', token.getRefreshToken()); - assertEquals(expirationTime, token.getExpirationTime()); + assertEquals(token.getExpirationTime(), expirationTime); } function testStsTokenManager_parseServerResponse() { - var expirationTime = now + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); - var serverResponse = { + // We should prefer the local clock + expiresIn to the expiration time in the + // JWT. + const expirationTimeServer = now + 4800 * 1000; + const expirationTimeClient = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTimeServer); + const serverResponse = { 'idToken': jwt, 'refreshToken': 'myStsRefreshToken', + 'expiresIn': '3600' }; - var accessToken = token.parseServerResponse(serverResponse); + const accessToken = token.parseServerResponse(serverResponse); + assertEquals(jwt, accessToken); + assertEquals('myStsRefreshToken', token.getRefreshToken()); + assertEquals(token.getExpirationTime(), expirationTimeClient); +} + + +function testStsTokenManager_parseServerResponse_noExpiresIn() { + // If response does not contain expiresIn, we should fallback to using the + // delta between the exp and iat in the JWT. + const expirationTimeServer = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTimeServer); + const serverResponse = {'idToken': jwt, 'refreshToken': 'myStsRefreshToken'}; + const accessToken = token.parseServerResponse(serverResponse); assertEquals(jwt, accessToken); assertEquals('myStsRefreshToken', token.getRefreshToken()); - assertEquals(expirationTime, token.getExpirationTime()); + assertEquals(expirationTimeServer, token.getExpirationTime()); } function testStsTokenManager_toServerResponse() { - var expirationTime = goog.now() + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); + const expirationTime = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTime); token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); assertObjectEquals( - { - 'refreshToken': 'refreshToken', - 'idToken': jwt - }, + {'refreshToken': 'refreshToken', 'idToken': jwt}, token.toServerResponse()); } function testStsTokenManager_copy() { - var expirationTime = goog.now() + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); + const expirationTime = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTime); token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); // Injects a new RPC handler with different API key. - var rpcHandlerWithDiffApiKey = new fireauth.RpcHandler( - 'apiKey2', - { - 'tokenEndpoint': 'https://securetoken.googleapis.com/v1/token', - 'tokenTimeout': 10000, - 'tokenHeaders': { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }); - var tokenToCopy = new fireauth.StsTokenManager(rpcHandlerWithDiffApiKey); - var newExpirationTime = goog.now() + 4800 * 1000; - var newJwt = fireauth.common.testHelper.createMockJwt( - null, newExpirationTime); - var serverResponse = { + const rpcHandlerWithDiffApiKey = new fireauth.RpcHandler('apiKey2', { + 'tokenEndpoint': 'https://securetoken.googleapis.com/v1/token', + 'tokenTimeout': 10000, + 'tokenHeaders': {'Content-Type': 'application/x-www-form-urlencoded'} + }); + const tokenToCopy = new fireauth.StsTokenManager(rpcHandlerWithDiffApiKey); + const newExpirationTime = now + 4800 * 1000; + const newJwt = + fireauth.common.testHelper.createMockJwt(null, newExpirationTime); + const serverResponse = { 'idToken': newJwt, - 'refreshToken': 'newRefreshToken' + 'refreshToken': 'newRefreshToken', + 'expiresIn': '4800' }; tokenToCopy.parseServerResponse(serverResponse); @@ -186,17 +200,56 @@ function testStsTokenManager_copy() { 'apiKey': 'apiKey', 'refreshToken': 'newRefreshToken', 'accessToken': newJwt, - 'expirationTime': newExpirationTime + 'expirationTime': token.getExpirationTime() }, token.toPlainObject()); assertEquals(token.getExpirationTime(), newExpirationTime); } +function testStsTokenManager_copy_withClockSkew() { + const expirationTime = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTime); + token.setRefreshToken('refreshToken'); + token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); + // Injects a new RPC handler with different API key. + const rpcHandlerWithDiffApiKey = new fireauth.RpcHandler('apiKey2', { + 'tokenEndpoint': 'https://securetoken.googleapis.com/v1/token', + 'tokenTimeout': 10000, + 'tokenHeaders': {'Content-Type': 'application/x-www-form-urlencoded'} + }); + const tokenToCopy = new fireauth.StsTokenManager(rpcHandlerWithDiffApiKey); + const newExpirationTimeServer = now + 1200 * 1000; + const newExpirationTimeClient = now + 4800 * 1000; + const newJwt = fireauth.common.testHelper.createMockJwt( + null, newExpirationTimeServer); + const serverResponse = { + 'idToken': newJwt, + 'refreshToken': 'newRefreshToken', + 'expiresIn': '4800' + }; + + tokenToCopy.parseServerResponse(serverResponse); + token.copy(tokenToCopy); + assertObjectEquals( + { + // ApiKey should remain the same. + 'apiKey': 'apiKey', + 'refreshToken': 'newRefreshToken', + 'accessToken': newJwt, + 'expirationTime': token.getExpirationTime() + }, + token.toPlainObject()); + assertEquals(token.getExpirationTime(), newExpirationTimeClient); +} + + function testStsTokenManager_getToken_noToken() { // No token. asyncTestCase.waitForSignals(1); - token.getToken().then(function(token) { + token.getToken().then((token) => { assertNull(token); asyncTestCase.signal(); }); @@ -206,9 +259,9 @@ function testStsTokenManager_getToken_noToken() { function testStsTokenManager_getToken_invalidResponse() { // Test when an network error is returned and then called again successfully // that the network error is not cached. - var expectedError = + const expectedError = new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); - var jwt = fireauth.common.testHelper.createMockJwt(); + const jwt = fireauth.common.testHelper.createMockJwt(); token.setRefreshToken('myRefreshToken'); // Simulate invalid response from server. assertRpcHandler( @@ -219,7 +272,7 @@ function testStsTokenManager_getToken_invalidResponse() { null, expectedError); asyncTestCase.waitForSignals(1); - token.getToken().then(fail, function(error) { + token.getToken().then(fail, (error) => { // Invalid response error should be triggered. assertErrorEquals(expectedError, error); // Since this is not an expired token error, another call should still @@ -231,7 +284,8 @@ function testStsTokenManager_getToken_invalidResponse() { }, { 'access_token': jwt, - 'refresh_token': 'refreshToken2' + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' }); token.getToken().then(function(response) { // Confirm all properties updated. @@ -246,15 +300,16 @@ function testStsTokenManager_getToken_invalidResponse() { function testStsTokenManager_getToken_tokenExpiredError() { // Test when expired refresh token error is returned. // Simulate Id token is expired to force refresh. - var expirationTime = goog.now() - 1; + const expirationTime = now - 1; // Expected token expired error. - var expectedError = + const expectedError = new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); - var expiredJwt = fireauth.common.testHelper.createMockJwt( + const expiredJwt = fireauth.common.testHelper.createMockJwt( null, expirationTime); - var newJwt = fireauth.common.testHelper.createMockJwt(); + const newJwt = fireauth.common.testHelper.createMockJwt(); token.setAccessToken(expiredJwt); token.setRefreshToken('myRefreshToken'); + token.setExpiresAt(expirationTime); assertFalse(token.isRefreshTokenExpired()); // Simulate token expired error from server. assertRpcHandler( @@ -266,7 +321,7 @@ function testStsTokenManager_getToken_tokenExpiredError() { expectedError); asyncTestCase.waitForSignals(4); // This call will return token expired error. - token.getToken().then(fail, function(error) { + token.getToken().then(fail, (error) => { assertTrue(token.isRefreshTokenExpired()); // If another RPC is sent, it will resolve with valid STS token. // This should not happen since the token expired error is cached. @@ -283,13 +338,13 @@ function testStsTokenManager_getToken_tokenExpiredError() { // Token expired error should be thrown. assertErrorEquals(expectedError, error); // Try again, cached expired token error should be triggered. - token.getToken(false).thenCatch(function(error) { + token.getToken(false).thenCatch((error) => { assertErrorEquals(expectedError, error); asyncTestCase.signal(); }); // Try again with forced refresh, cached expired token error should be // triggered. - token.getToken(true).thenCatch(function(error) { + token.getToken(true).thenCatch((error) => { assertErrorEquals(expectedError, error); asyncTestCase.signal(); }); @@ -306,20 +361,21 @@ function testStsTokenManager_getToken_tokenExpiredError() { // cleared. token.setRefreshToken('refreshToken2'); // This should now resolve. - token.getToken().then(function(response) { + token.getToken().then((response) => { assertFalse(token.isRefreshTokenExpired()); // Confirm all properties updated. assertEquals(newJwt, response['accessToken']); assertEquals('refreshToken2', response['refreshToken']); // Plain object should have the new refresh token set. assertObjectEquals( - { - 'apiKey': 'apiKey', - 'refreshToken': 'refreshToken2', - 'accessToken': newJwt, - 'expirationTime': goog.now() + 3600 * 1000 - }, - token.toPlainObject()); + { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken2', + 'accessToken': newJwt, + 'expirationTime': token.getExpirationTime() + }, + token.toPlainObject()); + assertEquals(token.getExpirationTime(), now + 3600 * 1000); asyncTestCase.signal(); }); asyncTestCase.signal(); @@ -327,28 +383,84 @@ function testStsTokenManager_getToken_tokenExpiredError() { } +function testStsTokenManager_getToken_withClockSkew_clientBehind() { + // Server clock is ahead, so it looks like the token is not yet expired. + const expirationTimeServer = now + 900 * 1000; + // Client should realize that the token is actually expired. + const expirationTimeClient = now - 300 * 1000; + const expiredJwt = + fireauth.common.testHelper.createMockJwt(null, expirationTimeServer); + const newJwtExpirationServer = now + 4800 * 1000; + const newJwtExpirationClient = now + 3600 * 1000; + const newJwt = fireauth.common.testHelper.createMockJwt( + null, newJwtExpirationServer); + token.setRefreshToken('refreshToken'); + // Expired access token. + token.setAccessToken(expiredJwt); + token.setExpiresAt(expirationTimeClient); + // It will attempt to exchange refresh token for STS token. + assertRpcHandler( + {'grant_type': 'refresh_token', 'refresh_token': 'refreshToken'}, { + 'access_token': newJwt, + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' + }); + asyncTestCase.waitForSignals(1); + token.getToken().then((response) => { + // Confirm all properties updated. + assertEquals('refreshToken2', token.getRefreshToken()); + assertEquals(token.getExpirationTime(), newJwtExpirationClient); + // Confirm correct STS response. + assertObjectEquals( + {'accessToken': newJwt, 'refreshToken': 'refreshToken2'}, response); + asyncTestCase.signal(); + }); +} + + +function testStsTokenManager_getToken_withClockSkew_clientAhead() { + // Server clock is behind, so token looks like it's expired. + const expirationTimeServer = now - 1 * 1000; + // Client should realize that the token is not actually expired. + const expirationTimeClient = now + 1200 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTimeServer); + token.setRefreshToken('refreshToken'); + // Not yet expired access token. + token.setAccessToken(jwt); + token.setExpiresAt(expirationTimeClient); + asyncTestCase.waitForSignals(1); + token.getToken().then((response) => { + assertEquals('refreshToken', token.getRefreshToken()); + assertEquals(token.getExpirationTime(), expirationTimeClient); + // Confirm correct STS response. + assertObjectEquals( + {'accessToken': jwt, 'refreshToken': 'refreshToken'}, response); + asyncTestCase.signal(); + }); +} + + function testStsTokenManager_getToken_tokenAlmostExpired() { // Test when cached token is within range of being almost expired. - var expirationTime = goog.now() + - fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER - 1; - var almostExpiredJwt = fireauth.common.testHelper.createMockJwt( - null, expirationTime); - var newJwt = fireauth.common.testHelper.createMockJwt(); + const expirationTime = + now + fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER - 1; + const almostExpiredJwt = + fireauth.common.testHelper.createMockJwt(null, expirationTime); + const newJwt = fireauth.common.testHelper.createMockJwt(); token.setAccessToken(almostExpiredJwt); token.setRefreshToken('myRefreshToken'); + token.setExpiresAt(expirationTime); assertFalse(token.isRefreshTokenExpired()); // Token will be refreshed since cached token is almost expired. assertRpcHandler( - { - 'grant_type': 'refresh_token', - 'refresh_token': 'myRefreshToken' - }, - { + {'grant_type': 'refresh_token', 'refresh_token': 'myRefreshToken'}, { 'access_token': newJwt, - 'refresh_token': 'refreshToken2' + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' }); asyncTestCase.waitForSignals(1); - token.getToken().then(function(response) { + token.getToken().then((response) => { assertFalse(token.isRefreshTokenExpired()); // Confirm all properties updated. assertEquals(newJwt, response['accessToken']); @@ -358,7 +470,7 @@ function testStsTokenManager_getToken_tokenAlmostExpired() { 'apiKey': 'apiKey', 'refreshToken': 'refreshToken2', 'accessToken': newJwt, - 'expirationTime': goog.now() + 3600 * 1000 + 'expirationTime': token.getExpirationTime() }, token.toPlainObject()); asyncTestCase.signal(); @@ -368,14 +480,16 @@ function testStsTokenManager_getToken_tokenAlmostExpired() { function testStsTokenManager_getToken_exchangeRefreshToken() { // Set a previously cached access token that is expired. - var expirationTime = goog.now() - 1; - var unexpiredTime = goog.now() + 3600 * 1000; - var expiredJwt = + const expirationTime = now - 1000; + const unexpiredTime = now + 3600 * 1000; + const expiredJwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); - var newJwt = fireauth.common.testHelper.createMockJwt(null, unexpiredTime); + const newJwt = + fireauth.common.testHelper.createMockJwt(null, unexpiredTime); token.setRefreshToken('refreshToken'); // Expired access token. token.setAccessToken(expiredJwt); + token.setExpiresAt(expirationTime); // It will attempt to exchange refresh token for STS token. assertRpcHandler( { @@ -384,13 +498,14 @@ function testStsTokenManager_getToken_exchangeRefreshToken() { }, { 'access_token': newJwt, - 'refresh_token': 'refreshToken2' + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' }); asyncTestCase.waitForSignals(1); - token.getToken().then(function(response) { + token.getToken().then((response) => { // Confirm all properties updated. assertEquals('refreshToken2', token.getRefreshToken()); - assertEquals(unexpiredTime, token.getExpirationTime()); + assertEquals(token.getExpirationTime(), unexpiredTime); // Confirm correct STS response. assertObjectEquals( { @@ -405,17 +520,17 @@ function testStsTokenManager_getToken_exchangeRefreshToken() { function testStsTokenManager_getToken_cached() { // Set a previously cached access token that hasn't expired yet. - var expirationTime = goog.now() + 60 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); + const expirationTime = now + 60 * 1000; + const jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); // Set refresh token and unexpired access token. // No XHR request needed. token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); asyncTestCase.waitForSignals(1); - token.getToken().then(function(response) { + token.getToken().then((response) => { assertEquals('refreshToken', token.getRefreshToken()); - assertEquals( - expirationTime, token.getExpirationTime()); + assertEquals(token.getExpirationTime(), expirationTime); // Confirm correct STS response. assertObjectEquals( { @@ -430,13 +545,14 @@ function testStsTokenManager_getToken_cached() { function testStsTokenManager_getToken_forceRefresh() { // Set a previously cached access token that hasn't expired yet. - var expirationTime = goog.now() + 3000 * 1000; - var expirationTime2 = goog.now() + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); - var newJwt = fireauth.common.testHelper.createMockJwt(null, expirationTime2); + const expirationTime = now + 3000 * 1000; + const expirationTime2 = now + 3600 * 1000; + const jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); + const newJwt = fireauth.common.testHelper.createMockJwt(null, expirationTime2); // Set ID token, refresh token and unexpired access token. token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); // Even though unexpired access token, it will attempt to exchange for refresh // token since force refresh is set to true. assertRpcHandler( @@ -446,13 +562,14 @@ function testStsTokenManager_getToken_forceRefresh() { }, { 'access_token': newJwt, - 'refresh_token': 'refreshToken2' + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' }); asyncTestCase.waitForSignals(1); - token.getToken(true).then(function(response) { + token.getToken(true).then((response) => { // Confirm all properties updated. assertEquals('refreshToken2', token.getRefreshToken()); - assertEquals(expirationTime2, token.getExpirationTime()); + assertEquals(token.getExpirationTime(), expirationTime2); // Confirm correct STS response. assertObjectEquals( { @@ -466,10 +583,12 @@ function testStsTokenManager_getToken_forceRefresh() { function testToPlainObject() { - var expirationTime = goog.now() + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); + const expirationTime = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTime); token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); assertObjectEquals( { 'apiKey': 'apiKey', @@ -481,23 +600,61 @@ function testToPlainObject() { } +function testToPlainObject_withClockSkew() { + const expirationTimeServer = now + 1200 * 1000; + const expirationTimeClient = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTimeServer); + token.setRefreshToken('refreshToken'); + token.setAccessToken(jwt); + token.setExpiresIn(3600); + assertObjectEquals( + { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken', + 'accessToken': jwt, + 'expirationTime': token.getExpirationTime() + }, + token.toPlainObject()); + assertEquals(expirationTimeClient, token.getExpirationTime()); +} + + function testFromPlainObject() { - var expirationTime = goog.now() + 3600 * 1000; - var jwt = fireauth.common.testHelper.createMockJwt(null, expirationTime); - assertNull( - fireauth.StsTokenManager.fromPlainObject( - new fireauth.RpcHandler('apiKey'), - {})); + const expirationTime = now + 3600 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTime); + assertNull(fireauth.StsTokenManager.fromPlainObject( + new fireauth.RpcHandler('apiKey'), {})); token.setRefreshToken('refreshToken'); token.setAccessToken(jwt); + token.setExpiresAt(expirationTime); assertObjectEquals( - token, - fireauth.StsTokenManager.fromPlainObject( - rpcHandler, - { - 'apiKey': 'apiKey', - 'refreshToken': 'refreshToken', - 'accessToken': jwt - })); + token, fireauth.StsTokenManager.fromPlainObject(rpcHandler, { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken', + 'accessToken': jwt, + 'expirationTime': expirationTime + })); } + + +function testFromPlainObject_withClockSkew() { + const expirationTimeServer = now + 1200 * 1000; + const jwt = + fireauth.common.testHelper.createMockJwt(null, expirationTimeServer); + assertNull(fireauth.StsTokenManager.fromPlainObject( + new fireauth.RpcHandler('apiKey'), {})); + + token.setRefreshToken('refreshToken'); + token.setAccessToken(jwt); + token.setExpiresIn(3600); + assertObjectEquals( + token, fireauth.StsTokenManager.fromPlainObject(rpcHandler, { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken', + 'accessToken': jwt, + 'expirationTime': token.getExpirationTime() + })); +} \ No newline at end of file