Skip to content

Commit 0c4f794

Browse files
authored
Update token expiration logic to account for missing expiresIn in certain requests. (#4085)
1 parent 34973cd commit 0c4f794

File tree

7 files changed

+97
-31
lines changed

7 files changed

+97
-31
lines changed

packages-exp/auth-exp/src/api/authentication/token.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ export const enum Endpoint {
3333

3434
/** The server responses with snake_case; we convert to camelCase */
3535
interface RequestStsTokenServerResponse {
36-
access_token?: string;
37-
expires_in?: string;
38-
refresh_token?: string;
36+
access_token: string;
37+
expires_in: string;
38+
refresh_token: string;
3939
}
4040

4141
export interface RequestStsTokenResponse {
42-
accessToken?: string;
43-
expiresIn?: string;
44-
refreshToken?: string;
42+
accessToken: string;
43+
expiresIn: string;
44+
refreshToken: string;
4545
}
4646

4747
export async function requestStsToken(

packages-exp/auth-exp/src/core/strategies/action_code_settings.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function _setActionCodeSettingsOnRequest(
2727
actionCodeSettings: ActionCodeSettings
2828
): void {
2929
_assert(
30-
actionCodeSettings.url.length > 0,
30+
actionCodeSettings.url?.length > 0,
3131
auth,
3232
AuthErrorCode.INVALID_CONTINUE_URI
3333
);

packages-exp/auth-exp/src/core/user/id_token_result.ts

+13
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,16 @@ export function _parseToken(token: string): externs.ParsedToken | null {
118118
return null;
119119
}
120120
}
121+
122+
/**
123+
* Extract expiresIn TTL from a token by subtracting the expiration from the issuance.
124+
*
125+
* @internal
126+
*/
127+
export function _tokenExpiresIn(token: string): number {
128+
const parsedToken = _parseToken(token);
129+
_assert(parsedToken, AuthErrorCode.INTERNAL_ERROR);
130+
_assert(typeof parsedToken.exp !== 'undefined', AuthErrorCode.INTERNAL_ERROR);
131+
_assert(typeof parsedToken.iat !== 'undefined', AuthErrorCode.INTERNAL_ERROR);
132+
return Number(parsedToken.exp) - Number(parsedToken.iat);
133+
}

packages-exp/auth-exp/src/core/user/token_manager.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import * as fetch from '../../../test/helpers/mock_fetch';
2626
import { Endpoint } from '../../api/authentication/token';
2727
import { IdTokenResponse } from '../../model/id_token';
2828
import { StsTokenManager, Buffer } from './token_manager';
29+
import { FinalizeMfaResponse } from '../../api/authentication/mfa';
30+
import { makeJWT } from '../../../test/helpers/jwt';
2931

3032
use(chaiAsPromised);
3133

@@ -74,6 +76,18 @@ describe('core/user/token_manager', () => {
7476
expect(stsTokenManager.accessToken).to.eq('id-token');
7577
expect(stsTokenManager.refreshToken).to.eq('refresh-token');
7678
});
79+
80+
it('falls back to exp and iat when expiresIn is ommited (ie: MFA)', () => {
81+
const idToken = makeJWT({ 'exp': '180', 'iat': '120' });
82+
stsTokenManager.updateFromServerResponse({
83+
idToken,
84+
refreshToken: 'refresh-token'
85+
} as FinalizeMfaResponse);
86+
87+
expect(stsTokenManager.expirationTime).to.eq(now + 60_000);
88+
expect(stsTokenManager.accessToken).to.eq(idToken);
89+
expect(stsTokenManager.refreshToken).to.eq('refresh-token');
90+
});
7791
});
7892

7993
describe('#clearRefreshToken', () => {

packages-exp/auth-exp/src/core/user/token_manager.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { IdTokenResponse } from '../../model/id_token';
2222
import { AuthErrorCode } from '../errors';
2323
import { PersistedBlob } from '../persistence';
2424
import { _assert, debugFail } from '../util/assert';
25+
import { _tokenExpiresIn } from './id_token_result';
2526

2627
/**
2728
* The number of milliseconds before the official expiration time of a token
@@ -48,10 +49,23 @@ export class StsTokenManager {
4849
updateFromServerResponse(
4950
response: IdTokenResponse | FinalizeMfaResponse
5051
): void {
52+
_assert(response.idToken, AuthErrorCode.INTERNAL_ERROR);
53+
_assert(
54+
typeof response.idToken !== 'undefined',
55+
AuthErrorCode.INTERNAL_ERROR
56+
);
57+
_assert(
58+
typeof response.refreshToken !== 'undefined',
59+
AuthErrorCode.INTERNAL_ERROR
60+
);
61+
const expiresIn =
62+
'expiresIn' in response && typeof response.expiresIn !== 'undefined'
63+
? Number(response.expiresIn)
64+
: _tokenExpiresIn(response.idToken);
5165
this.updateTokensAndExpiration(
5266
response.idToken,
5367
response.refreshToken,
54-
'expiresIn' in response ? response.expiresIn : undefined
68+
expiresIn
5569
);
5670
}
5771

@@ -83,19 +97,21 @@ export class StsTokenManager {
8397
auth,
8498
oldToken
8599
);
86-
this.updateTokensAndExpiration(accessToken, refreshToken, expiresIn);
100+
this.updateTokensAndExpiration(
101+
accessToken,
102+
refreshToken,
103+
Number(expiresIn)
104+
);
87105
}
88106

89107
private updateTokensAndExpiration(
90-
accessToken?: string,
91-
refreshToken?: string,
92-
expiresInSec?: string
108+
accessToken: string,
109+
refreshToken: string,
110+
expiresInSec: number
93111
): void {
94112
this.refreshToken = refreshToken || null;
95113
this.accessToken = accessToken || null;
96-
if (expiresInSec) {
97-
this.expirationTime = Date.now() + Number(expiresInSec) * 1000;
98-
}
114+
this.expirationTime = Date.now() + expiresInSec * 1000;
99115
}
100116

101117
static fromJSON(appName: string, object: PersistedBlob): StsTokenManager {

packages-exp/auth-exp/src/mfa/mfa_resolver.test.ts

+20-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { expect, use } from 'chai';
1919
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
2021

2122
import { OperationType, ProviderId } from '@firebase/auth-types-exp';
2223
import { FirebaseError } from '@firebase/util';
@@ -36,16 +37,20 @@ import { PhoneMultiFactorAssertion } from '../platform_browser/mfa/assertions/ph
3637
import { MultiFactorError } from './mfa_error';
3738
import { getMultiFactorResolver, MultiFactorResolver } from './mfa_resolver';
3839
import { _createError } from '../core/util/assert';
40+
import { makeJWT } from '../../test/helpers/jwt';
3941

4042
use(chaiAsPromised);
4143

4244
describe('core/mfa/mfa_resolver/MultiFactorResolver', () => {
45+
const finalIdToken = makeJWT({ 'exp': '3600', 'iat': '1200' });
4346
let auth: TestAuth;
4447
let underlyingError: FirebaseError;
4548
let error: MultiFactorError;
4649
let primaryFactorCredential: AuthCredential;
50+
let clock: sinon.SinonFakeTimers;
4751

4852
beforeEach(async () => {
53+
clock = sinon.useFakeTimers();
4954
auth = await testAuth();
5055
auth.tenantId = 'tenant-id';
5156
primaryFactorCredential = EmailAuthProvider.credential(
@@ -55,19 +60,23 @@ describe('core/mfa/mfa_resolver/MultiFactorResolver', () => {
5560
underlyingError = _createError(auth, AuthErrorCode.MFA_REQUIRED, {
5661
serverResponse: {
5762
localId: 'local-id',
58-
expiresIn: '3600',
5963
mfaPendingCredential: 'mfa-pending-credential',
6064
mfaInfo: [
6165
{
6266
mfaEnrollmentId: 'mfa-enrollment-id',
6367
enrolledAt: Date.now(),
64-
phoneInfo: 'phone-info'
68+
phoneInfo: '+*******1234',
69+
displayName: ''
6570
}
6671
]
6772
}
6873
});
6974
});
7075

76+
afterEach(() => {
77+
sinon.restore();
78+
});
79+
7180
describe('MultiFactorResolver', () => {
7281
let assertion: MultiFactorAssertion;
7382
let secondFactorCredential: PhoneAuthCredential;
@@ -110,7 +119,7 @@ describe('core/mfa/mfa_resolver/MultiFactorResolver', () => {
110119

111120
beforeEach(() => {
112121
mock = mockEndpoint(Endpoint.FINALIZE_PHONE_MFA_SIGN_IN, {
113-
idToken: 'final-id-token',
122+
idToken: finalIdToken,
114123
refreshToken: 'final-refresh-token'
115124
});
116125

@@ -135,13 +144,13 @@ describe('core/mfa/mfa_resolver/MultiFactorResolver', () => {
135144
assertion
136145
)) as UserCredential;
137146
expect(userCredential.user.uid).to.eq('local-id');
138-
expect(await userCredential.user.getIdToken()).to.eq(
139-
'final-id-token'
147+
expect(await userCredential.user.getIdToken()).to.eq(finalIdToken);
148+
expect(userCredential.user.stsTokenManager.expirationTime).to.eq(
149+
clock.now + 2400 * 1000
140150
);
141151
expect(userCredential._tokenResponse).to.eql({
142152
localId: 'local-id',
143-
expiresIn: '3600',
144-
idToken: 'final-id-token',
153+
idToken: finalIdToken,
145154
refreshToken: 'final-refresh-token'
146155
});
147156
expect(mock.calls[0].request).to.eql({
@@ -175,13 +184,13 @@ describe('core/mfa/mfa_resolver/MultiFactorResolver', () => {
175184
assertion
176185
)) as UserCredential;
177186
expect(userCredential.user).to.eq(user);
178-
expect(await userCredential.user.getIdToken()).to.eq(
179-
'final-id-token'
187+
expect(await userCredential.user.getIdToken()).to.eq(finalIdToken);
188+
expect(userCredential.user.stsTokenManager.expirationTime).to.eq(
189+
clock.now + 2400 * 1000
180190
);
181191
expect(userCredential._tokenResponse).to.eql({
182192
localId: 'local-id',
183-
expiresIn: '3600',
184-
idToken: 'final-id-token',
193+
idToken: finalIdToken,
185194
refreshToken: 'final-refresh-token'
186195
});
187196
expect(mock.calls[0].request).to.eql({

packages-exp/auth-exp/src/mfa/mfa_user.test.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import { expect, use } from 'chai';
1919
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
2021

2122
import { ProviderId } from '@firebase/auth-types-exp';
2223

@@ -33,6 +34,7 @@ import { MultiFactorSession, MultiFactorSessionType } from './mfa_session';
3334
import { multiFactor, MultiFactorUser } from './mfa_user';
3435
import { MultiFactorAssertion } from './mfa_assertion';
3536
import { Auth } from '../model/auth';
37+
import { makeJWT } from '../../test/helpers/jwt';
3638

3739
use(chaiAsPromised);
3840

@@ -57,16 +59,22 @@ class MockMultiFactorAssertion extends MultiFactorAssertion {
5759
}
5860

5961
describe('core/mfa/mfa_user/MultiFactorUser', () => {
62+
const idToken = makeJWT({ 'exp': '3600', 'iat': '1200' });
6063
let auth: TestAuth;
6164
let mfaUser: MultiFactorUser;
65+
let clock: sinon.SinonFakeTimers;
6266

6367
beforeEach(async () => {
6468
auth = await testAuth();
6569
mockFetch.setUp();
70+
clock = sinon.useFakeTimers();
6671
mfaUser = MultiFactorUser._fromUser(testUser(auth, 'uid', undefined, true));
6772
});
6873

69-
afterEach(mockFetch.tearDown);
74+
afterEach(() => {
75+
mockFetch.tearDown();
76+
sinon.restore();
77+
});
7078

7179
describe('getSession', () => {
7280
it('should return the id token', async () => {
@@ -99,7 +107,7 @@ describe('core/mfa/mfa_user/MultiFactorUser', () => {
99107
};
100108

101109
const serverResponse: FinalizeMfaResponse = {
102-
idToken: 'final-id-token',
110+
idToken,
103111
refreshToken: 'refresh-token'
104112
};
105113

@@ -114,7 +122,10 @@ describe('core/mfa/mfa_user/MultiFactorUser', () => {
114122
it('should update the tokens', async () => {
115123
await mfaUser.enroll(assertion);
116124

117-
expect(await mfaUser.user.getIdToken()).to.eq('final-id-token');
125+
expect(await mfaUser.user.getIdToken()).to.eq(idToken);
126+
expect(mfaUser.user.stsTokenManager.expirationTime).to.eq(
127+
clock.now + 2400 * 1000
128+
);
118129
});
119130

120131
it('should update the enrolled Factors', async () => {
@@ -131,7 +142,7 @@ describe('core/mfa/mfa_user/MultiFactorUser', () => {
131142
let withdrawMfaEnrollmentMock: mockFetch.Route;
132143

133144
const serverResponse: FinalizeMfaResponse = {
134-
idToken: 'final-id-token',
145+
idToken,
135146
refreshToken: 'refresh-token'
136147
};
137148

@@ -199,7 +210,10 @@ describe('core/mfa/mfa_user/MultiFactorUser', () => {
199210
it('should update the tokens', async () => {
200211
await mfaUser.unenroll(mfaInfo);
201212

202-
expect(await mfaUser.user.getIdToken()).to.eq('final-id-token');
213+
expect(await mfaUser.user.getIdToken()).to.eq(idToken);
214+
expect(mfaUser.user.stsTokenManager.expirationTime).to.eq(
215+
clock.now + 2400 * 1000
216+
);
203217
});
204218

205219
context('token revoked by backend', () => {

0 commit comments

Comments
 (0)