Skip to content

Commit 68ec30c

Browse files
authored
Merge 3df20fb into 4418d78
2 parents 4418d78 + 3df20fb commit 68ec30c

10 files changed

+481
-231
lines changed

.changeset/large-books-call.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/auth": patch
3+
---
4+
5+
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

packages/auth/src/authuser.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ fireauth.AuthUser.prototype.initializeProactiveRefreshUtility_ = function() {
508508
function() {
509509
// Get time until expiration minus the refresh offset.
510510
var waitInterval =
511-
self.stsTokenManager_.getExpirationTime() - goog.now() -
511+
self.stsTokenManager_.getExpirationTime() - Date.now() -
512512
fireauth.TokenRefreshTime.OFFSET_DURATION;
513513
// Set to zero if wait interval is negative.
514514
return waitInterval > 0 ? waitInterval : 0;
@@ -2436,6 +2436,12 @@ fireauth.AuthUser.fromPlainObject = function(user) {
24362436
// Refresh token could be expired.
24372437
stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN] =
24382438
user['stsTokenManager']['refreshToken'] || null;
2439+
const expirationTime = user['stsTokenManager']['expirationTime'];
2440+
if (expirationTime) {
2441+
stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField
2442+
.EXPIRES_IN] =
2443+
(expirationTime - Date.now()) / 1000;
2444+
}
24392445
} else {
24402446
// Token response is a required field.
24412447
return null;

packages/auth/src/idtoken.js

+26-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -30,7 +30,7 @@ goog.require('goog.crypt.base64');
3030
* @constructor
3131
*/
3232
fireauth.IdToken = function(tokenString) {
33-
var token = fireauth.IdToken.parseIdTokenClaims(tokenString);
33+
const token = fireauth.IdToken.parseIdTokenClaims(tokenString);
3434
if (!(token && token['sub'] && token['iss'] &&
3535
token['aud'] && token['exp'])) {
3636
throw new Error('Invalid JWT');
@@ -45,7 +45,7 @@ fireauth.IdToken = function(tokenString) {
4545
this.exp_ = token['exp'];
4646
/** @const @private {string} The local user ID of the token. */
4747
this.localId_ = token['sub'];
48-
var now = goog.now() / 1000;
48+
const now = Date.now() / 1000;
4949
/** @const @private {number} The issue time in seconds of the token. */
5050
this.iat_ = token['iat'] || (now > this.exp_ ? this.exp_ : now);
5151
/** @const @private {?string} The email address of the token. */
@@ -111,12 +111,24 @@ fireauth.IdToken.prototype.getEmail = function() {
111111
};
112112

113113

114-
/** @return {number} The expire time in seconds. */
114+
/**
115+
* @deprecated Use client side clock to calculate when the token expires.
116+
* @return {number} The expire time in seconds.
117+
*/
115118
fireauth.IdToken.prototype.getExp = function() {
116119
return this.exp_;
117120
};
118121

119122

123+
/**
124+
* @return {number} The difference in seconds between when the token was
125+
* issued and when it expires.
126+
*/
127+
fireauth.IdToken.prototype.getExpiresIn = function() {
128+
return this.exp_ - this.iat_;
129+
};
130+
131+
120132
/** @return {?string} The ID of the identity provider. */
121133
fireauth.IdToken.prototype.getProviderId = function() {
122134
return this.providerId_;
@@ -165,9 +177,12 @@ fireauth.IdToken.prototype.isVerified = function() {
165177
};
166178

167179

168-
/** @return {boolean} Whether token is expired. */
180+
/**
181+
* @deprecated Use client side clock to calculate when the token expires.
182+
* @return {boolean} Whether token is expired.
183+
*/
169184
fireauth.IdToken.prototype.isExpired = function() {
170-
var now = Math.floor(goog.now() / 1000);
185+
const now = Math.floor(Date.now() / 1000);
171186
// It is expired if token expiration time is less than current time.
172187
return this.getExp() <= now;
173188
};
@@ -218,18 +233,18 @@ fireauth.IdToken.parseIdTokenClaims = function(tokenString) {
218233
return null;
219234
}
220235
// Token format is <algorithm>.<info>.<sig>
221-
var fields = tokenString.split('.');
236+
const fields = tokenString.split('.');
222237
if (fields.length != 3) {
223238
return null;
224239
}
225-
var jsonInfo = fields[1];
240+
let jsonInfo = fields[1];
226241
// Google base64 library does not handle padding.
227-
var padLen = (4 - jsonInfo.length % 4) % 4;
228-
for (var i = 0; i < padLen; i++) {
242+
const padLen = (4 - jsonInfo.length % 4) % 4;
243+
for (let i = 0; i < padLen; i++) {
229244
jsonInfo += '.';
230245
}
231246
try {
232-
var token = JSON.parse(goog.crypt.base64.decodeString(jsonInfo, true));
247+
const token = JSON.parse(goog.crypt.base64.decodeString(jsonInfo, true));
233248
return /** @type {?Object} */ (token);
234249
} catch (e) {}
235250
return null;

packages/auth/src/token.js

+71-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2017 Google Inc.
3+
* Copyright 2017 Google LLC
44
*
55
* Licensed under the Apache License, Version 2.0 (the "License");
66
* you may not use this file except in compliance with the License.
@@ -47,6 +47,8 @@ fireauth.StsTokenManager = function(rpcHandler) {
4747
this.refreshToken_ = null;
4848
/** @private {?fireauth.IdToken} The STS ID token. */
4949
this.accessToken_ = null;
50+
/** @private {number} The expiration time of the token in epoch millis. */
51+
this.expiresAt_ = Date.now();
5052
};
5153

5254

@@ -73,13 +75,14 @@ fireauth.StsTokenManager.prototype.toPlainObject = function() {
7375
* plain object provided using the RPC handler provided.
7476
*/
7577
fireauth.StsTokenManager.fromPlainObject = function(rpcHandler, obj) {
76-
var stsTokenManager = null;
78+
let stsTokenManager = null;
7779
if (obj && obj['apiKey']) {
7880
// These should be always equals and must be enforced in internal use.
7981
goog.asserts.assert(obj['apiKey'] == rpcHandler.getApiKey());
8082
stsTokenManager = new fireauth.StsTokenManager(rpcHandler);
8183
stsTokenManager.setRefreshToken(obj['refreshToken']);
8284
stsTokenManager.setAccessToken(obj['accessToken']);
85+
stsTokenManager.setExpiresAt(obj['expirationTime']);
8386
}
8487
return stsTokenManager;
8588
};
@@ -118,6 +121,30 @@ fireauth.StsTokenManager.prototype.setAccessToken = function(accessToken) {
118121
this.accessToken_ = fireauth.IdToken.parse(accessToken || '');
119122
};
120123

124+
/**
125+
* To account for client side clock skew, we try to set the expiration time
126+
* using the local clock by adding the server TTL. If not provided, expiresAt
127+
* will be set from the accessToken by taking the difference between the exp
128+
* and iat fields.
129+
*
130+
* @param {number=} expiresIn The expiration TTL in seconds.
131+
*/
132+
fireauth.StsTokenManager.prototype.setExpiresIn = function(expiresIn) {
133+
expiresIn = typeof expiresIn !== 'undefined' ? expiresIn :
134+
this.accessToken_ ? this.accessToken_.getExpiresIn() :
135+
0;
136+
this.expiresAt_ = Date.now() + expiresIn * 1000;
137+
}
138+
139+
/**
140+
* Allow setting expiresAt directly when we know the time is already in the
141+
* local clock.
142+
*
143+
* @param {number} expiresAt The expiration time in epoch millis.
144+
*/
145+
fireauth.StsTokenManager.prototype.setExpiresAt = function(expiresAt) {
146+
this.expiresAt_ = expiresAt;
147+
}
121148

122149
/**
123150
* @return {?string} The refresh token.
@@ -131,7 +158,7 @@ fireauth.StsTokenManager.prototype.getRefreshToken = function() {
131158
* @return {number} The STS access token expiration time in milliseconds.
132159
*/
133160
fireauth.StsTokenManager.prototype.getExpirationTime = function() {
134-
return (this.accessToken_ && this.accessToken_.getExp() * 1000) || 0;
161+
return this.expiresAt_;
135162
};
136163

137164

@@ -148,7 +175,7 @@ fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER = 30 * 1000;
148175
* @private
149176
*/
150177
fireauth.StsTokenManager.prototype.isExpired_ = function() {
151-
return goog.now() >
178+
return Date.now() >
152179
this.getExpirationTime() - fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER;
153180
};
154181

@@ -161,11 +188,14 @@ fireauth.StsTokenManager.prototype.isExpired_ = function() {
161188
* @return {string} The STS access token.
162189
*/
163190
fireauth.StsTokenManager.prototype.parseServerResponse = function(response) {
164-
var idToken = response[fireauth.RpcHandler.AuthServerField.ID_TOKEN];
165-
var refreshToken =
166-
response[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN];
191+
const idToken = response[fireauth.RpcHandler.AuthServerField.ID_TOKEN];
167192
this.setAccessToken(idToken);
168-
this.setRefreshToken(refreshToken);
193+
this.setRefreshToken(
194+
response[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN]);
195+
// Not all IDP server responses come with expiresIn, some like MFA omit it.
196+
const expiresIn = response[fireauth.RpcHandler.AuthServerField.EXPIRES_IN];
197+
this.setExpiresIn(
198+
typeof expiresIn !== 'undefined' ? Number(expiresIn) : undefined);
169199
return idToken;
170200
};
171201

@@ -175,7 +205,7 @@ fireauth.StsTokenManager.prototype.parseServerResponse = function(response) {
175205
* @return {!Object}
176206
*/
177207
fireauth.StsTokenManager.prototype.toServerResponse = function() {
178-
var stsTokenManagerResponse = {};
208+
const stsTokenManagerResponse = {};
179209
stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.ID_TOKEN] =
180210
this.accessToken_ && this.accessToken_.toString();
181211
// Refresh token could be expired.
@@ -192,6 +222,7 @@ fireauth.StsTokenManager.prototype.toServerResponse = function() {
192222
fireauth.StsTokenManager.prototype.copy = function(tokenManagerToCopy) {
193223
this.accessToken_ = tokenManagerToCopy.accessToken_;
194224
this.refreshToken_ = tokenManagerToCopy.refreshToken_;
225+
this.expiresAt_ = tokenManagerToCopy.expiresAt_;
195226
};
196227

197228

@@ -201,7 +232,7 @@ fireauth.StsTokenManager.prototype.copy = function(tokenManagerToCopy) {
201232
* @private
202233
*/
203234
fireauth.StsTokenManager.prototype.exchangeRefreshToken_ = function() {
204-
var data = {
235+
const data = {
205236
'grant_type': 'refresh_token',
206237
'refresh_token': this.refreshToken_
207238
};
@@ -216,27 +247,32 @@ fireauth.StsTokenManager.prototype.exchangeRefreshToken_ = function() {
216247
* @private
217248
*/
218249
fireauth.StsTokenManager.prototype.requestToken_ = function(data) {
219-
var self = this;
220250
// Send RPC request to STS token endpoint.
221-
return this.rpcHandler_.requestStsToken(data).then(function(resp) {
222-
var response = /** @type {!fireauth.StsTokenManager.ResponseData} */ (resp);
223-
self.accessToken_ = fireauth.IdToken.parse(
224-
response[fireauth.RpcHandler.StsServerField.ACCESS_TOKEN]);
225-
self.refreshToken_ =
226-
response[fireauth.RpcHandler.StsServerField.REFRESH_TOKEN];
227-
return /** @type {!fireauth.StsTokenManager.Response} */ ({
228-
'accessToken': self.accessToken_.toString(),
229-
'refreshToken': self.refreshToken_
230-
});
231-
}).thenCatch(function(error) {
232-
// Refresh token expired or user deleted. In this case, reset refresh token
233-
// to prevent sending the request again to the STS server unless
234-
// the token is manually updated, perhaps via successful reauthentication.
235-
if (error['code'] == 'auth/user-token-expired') {
236-
self.refreshToken_ = null;
237-
}
238-
throw error;
239-
});
251+
return this.rpcHandler_.requestStsToken(data)
252+
.then((resp) => {
253+
const response =
254+
/** @type {!fireauth.StsTokenManager.ResponseData} */ (resp);
255+
this.accessToken_ = fireauth.IdToken.parse(
256+
response[fireauth.RpcHandler.StsServerField.ACCESS_TOKEN]);
257+
this.refreshToken_ =
258+
response[fireauth.RpcHandler.StsServerField.REFRESH_TOKEN];
259+
this.setExpiresIn(
260+
response[fireauth.RpcHandler.StsServerField.EXPIRES_IN]);
261+
return /** @type {!fireauth.StsTokenManager.Response} */ ({
262+
'accessToken': this.accessToken_.toString(),
263+
'refreshToken': this.refreshToken_
264+
});
265+
})
266+
.thenCatch((error) => {
267+
// Refresh token expired or user deleted. In this case, reset refresh
268+
// token to prevent sending the request again to the STS server unless
269+
// the token is manually updated, perhaps via successful
270+
// reauthentication.
271+
if (error['code'] == 'auth/user-token-expired') {
272+
this.refreshToken_ = null;
273+
}
274+
throw error;
275+
});
240276
};
241277

242278

@@ -251,12 +287,11 @@ fireauth.StsTokenManager.prototype.isRefreshTokenExpired = function() {
251287
* Otherwise the existing ID token or refresh token is exchanged for a new one.
252288
* If there is no user signed in, returns null.
253289
*
254-
* @param {boolean=} opt_forceRefresh Whether to force refresh token exchange.
290+
* @param {boolean=} forceRefresh Whether to force refresh token exchange.
255291
* @return {!goog.Promise<?fireauth.StsTokenManager.Response>}
256292
*/
257-
fireauth.StsTokenManager.prototype.getToken = function(opt_forceRefresh) {
258-
var self = this;
259-
var forceRefresh = !!opt_forceRefresh;
293+
fireauth.StsTokenManager.prototype.getToken = function(forceRefresh) {
294+
forceRefresh = !!forceRefresh;
260295
// Refresh token is expired.
261296
if (this.isRefreshTokenExpired()) {
262297
return goog.Promise.reject(
@@ -265,8 +300,8 @@ fireauth.StsTokenManager.prototype.getToken = function(opt_forceRefresh) {
265300
if (!forceRefresh && this.accessToken_ && !this.isExpired_()) {
266301
// Cached STS access token not expired, return it.
267302
return /** @type {!goog.Promise} */ (goog.Promise.resolve({
268-
'accessToken': self.accessToken_.toString(),
269-
'refreshToken': self.refreshToken_
303+
'accessToken': this.accessToken_.toString(),
304+
'refreshToken': this.refreshToken_
270305
}));
271306
} else if (this.refreshToken_) {
272307
// Expired but refresh token available, exchange refresh token for STS

0 commit comments

Comments
 (0)