Skip to content

Commit 12bfc51

Browse files
authored
Add getIdTokenResult implementation (#3014)
1 parent 6b951a1 commit 12bfc51

File tree

6 files changed

+317
-17
lines changed

6 files changed

+317
-17
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
22+
import { FirebaseError } from '@firebase/util';
23+
24+
import { makeJWT } from '../../../test/jwt';
25+
import { testUser } from '../../../test/mock_auth';
26+
import { User } from '../../model/user';
27+
import { ProviderId } from '../providers';
28+
import { getIdTokenResult } from './id_token_result';
29+
30+
use(chaiAsPromised);
31+
32+
const MAY_1 = new Date('May 1, 2020');
33+
const MAY_2 = new Date('May 2, 2020');
34+
const MAY_3 = new Date('May 3, 2020');
35+
36+
describe('/core/user/id_token_result', () => {
37+
let user: User;
38+
39+
beforeEach(() => {
40+
user = testUser('uid');
41+
});
42+
43+
function setup(token: string): void {
44+
sinon.stub(user, 'getIdToken').returns(Promise.resolve(token));
45+
}
46+
47+
it('throws an internal error when the token is malformed', async () => {
48+
setup('not.valid');
49+
await expect(getIdTokenResult(user)).to.be.rejectedWith(
50+
FirebaseError,
51+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
52+
);
53+
});
54+
55+
it('builds the result properly w/ timestamps', async () => {
56+
const token = {
57+
'iat': (MAY_1.getTime() / 1000).toString(),
58+
'auth_time': (MAY_2.getTime() / 1000).toString(),
59+
'exp': (MAY_3.getTime() / 1000).toString()
60+
};
61+
62+
const encodedStr = makeJWT(token);
63+
setup(encodedStr);
64+
const result = await getIdTokenResult(user);
65+
expect(result).to.eql({
66+
claims: token,
67+
token: encodedStr,
68+
issuedAtTime: MAY_1.toUTCString(),
69+
authTime: MAY_2.toUTCString(),
70+
expirationTime: MAY_3.toUTCString(),
71+
signInProvider: null,
72+
signInSecondFactor: null
73+
});
74+
});
75+
76+
it('sets provider and second factor if available', async () => {
77+
const token = {
78+
'iat': (MAY_1.getTime() / 1000).toString(),
79+
'auth_time': (MAY_2.getTime() / 1000).toString(),
80+
'exp': (MAY_3.getTime() / 1000).toString(),
81+
'firebase': {
82+
'sign_in_provider': ProviderId.GOOGLE,
83+
'sign_in_second_factor': 'sure'
84+
}
85+
};
86+
87+
const encodedStr = makeJWT(token);
88+
setup(encodedStr);
89+
const result = await getIdTokenResult(user);
90+
expect(result).to.eql({
91+
claims: token,
92+
token: encodedStr,
93+
issuedAtTime: MAY_1.toUTCString(),
94+
authTime: MAY_2.toUTCString(),
95+
expirationTime: MAY_3.toUTCString(),
96+
signInProvider: ProviderId.GOOGLE,
97+
signInSecondFactor: 'sure'
98+
});
99+
});
100+
101+
it('errors if iat is missing', async () => {
102+
const token = {
103+
'auth_time': (MAY_2.getTime() / 1000).toString(),
104+
'exp': (MAY_3.getTime() / 1000).toString()
105+
};
106+
107+
const encodedStr = makeJWT(token);
108+
setup(encodedStr);
109+
await expect(getIdTokenResult(user)).to.be.rejectedWith(
110+
FirebaseError,
111+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
112+
);
113+
});
114+
115+
it('errors if auth_time is missing', async () => {
116+
const token = {
117+
'iat': (MAY_1.getTime() / 1000).toString(),
118+
'exp': (MAY_3.getTime() / 1000).toString()
119+
};
120+
121+
const encodedStr = makeJWT(token);
122+
setup(encodedStr);
123+
await expect(getIdTokenResult(user)).to.be.rejectedWith(
124+
FirebaseError,
125+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
126+
);
127+
});
128+
129+
it('errors if exp is missing', async () => {
130+
const token = {
131+
'iat': (MAY_1.getTime() / 1000).toString(),
132+
'auth_time': (MAY_2.getTime() / 1000).toString()
133+
};
134+
135+
const encodedStr = makeJWT(token);
136+
setup(encodedStr);
137+
await expect(getIdTokenResult(user)).to.be.rejectedWith(
138+
FirebaseError,
139+
'Firebase: An internal AuthError has occurred. (auth/internal-error).'
140+
);
141+
});
142+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { base64Decode } from '@firebase/util';
19+
20+
import { IdTokenResult, ParsedToken } from '../../model/id_token';
21+
import { User } from '../../model/user';
22+
import { ProviderId } from '../providers';
23+
import { assert } from '../util/assert';
24+
import { _logError } from '../util/log';
25+
26+
export async function getIdTokenResult(
27+
user: User,
28+
forceRefresh = false
29+
): Promise<IdTokenResult> {
30+
const token = await user.getIdToken(forceRefresh);
31+
const claims = parseToken(token);
32+
33+
assert(
34+
claims && claims.exp && claims.auth_time && claims.iat,
35+
user.auth.name
36+
);
37+
const firebase =
38+
typeof claims.firebase === 'object' ? claims.firebase : undefined;
39+
40+
const signInProvider: ProviderId | undefined = firebase?.[
41+
'sign_in_provider'
42+
] as ProviderId;
43+
assert(
44+
!signInProvider || Object.values(ProviderId).includes(signInProvider),
45+
user.auth.name
46+
);
47+
48+
return {
49+
claims,
50+
token,
51+
authTime: utcTimestampToDateString(
52+
secondsStringToMilliseconds(claims.auth_time)
53+
),
54+
issuedAtTime: utcTimestampToDateString(
55+
secondsStringToMilliseconds(claims.iat)
56+
),
57+
expirationTime: utcTimestampToDateString(
58+
secondsStringToMilliseconds(claims.exp)
59+
),
60+
signInProvider: signInProvider || null,
61+
signInSecondFactor: firebase?.['sign_in_second_factor'] || null
62+
};
63+
}
64+
65+
function secondsStringToMilliseconds(seconds: string): number {
66+
return Number(seconds) * 1000;
67+
}
68+
69+
function utcTimestampToDateString(timestamp: string | number): string | null {
70+
try {
71+
const date = new Date(Number(timestamp));
72+
if (!isNaN(date.getTime())) {
73+
return date.toUTCString();
74+
}
75+
} catch {
76+
// Do nothing, return null
77+
}
78+
79+
return null;
80+
}
81+
82+
function parseToken(token: string): ParsedToken | null {
83+
const [algorithm, payload, signature] = token.split('.');
84+
if (
85+
algorithm === undefined ||
86+
payload === undefined ||
87+
signature === undefined
88+
) {
89+
_logError('JWT malformed, contained fewer than 3 sections');
90+
return null;
91+
}
92+
93+
try {
94+
const decoded = base64Decode(payload);
95+
if (!decoded) {
96+
_logError('Failed to decode base64 JWT payload');
97+
return null;
98+
}
99+
return JSON.parse(decoded);
100+
} catch (e) {
101+
_logError('Caught error parsing JWT payload as JSON', e);
102+
return null;
103+
}
104+
}

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import * as chaiAsPromised from 'chai-as-promised';
2020

2121
import { FirebaseError } from '@firebase/util';
2222

23+
import { makeJWT } from '../../../test/jwt';
2324
import { mockAuth } from '../../../test/mock_auth';
2425
import { IdTokenResponse } from '../../model/id_token';
2526
import { StsTokenManager } from './token_manager';
@@ -84,9 +85,33 @@ describe('core/user/user_impl', () => {
8485
});
8586

8687
describe('#getIdTokenResult', () => {
87-
it('throws', async () => {
88+
// Smoke test; comprehensive tests in id_token_result.test.ts
89+
it('calls through to getIdTokenResult', async () => {
90+
const token = {
91+
'iat': String(new Date('May 1, 2020').getTime() / 1000),
92+
'auth_time': String(new Date('May 2, 2020').getTime() / 1000),
93+
'exp': String(new Date('May 3, 2020').getTime() / 1000)
94+
};
95+
96+
const jwt = makeJWT(token);
97+
98+
stsTokenManager.updateFromServerResponse({
99+
idToken: jwt,
100+
refreshToken: 'refresh-token-string',
101+
expiresIn: '100000'
102+
} as IdTokenResponse);
103+
88104
const user = new UserImpl({ uid: 'uid', auth, stsTokenManager });
89-
await expect(user.getIdTokenResult()).to.be.rejectedWith(Error);
105+
const tokenResult = await user.getIdTokenResult();
106+
expect(tokenResult).to.eql({
107+
issuedAtTime: new Date('May 1, 2020').toUTCString(),
108+
authTime: new Date('May 2, 2020').toUTCString(),
109+
expirationTime: new Date('May 3, 2020').toUTCString(),
110+
token: jwt,
111+
claims: token,
112+
signInProvider: null,
113+
signInSecondFactor: null
114+
});
90115
});
91116
});
92117

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { User } from '../../model/user';
2121
import { PersistedBlob } from '../persistence';
2222
import { ProviderId } from '../providers';
2323
import { assert } from '../util/assert';
24+
import { getIdTokenResult } from './id_token_result';
2425
import { reload } from './reload';
2526
import { StsTokenManager } from './token_manager';
2627

@@ -78,22 +79,18 @@ export class UserImpl implements User {
7879
const tokens = await this.stsTokenManager.getToken(this.auth, forceRefresh);
7980
assert(tokens, this.auth.name);
8081

81-
// TODO: remove ! after #2934 is merged --
82-
const { refreshToken, accessToken /* wasRefreshed */ } = tokens!;
82+
const { refreshToken, accessToken, wasRefreshed } = tokens;
8383
this.refreshToken = refreshToken || '';
8484

85-
// TODO: Uncomment after #2961 is merged
86-
// if (wasRefreshed && this.auth.currentUser === this) {
87-
// this.auth._notifyListeners();
88-
// }
85+
if (wasRefreshed && this.auth.currentUser === this) {
86+
this.auth._notifyStateListeners();
87+
}
8988

9089
return accessToken;
9190
}
9291

93-
async getIdTokenResult(forceRefresh?: boolean): Promise<IdTokenResult> {
94-
await this.getIdToken(forceRefresh);
95-
// TODO: Parse token
96-
throw new Error('Method not implemented');
92+
getIdTokenResult(forceRefresh?: boolean): Promise<IdTokenResult> {
93+
return getIdTokenResult(this, forceRefresh);
9794
}
9895

9996
reload(): Promise<void> {

packages-exp/auth-exp/src/model/id_token.d.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,21 @@ export interface APIMFAInfo {
7878
*/
7979
export interface IdTokenResult {
8080
token: string;
81-
authTime: string;
82-
expirationTime: string;
83-
issuedAtTime: string;
81+
authTime: string | null;
82+
expirationTime: string | null;
83+
issuedAtTime: string | null;
8484
signInProvider: ProviderId | null;
8585
signInSecondFactor: string | null;
86-
claims: {
87-
[claim: string]: string;
86+
claims: ParsedToken;
87+
}
88+
89+
export interface ParsedToken {
90+
'exp'?: string;
91+
'auth_time'?: string;
92+
'iat'?: string;
93+
'firebase'?: {
94+
'sign_in_provider'?: string;
95+
'sign_in_second_factor'?: string;
8896
};
97+
[key: string]: string | object | undefined;
8998
}

packages-exp/auth-exp/test/jwt.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { base64Encode } from '@firebase/util';
19+
20+
export function makeJWT(claims: object): string {
21+
const payload = base64Encode(JSON.stringify(claims));
22+
return `algorithm.${payload}.signature`;
23+
}

0 commit comments

Comments
 (0)