Skip to content

Commit a727dce

Browse files
committed
Cleanup and add cookieStore fallback
1 parent 8acd061 commit a727dce

File tree

7 files changed

+93
-37
lines changed

7 files changed

+93
-37
lines changed

packages/auth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@
136136
"@rollup/plugin-strip": "2.1.0",
137137
"@types/express": "4.17.21",
138138
"chromedriver": "119.0.1",
139+
"cookie-store": "4.0.0-next.4",
139140
"rollup": "2.79.2",
140141
"rollup-plugin-sourcemaps": "0.6.3",
141142
"rollup-plugin-typescript2": "0.36.0",

packages/auth/src/api/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,9 +242,6 @@ export async function _performSignInRequest<T, V extends IdTokenResponse>(
242242
request?: T,
243243
customErrorMap: Partial<ServerErrorMap<ServerError>> = {}
244244
): Promise<V> {
245-
// TODO(jamedaniels) if auth persistence is cookie, proxy through the server endpoint
246-
// Q, do we want to allow signIn/Out to work normally on the server if cookie
247-
// persistence is set and FirebaseServerApp authIdToken is not?
248245
const serverResponse = await _performApiRequest<T, V | IdTokenMfaResponse>(
249246
auth,
250247
method,

packages/auth/src/core/persistence/persistence_user_manager.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,7 @@ export class PersistenceUserManager {
7575
}
7676
if (typeof blob === 'string') {
7777
const response = await getAccountInfo(this.auth, { idToken: blob });
78-
return await UserImpl._fromGetAccountInfoResponse(
79-
this.auth,
80-
response,
81-
blob
82-
);
78+
return UserImpl._fromGetAccountInfoResponse(this.auth, response, blob);
8379
}
8480
return UserImpl._fromJSON(this.auth, blob);
8581
}

packages/auth/src/core/user/user_credential_impl.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ export class UserCredentialImpl
4444
this.operationType = params.operationType;
4545
}
4646

47-
// TODO(jamesdaniels) fetch the user credential from the cookie and response returned from the
48-
// proxy endpoint
4947
static async _fromIdTokenResponse(
5048
auth: AuthInternal,
5149
operationType: OperationType,

packages/auth/src/model/user.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ export interface UserInternal extends User {
6262

6363
auth: AuthInternal;
6464
providerId: ProviderId.FIREBASE;
65-
// TODO(jamesdaniels): refreshToken should either be optional or a sentinel value for COOKIE
66-
// persistence, if refresh token has an identifier maybe that?
6765
refreshToken: string;
6866
emailVerified: boolean;
6967
tenantId: string | null;

packages/auth/src/platform_browser/persistence/cookie_storage.ts

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright 2019 Google LLC
3+
* Copyright 2025 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.
@@ -16,6 +16,9 @@
1616
*/
1717

1818
import { Persistence } from '../../model/public_types';
19+
import type { CookieChangeEvent } from 'cookie-store';
20+
21+
const POLLING_INTERVAL_MS = 1_000;
1922

2023
import {
2124
PersistenceInternal,
@@ -24,58 +27,116 @@ import {
2427
StorageEventListener
2528
} from '../../core/persistence';
2629

30+
const getDocumentCookie = (name: string): string | null => {
31+
const escapedName = name.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
32+
const matcher = RegExp(`${escapedName}=([^;]+)`);
33+
return document.cookie.match(matcher)?.[1] ?? null;
34+
};
35+
2736
export class CookiePersistence implements PersistenceInternal {
2837
static type: 'COOKIE' = 'COOKIE';
2938
readonly type = PersistenceType.COOKIE;
30-
listeners: Map<StorageEventListener, (e: any) => void> = new Map();
39+
cookieStoreListeners: Map<
40+
StorageEventListener,
41+
(event: CookieChangeEvent) => void
42+
> = new Map();
43+
cookiePollingIntervals: Map<StorageEventListener, NodeJS.Timeout> = new Map();
3144

3245
async _isAvailable(): Promise<boolean> {
33-
return navigator.hasOwnProperty('cookieEnabled') ?
34-
navigator.cookieEnabled :
35-
true;
46+
if (typeof navigator === 'undefined' || typeof document === 'undefined') {
47+
return false;
48+
}
49+
return navigator.cookieEnabled ?? true;
3650
}
3751

3852
async _set(_key: string, _value: PersistenceValue): Promise<void> {
3953
return;
4054
}
4155

4256
async _get<T extends PersistenceValue>(key: string): Promise<T | null> {
43-
const cookie = await (window as any).cookieStore.get(key);
44-
return cookie?.value;
57+
if (!this._isAvailable()) {
58+
return null;
59+
}
60+
if (window.cookieStore) {
61+
const cookie = await window.cookieStore.get(key);
62+
return cookie?.value as T;
63+
} else {
64+
return getDocumentCookie(key) as T;
65+
}
4566
}
4667

4768
async _remove(key: string): Promise<void> {
48-
const cookie = await (window as any).cookieStore.get(key);
49-
if (!cookie) {
69+
if (!this._isAvailable()) {
5070
return;
5171
}
52-
await (window as any).cookieStore.set({ ...cookie, value: "" });
72+
if (window.cookieStore) {
73+
const cookie = await window.cookieStore.get(key);
74+
if (!cookie) {
75+
return;
76+
}
77+
await window.cookieStore.delete(cookie);
78+
} else {
79+
// TODO how do I get the cookie properties?
80+
document.cookie = `${key}=;Max-Age=34560000;Partitioned;Secure;SameSite=Strict;Path=/`;
81+
}
5382
await fetch(`/__cookies__`, { method: 'DELETE' }).catch(() => undefined);
5483
}
5584

56-
_addListener(_key: string, _listener: StorageEventListener): void {
57-
// TODO fallback to polling if cookieStore is not available
58-
const cb = (event: any) => {
59-
const cookie = event.changed.find((change: any) => change.name === _key);
60-
if (cookie) {
61-
_listener(cookie.value);
62-
}
63-
};
64-
this.listeners.set(_listener, cb);
65-
(window as any).cookieStore.addEventListener('change', cb);
85+
_addListener(key: string, listener: StorageEventListener): void {
86+
if (!this._isAvailable()) {
87+
return;
88+
}
89+
if (window.cookieStore) {
90+
const cb = (event: CookieChangeEvent): void => {
91+
const changedCookie = event.changed.find(change => change.name === key);
92+
if (changedCookie) {
93+
listener(changedCookie.value as PersistenceValue);
94+
}
95+
const deletedCookie = event.deleted.find(change => change.name === key);
96+
if (deletedCookie) {
97+
listener(null);
98+
}
99+
};
100+
this.cookieStoreListeners.set(listener, cb);
101+
window.cookieStore.addEventListener('change', cb as EventListener);
102+
} else {
103+
let lastValue = getDocumentCookie(key);
104+
const interval = setInterval(() => {
105+
const currentValue = getDocumentCookie(key);
106+
if (currentValue !== lastValue) {
107+
listener(currentValue as PersistenceValue | null);
108+
lastValue = currentValue;
109+
}
110+
}, POLLING_INTERVAL_MS);
111+
this.cookiePollingIntervals.set(listener, interval);
112+
}
66113
}
67114

68-
_removeListener(_key: string, _listener: StorageEventListener): void {
69-
const cb = this.listeners.get(_listener);
70-
if (!cb) {
115+
// TODO can we tidy this logic up into a single unsubscribe function? () => void;
116+
_removeListener(_key: string, listener: StorageEventListener): void {
117+
if (!this._isAvailable()) {
71118
return;
72119
}
73-
(window as any).cookieStore.removeEventListener('change', cb);
120+
if (window.cookieStore) {
121+
const cb = this.cookieStoreListeners.get(listener);
122+
if (!cb) {
123+
return;
124+
}
125+
window.cookieStore.removeEventListener('change', cb as EventListener);
126+
this.cookieStoreListeners.delete(listener);
127+
} else {
128+
const interval = this.cookiePollingIntervals.get(listener);
129+
if (!interval) {
130+
return;
131+
}
132+
clearInterval(interval);
133+
this.cookiePollingIntervals.delete(listener);
134+
}
74135
}
75136
}
76137

77138
/**
78-
* An implementation of {@link Persistence} of type 'NONE'.
139+
* An implementation of {@link Persistence} of type 'COOKIE'.
79140
*
80141
* @public
81142
*/

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5893,6 +5893,11 @@ [email protected]:
58935893
resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
58945894
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
58955895

5896+
5897+
version "4.0.0-next.4"
5898+
resolved "https://registry.npmjs.org/cookie-store/-/cookie-store-4.0.0-next.4.tgz#8b13981bfd93e10e30694e9816928f8c478a326b"
5899+
integrity sha512-RVcIK13cCiAa+rsxAbFhrIThn1eBcgt9WTyLq539zMafDnhdGb6u/O5JdMTC3/pcJVqqHJmctiWxAYPpwT/fxw==
5900+
58965901
58975902
version "0.6.0"
58985903
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"

0 commit comments

Comments
 (0)