Skip to content

Commit 6e151bf

Browse files
committed
added verifySessionCookie
1 parent abcb2af commit 6e151bf

File tree

5 files changed

+227
-101
lines changed

5 files changed

+227
-101
lines changed

src/auth.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import type { EmulatorEnv } from './emulator';
22
import { useEmulator } from './emulator';
33
import type { KeyStorer } from './key-store';
44
import type { FirebaseIdToken, FirebaseTokenVerifier } from './token-verifier';
5-
import { createIdTokenVerifier } from './token-verifier';
5+
import { createIdTokenVerifier, createSessionCookieVerifier } from './token-verifier';
66

77
export class BaseAuth {
88
/** @internal */
99
protected readonly idTokenVerifier: FirebaseTokenVerifier;
10+
protected readonly sessionCookieVerifier: FirebaseTokenVerifier;
1011

1112
constructor(projectId: string, keyStore: KeyStorer) {
1213
this.idTokenVerifier = createIdTokenVerifier(projectId, keyStore);
14+
this.sessionCookieVerifier = createSessionCookieVerifier(projectId, keyStore);
1315
}
1416

1517
/**
@@ -28,4 +30,30 @@ export class BaseAuth {
2830
const isEmulator = useEmulator(env);
2931
return this.idTokenVerifier.verifyJWT(idToken, isEmulator);
3032
}
33+
34+
/**
35+
* Verifies a Firebase session cookie. Returns a Promise with the cookie claims.
36+
* Rejects the promise if the cookie could not be verified.
37+
*
38+
* If `checkRevoked` is set to true, first verifies whether the corresponding
39+
* user is disabled: If yes, an `auth/user-disabled` error is thrown. If no,
40+
* verifies if the session corresponding to the session cookie was revoked.
41+
* If the corresponding user's session was invalidated, an
42+
* `auth/session-cookie-revoked` error is thrown. If not specified the check
43+
* is not performed.
44+
*
45+
* See {@link https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookie_and_check_permissions |
46+
* Verify Session Cookies}
47+
* for code samples and detailed documentation
48+
*
49+
* @param sessionCookie - The session cookie to verify.
50+
*
51+
* @returns A promise fulfilled with the
52+
* session cookie's decoded claims if the session cookie is valid; otherwise,
53+
* a rejected promise.
54+
*/
55+
public verifySessionCookie(sessionCookie: string, env?: EmulatorEnv): Promise<FirebaseIdToken> {
56+
const isEmulator = useEmulator(env);
57+
return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator);
58+
}
3159
}

src/errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export class AuthClientErrorCode {
6767
code: 'user-disabled',
6868
message: 'The user record is disabled.',
6969
};
70+
public static SESSION_COOKIE_EXPIRED = {
71+
code: 'session-cookie-expired',
72+
message: 'The Firebase session cookie is expired.',
73+
};
7074
}
7175

7276
/**

src/token-verifier.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,16 +405,60 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = {
405405
*/
406406
export function createIdTokenVerifier(projectID: string, keyStorer: KeyStorer): FirebaseTokenVerifier {
407407
const signatureVerifier = PublicKeySignatureVerifier.withCertificateUrl(CLIENT_JWK_URL, keyStorer);
408-
return createFirebaseTokenVerifier(signatureVerifier, projectID);
408+
return baseCreateIdTokenVerifier(signatureVerifier, projectID);
409409
}
410410

411411
/**
412412
* @internal
413413
* @returns FirebaseTokenVerifier
414414
*/
415-
export function createFirebaseTokenVerifier(
415+
export function baseCreateIdTokenVerifier(
416416
signatureVerifier: SignatureVerifier,
417417
projectID: string
418418
): FirebaseTokenVerifier {
419419
return new FirebaseTokenVerifier(signatureVerifier, projectID, 'https://securetoken.google.com/', ID_TOKEN_INFO);
420420
}
421+
422+
// URL containing the public keys for Firebase session cookies.
423+
const SESSION_COOKIE_CERT_URL = 'https://identitytoolkit.googleapis.com/v1/sessionCookiePublicKeys';
424+
425+
/**
426+
* User facing token information related to the Firebase session cookie.
427+
*
428+
* @internal
429+
*/
430+
export const SESSION_COOKIE_INFO: FirebaseTokenInfo = {
431+
url: 'https://firebase.google.com/docs/auth/admin/manage-cookies',
432+
verifyApiName: 'verifySessionCookie()',
433+
jwtName: 'Firebase session cookie',
434+
shortName: 'session cookie',
435+
expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED,
436+
};
437+
438+
/**
439+
* Creates a new FirebaseTokenVerifier to verify Firebase session cookies.
440+
*
441+
* @internal
442+
* @param app - Firebase app instance.
443+
* @returns FirebaseTokenVerifier
444+
*/
445+
export function createSessionCookieVerifier(projectID: string, keyStorer: KeyStorer): FirebaseTokenVerifier {
446+
const signatureVerifier = PublicKeySignatureVerifier.withCertificateUrl(SESSION_COOKIE_CERT_URL, keyStorer);
447+
return baseCreateSessionCookieVerifier(signatureVerifier, projectID);
448+
}
449+
450+
/**
451+
* @internal
452+
* @returns FirebaseTokenVerifier
453+
*/
454+
export function baseCreateSessionCookieVerifier(
455+
signatureVerifier: SignatureVerifier,
456+
projectID: string
457+
): FirebaseTokenVerifier {
458+
return new FirebaseTokenVerifier(
459+
signatureVerifier,
460+
projectID,
461+
'https://session.firebase.google.com/',
462+
SESSION_COOKIE_INFO
463+
);
464+
}

tests/firebase-utils.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { PublicKeySignatureVerifier } from '../src/jws-verifier';
2+
import type { FirebaseIdToken } from '../src/token-verifier';
3+
import { signJWT, genTime, genIss, TestingKeyFetcher } from './jwk-utils';
4+
5+
export const projectId = 'projectId1234';
6+
export const userId = 'userId12345';
7+
8+
export async function generateIdToken(
9+
currentTimestamp?: number,
10+
overrides?: Partial<FirebaseIdToken>
11+
): Promise<FirebaseIdToken> {
12+
const now = currentTimestamp ?? genTime(Date.now());
13+
return Object.assign(
14+
{
15+
aud: projectId,
16+
exp: now + 9999,
17+
iat: now - 10000, // -10s
18+
iss: genIss(projectId),
19+
sub: userId,
20+
auth_time: now - 20000, // -20s
21+
uid: userId,
22+
firebase: {
23+
identities: {},
24+
sign_in_provider: 'google.com',
25+
},
26+
} satisfies FirebaseIdToken,
27+
overrides
28+
);
29+
}
30+
31+
export async function generateSessionCookie(
32+
currentTimestamp?: number,
33+
overrides?: Partial<FirebaseIdToken>
34+
): Promise<FirebaseIdToken> {
35+
const now = currentTimestamp ?? genTime(Date.now());
36+
return Object.assign(
37+
{
38+
aud: projectId,
39+
exp: now + 9999,
40+
iat: now - 10000, // -10s
41+
iss: 'https://session.firebase.google.com/' + projectId,
42+
sub: userId,
43+
auth_time: now - 20000, // -20s
44+
uid: userId,
45+
firebase: {
46+
identities: {},
47+
sign_in_provider: 'google.com',
48+
},
49+
} satisfies FirebaseIdToken,
50+
overrides
51+
);
52+
}
53+
54+
interface SignVerifyPair {
55+
jwt: string;
56+
verifier: PublicKeySignatureVerifier;
57+
}
58+
59+
export async function createTestingSignVerifyPair(payload: FirebaseIdToken): Promise<SignVerifyPair> {
60+
const kid = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd';
61+
const testingKeyFetcher = await TestingKeyFetcher.withKeyPairGeneration(kid);
62+
const jwt = await signJWT(kid, payload, testingKeyFetcher.getPrivateKey());
63+
return {
64+
jwt,
65+
verifier: new PublicKeySignatureVerifier(testingKeyFetcher),
66+
};
67+
}

tests/token-verifier.test.ts

Lines changed: 81 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,96 @@
11
import { describe, it, expect } from 'vitest';
22
import { PublicKeySignatureVerifier, rs256alg } from '../src/jws-verifier';
3-
import type { FirebaseIdToken } from '../src/token-verifier';
4-
import { createFirebaseTokenVerifier, FIREBASE_AUDIENCE } from '../src/token-verifier';
5-
import { genIss, genTime, signJWT, TestingKeyFetcher } from './jwk-utils';
3+
import { FIREBASE_AUDIENCE, baseCreateIdTokenVerifier, baseCreateSessionCookieVerifier } from '../src/token-verifier';
4+
import { createTestingSignVerifyPair, generateIdToken, generateSessionCookie, projectId } from './firebase-utils';
5+
import { genTime, signJWT, TestingKeyFetcher } from './jwk-utils';
66

77
describe('FirebaseTokenVerifier', () => {
8-
const kid = 'kid123456';
9-
const projectId = 'projectId1234';
10-
const currentTimestamp = genTime(Date.now());
11-
const userId = 'userId12345';
12-
const payload: FirebaseIdToken = {
13-
aud: projectId,
14-
exp: currentTimestamp + 9999,
15-
iat: currentTimestamp - 10000, // -10s
16-
iss: genIss(projectId),
17-
sub: userId,
18-
auth_time: currentTimestamp - 20000, // -20s
19-
uid: userId,
20-
firebase: {
21-
identities: {},
22-
sign_in_provider: 'google.com',
8+
const testCases = [
9+
{
10+
name: 'createIdTokenVerifier',
11+
tokenGenerator: generateIdToken,
12+
firebaseTokenVerifier: baseCreateIdTokenVerifier,
2313
},
24-
};
25-
26-
it.each([
27-
['valid without firebase emulator', payload],
28-
[
29-
'valid custom token without firebase emulator',
30-
{
31-
...payload,
32-
aud: FIREBASE_AUDIENCE,
33-
},
34-
],
35-
])('%s', async (_, payload) => {
36-
const testingKeyFetcher = await TestingKeyFetcher.withKeyPairGeneration(kid);
37-
const jwt = await signJWT(kid, payload, testingKeyFetcher.getPrivateKey());
38-
39-
const verifier = new PublicKeySignatureVerifier(testingKeyFetcher);
40-
const ftv = createFirebaseTokenVerifier(verifier, projectId);
41-
const token = await ftv.verifyJWT(jwt, false);
14+
{
15+
name: 'createSessionCookieVerifier',
16+
tokenGenerator: generateSessionCookie,
17+
firebaseTokenVerifier: baseCreateSessionCookieVerifier,
18+
},
19+
];
20+
for (const tc of testCases) {
21+
describe(tc.name, () => {
22+
const currentTimestamp = genTime(Date.now());
4223

43-
expect(token).toStrictEqual(payload);
44-
});
24+
it.each([
25+
['valid without firebase emulator', tc.tokenGenerator(currentTimestamp)],
26+
[
27+
'valid custom token without firebase emulator',
28+
tc.tokenGenerator(currentTimestamp, { aud: FIREBASE_AUDIENCE }),
29+
],
30+
])('%s', async (_, promise) => {
31+
const payload = await promise;
32+
const pair = await createTestingSignVerifyPair(payload);
33+
const ftv = tc.firebaseTokenVerifier(pair.verifier, projectId);
34+
const token = await ftv.verifyJWT(pair.jwt, false);
4535

46-
it('valid with firebase emulator', async () => {
47-
const testingKeyFetcher = await TestingKeyFetcher.withKeyPairGeneration(kid);
36+
expect(token).toStrictEqual(payload);
37+
});
4838

49-
// sign as invalid private key with fetched public key
50-
const keyPair = await crypto.subtle.generateKey(rs256alg, true, ['sign', 'verify']);
39+
it.each([
40+
['aud', tc.tokenGenerator(currentTimestamp, { aud: 'unknown' }), 'has incorrect "aud" (audience) claim.'],
41+
[
42+
'iss',
43+
tc.tokenGenerator(currentTimestamp, {
44+
iss: projectId, // set just projectId
45+
}),
46+
'has incorrect "iss" (issuer) claim.',
47+
],
48+
[
49+
'sub',
50+
tc.tokenGenerator(currentTimestamp, {
51+
sub: 'x'.repeat(129),
52+
}),
53+
'has "sub" (subject) claim longer than 128 characters.',
54+
],
55+
[
56+
'auth_time',
57+
tc.tokenGenerator(currentTimestamp, {
58+
auth_time: undefined,
59+
}),
60+
'has no "auth_time" claim.',
61+
],
62+
[
63+
'auth_time is in future',
64+
tc.tokenGenerator(currentTimestamp, {
65+
auth_time: currentTimestamp + 3000, // +3s
66+
}),
67+
'has incorrect "auth_time" claim.',
68+
],
69+
])('invalid verifyPayload %s', async (_, promise, wantContainMsg) => {
70+
const payload = await promise;
71+
const pair = await createTestingSignVerifyPair(payload);
72+
const ftv = tc.firebaseTokenVerifier(pair.verifier, projectId);
73+
expect(() => ftv.verifyJWT(pair.jwt, false)).rejects.toThrowError(wantContainMsg);
74+
});
5175

52-
// set with invalid kid because jwt does not contain kid which issued from firebase emulator.
53-
const jwt = await signJWT('invalid-kid', payload, keyPair.privateKey);
76+
it('valid with firebase emulator', async () => {
77+
const payload = await tc.tokenGenerator(currentTimestamp);
78+
const testingKeyFetcher = await TestingKeyFetcher.withKeyPairGeneration('valid-kid');
5479

55-
const verifier = new PublicKeySignatureVerifier(testingKeyFetcher);
56-
const ftv = createFirebaseTokenVerifier(verifier, projectId);
80+
// sign as invalid private key with fetched public key
81+
const keyPair = await crypto.subtle.generateKey(rs256alg, true, ['sign', 'verify']);
5782

58-
// firebase emulator ignores signature verification step.
59-
const token = await ftv.verifyJWT(jwt, true);
83+
// set with invalid kid because jwt does not contain kid which issued from firebase emulator.
84+
const jwt = await signJWT('invalid-kid', payload, keyPair.privateKey);
6085

61-
expect(token).toStrictEqual(payload);
62-
});
86+
const verifier = new PublicKeySignatureVerifier(testingKeyFetcher);
87+
const ftv = tc.firebaseTokenVerifier(verifier, projectId);
6388

64-
it.each([
65-
[
66-
'aud',
67-
{
68-
...payload,
69-
aud: 'unknown',
70-
},
71-
'has incorrect "aud" (audience) claim.',
72-
],
73-
[
74-
'iss',
75-
{
76-
...payload,
77-
iss: projectId, // set just projectId
78-
},
79-
'has incorrect "iss" (issuer) claim.',
80-
],
81-
[
82-
'sub',
83-
{
84-
...payload,
85-
sub: 'x'.repeat(129),
86-
},
87-
'has "sub" (subject) claim longer than 128 characters.',
88-
],
89-
[
90-
'auth_time',
91-
{
92-
...payload,
93-
auth_time: undefined,
94-
},
95-
'has no "auth_time" claim.',
96-
],
97-
[
98-
'auth_time is in future',
99-
{
100-
...payload,
101-
auth_time: currentTimestamp + 3000, // +3s
102-
},
103-
'has incorrect "auth_time" claim.',
104-
],
105-
])('invalid verifyPayload %s', async (_, payload, wantContainMsg) => {
106-
const testingKeyFetcher = await TestingKeyFetcher.withKeyPairGeneration(kid);
107-
const jwt = await signJWT(kid, payload, testingKeyFetcher.getPrivateKey());
89+
// firebase emulator ignores signature verification step.
90+
const token = await ftv.verifyJWT(jwt, true);
10891

109-
const verifier = new PublicKeySignatureVerifier(testingKeyFetcher);
110-
const ftv = createFirebaseTokenVerifier(verifier, projectId);
111-
expect(() => ftv.verifyJWT(jwt, false)).rejects.toThrowError(wantContainMsg);
112-
});
92+
expect(token).toStrictEqual(payload);
93+
});
94+
});
95+
}
11396
});

0 commit comments

Comments
 (0)