mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
Auth cookie persistence (#8839)
Adding `Persistence.COOKIE` a new persistence method backed by cookies. The `browserCookiePersistence` implementation is designed to be used in conjunction with middleware that ensures both your front and backend authentication state remains synchronized.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'firebase': minor
|
||||
'@firebase/auth': minor
|
||||
---
|
||||
|
||||
Adding `Persistence.COOKIE` a new persistence method backed by cookies. The
|
||||
`browserCookiePersistence` implementation is designed to be used in conjunction with middleware that
|
||||
ensures both your front and backend authentication state remains synchronized.
|
||||
@@ -258,6 +258,9 @@ export interface AuthSettings {
|
||||
// @public
|
||||
export function beforeAuthStateChanged(auth: Auth, callback: (user: User | null) => void | Promise<void>, onAbort?: () => void): Unsubscribe;
|
||||
|
||||
// @beta
|
||||
export const browserCookiePersistence: Persistence;
|
||||
|
||||
// @public
|
||||
export const browserLocalPersistence: Persistence;
|
||||
|
||||
@@ -596,7 +599,7 @@ export interface PasswordValidationStatus {
|
||||
|
||||
// @public
|
||||
export interface Persistence {
|
||||
readonly type: 'SESSION' | 'LOCAL' | 'NONE';
|
||||
readonly type: 'SESSION' | 'LOCAL' | 'NONE' | 'COOKIE';
|
||||
}
|
||||
|
||||
// @public
|
||||
|
||||
@@ -150,6 +150,7 @@ Firebase Authentication
|
||||
| --- | --- |
|
||||
| [ActionCodeOperation](./auth.md#actioncodeoperation) | An enumeration of the possible email action types. |
|
||||
| [AuthErrorCodes](./auth.md#autherrorcodes) | A map of potential <code>Auth</code> error codes, for easier comparison with errors thrown by the SDK. |
|
||||
| [browserCookiePersistence](./auth.md#browsercookiepersistence) | <b><i>(Public Preview)</i></b> An implementation of [Persistence](./auth.persistence.md#persistence_interface) of type <code>COOKIE</code>, for use on the client side in applications leveraging hybrid rendering and middleware. |
|
||||
| [browserLocalPersistence](./auth.md#browserlocalpersistence) | An implementation of [Persistence](./auth.persistence.md#persistence_interface) of type <code>LOCAL</code> using <code>localStorage</code> for the underlying storage. |
|
||||
| [browserPopupRedirectResolver](./auth.md#browserpopupredirectresolver) | An implementation of [PopupRedirectResolver](./auth.popupredirectresolver.md#popupredirectresolver_interface) suitable for browser based applications. |
|
||||
| [browserSessionPersistence](./auth.md#browsersessionpersistence) | An implementation of [Persistence](./auth.persistence.md#persistence_interface) of <code>SESSION</code> using <code>sessionStorage</code> for the underlying storage. |
|
||||
@@ -1960,6 +1961,21 @@ AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY: {
|
||||
}
|
||||
```
|
||||
|
||||
## browserCookiePersistence
|
||||
|
||||
> This API is provided as a preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.
|
||||
>
|
||||
|
||||
An implementation of [Persistence](./auth.persistence.md#persistence_interface) of type `COOKIE`<!-- -->, for use on the client side in applications leveraging hybrid rendering and middleware.
|
||||
|
||||
This persistence method requires companion middleware to function, such as that provided by [ReactFire](https://firebaseopensource.com/projects/firebaseextended/reactfire/) for NextJS.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
browserCookiePersistence: Persistence
|
||||
```
|
||||
|
||||
## browserLocalPersistence
|
||||
|
||||
An implementation of [Persistence](./auth.persistence.md#persistence_interface) of type `LOCAL` using `localStorage` for the underlying storage.
|
||||
|
||||
@@ -22,14 +22,14 @@ export interface Persistence
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [type](./auth.persistence.md#persistencetype) | 'SESSION' \| 'LOCAL' \| 'NONE' | Type of Persistence. - 'SESSION' is used for temporary persistence such as <code>sessionStorage</code>. - 'LOCAL' is used for long term persistence such as <code>localStorage</code> or <code>IndexedDB</code>. - 'NONE' is used for in-memory, or no persistence. |
|
||||
| [type](./auth.persistence.md#persistencetype) | 'SESSION' \| 'LOCAL' \| 'NONE' \| 'COOKIE' | Type of Persistence. - 'SESSION' is used for temporary persistence such as <code>sessionStorage</code>. - 'LOCAL' is used for long term persistence such as <code>localStorage</code> or <code>IndexedDB</code>. - 'NONE' is used for in-memory, or no persistence. - 'COOKIE' is used for cookie persistence, useful for server-side rendering. |
|
||||
|
||||
## Persistence.type
|
||||
|
||||
Type of Persistence. - 'SESSION' is used for temporary persistence such as `sessionStorage`<!-- -->. - 'LOCAL' is used for long term persistence such as `localStorage` or `IndexedDB`<!-- -->. - 'NONE' is used for in-memory, or no persistence.
|
||||
Type of Persistence. - 'SESSION' is used for temporary persistence such as `sessionStorage`<!-- -->. - 'LOCAL' is used for long term persistence such as `localStorage` or `IndexedDB`<!-- -->. - 'NONE' is used for in-memory, or no persistence. - 'COOKIE' is used for cookie persistence, useful for server-side rendering.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly type: 'SESSION' | 'LOCAL' | 'NONE';
|
||||
readonly type: 'SESSION' | 'LOCAL' | 'NONE' | 'COOKIE';
|
||||
```
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('auth compat', () => {
|
||||
it('saves the persistence into session storage if available', async () => {
|
||||
if (typeof self !== 'undefined') {
|
||||
underlyingAuth._initializationPromise = Promise.resolve();
|
||||
sinon.stub(underlyingAuth, '_getPersistence').returns('TEST');
|
||||
sinon.stub(underlyingAuth, '_getPersistenceType').returns('TEST');
|
||||
sinon
|
||||
.stub(underlyingAuth, '_initializationPromise')
|
||||
.value(Promise.resolve());
|
||||
@@ -97,7 +97,7 @@ describe('auth compat', () => {
|
||||
}
|
||||
} as unknown as Window);
|
||||
const setItemSpy = sinon.spy(sessionStorage, 'setItem');
|
||||
sinon.stub(underlyingAuth, '_getPersistence').returns('TEST');
|
||||
sinon.stub(underlyingAuth, '_getPersistenceType').returns('TEST');
|
||||
sinon
|
||||
.stub(underlyingAuth, '_initializationPromise')
|
||||
.value(Promise.resolve());
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function _savePersistenceForRedirect(
|
||||
auth.name
|
||||
);
|
||||
if (session) {
|
||||
session.setItem(key, auth._getPersistence());
|
||||
session.setItem(key, auth._getPersistenceType());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export * from './src';
|
||||
|
||||
// persistence
|
||||
import { browserLocalPersistence } from './src/platform_browser/persistence/local_storage';
|
||||
import { browserCookiePersistence } from './src/platform_browser/persistence/cookie_storage';
|
||||
import { browserSessionPersistence } from './src/platform_browser/persistence/session_storage';
|
||||
import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db';
|
||||
|
||||
@@ -83,6 +84,7 @@ import { getAuth } from './src/platform_browser';
|
||||
|
||||
export {
|
||||
browserLocalPersistence,
|
||||
browserCookiePersistence,
|
||||
browserSessionPersistence,
|
||||
indexedDBLocalPersistence,
|
||||
PhoneAuthProvider,
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
"@rollup/plugin-strip": "2.1.0",
|
||||
"@types/express": "4.17.21",
|
||||
"chromedriver": "119.0.1",
|
||||
"cookie-store": "4.0.0-next.4",
|
||||
"rollup": "2.79.2",
|
||||
"rollup-plugin-sourcemaps": "0.6.3",
|
||||
"rollup-plugin-typescript2": "0.36.0",
|
||||
|
||||
@@ -74,7 +74,7 @@ export async function requestStsToken(
|
||||
'refresh_token': refreshToken
|
||||
}).slice(1);
|
||||
const { tokenApiHost, apiKey } = auth.config;
|
||||
const url = _getFinalTarget(
|
||||
const url = await _getFinalTarget(
|
||||
auth,
|
||||
tokenApiHost,
|
||||
Endpoint.TOKEN,
|
||||
|
||||
@@ -509,17 +509,17 @@ describe('api/_performApiRequest', () => {
|
||||
});
|
||||
|
||||
context('_getFinalTarget', () => {
|
||||
it('works properly with a non-emulated environment', () => {
|
||||
expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq(
|
||||
it('works properly with a non-emulated environment', async () => {
|
||||
expect(await _getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq(
|
||||
'mock://host/path?query=test'
|
||||
);
|
||||
});
|
||||
|
||||
it('works properly with an emulated environment', () => {
|
||||
it('works properly with an emulated environment', async () => {
|
||||
(auth.config as ConfigInternal).emulator = {
|
||||
url: 'http://localhost:5000/'
|
||||
};
|
||||
expect(_getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq(
|
||||
expect(await _getFinalTarget(auth, 'host', '/path', 'query=test')).to.eq(
|
||||
'http://localhost:5000/host/path?query=test'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -31,6 +31,8 @@ import { AuthInternal, ConfigInternal } from '../model/auth';
|
||||
import { IdTokenResponse, TaggedWithTokenResponse } from '../model/id_token';
|
||||
import { IdTokenMfaResponse } from './authentication/mfa';
|
||||
import { SERVER_ERROR_MAP, ServerError, ServerErrorMap } from './errors';
|
||||
import { PersistenceType } from '../core/persistence';
|
||||
import { CookiePersistence } from '../platform_browser/persistence/cookie_storage';
|
||||
|
||||
export const enum HttpMethod {
|
||||
POST = 'POST',
|
||||
@@ -73,6 +75,15 @@ export const enum Endpoint {
|
||||
REVOKE_TOKEN = '/v2/accounts:revokeToken'
|
||||
}
|
||||
|
||||
const CookieAuthProxiedEndpoints: string[] = [
|
||||
Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN,
|
||||
Endpoint.SIGN_IN_WITH_EMAIL_LINK,
|
||||
Endpoint.SIGN_IN_WITH_IDP,
|
||||
Endpoint.SIGN_IN_WITH_PASSWORD,
|
||||
Endpoint.SIGN_IN_WITH_PHONE_NUMBER,
|
||||
Endpoint.TOKEN
|
||||
];
|
||||
|
||||
export const enum RecaptchaClientType {
|
||||
WEB = 'CLIENT_TYPE_WEB',
|
||||
ANDROID = 'CLIENT_TYPE_ANDROID',
|
||||
@@ -167,7 +178,7 @@ export async function _performApiRequest<T, V>(
|
||||
}
|
||||
|
||||
return FetchProvider.fetch()(
|
||||
_getFinalTarget(auth, auth.config.apiHost, path, query),
|
||||
await _getFinalTarget(auth, auth.config.apiHost, path, query),
|
||||
fetchArgs
|
||||
);
|
||||
});
|
||||
@@ -257,19 +268,34 @@ export async function _performSignInRequest<T, V extends IdTokenResponse>(
|
||||
return serverResponse as V;
|
||||
}
|
||||
|
||||
export function _getFinalTarget(
|
||||
export async function _getFinalTarget(
|
||||
auth: Auth,
|
||||
host: string,
|
||||
path: string,
|
||||
query: string
|
||||
): string {
|
||||
): Promise<string> {
|
||||
const base = `${host}${path}?${query}`;
|
||||
|
||||
if (!(auth as AuthInternal).config.emulator) {
|
||||
return `${auth.config.apiScheme}://${base}`;
|
||||
const authInternal = auth as AuthInternal;
|
||||
const finalTarget = authInternal.config.emulator
|
||||
? _emulatorUrl(auth.config as ConfigInternal, base)
|
||||
: `${auth.config.apiScheme}://${base}`;
|
||||
|
||||
// Cookie auth works by MiTMing the signIn and token endpoints from the developer's backend,
|
||||
// saving the idToken and refreshToken into cookies, and then redacting the refreshToken
|
||||
// from the response
|
||||
if (CookieAuthProxiedEndpoints.includes(path)) {
|
||||
// Persistence manager is async, we need to await it. We can't just wait for auth initialized
|
||||
// here since auth initialization calls this function.
|
||||
await authInternal._persistenceManagerAvailable;
|
||||
if (authInternal._getPersistenceType() === PersistenceType.COOKIE) {
|
||||
const cookiePersistence =
|
||||
authInternal._getPersistence() as CookiePersistence;
|
||||
return cookiePersistence._getFinalTarget(finalTarget).toString();
|
||||
}
|
||||
}
|
||||
|
||||
return _emulatorUrl(auth.config as ConfigInternal, base);
|
||||
return finalTarget;
|
||||
}
|
||||
|
||||
export function _parseEnforcementState(
|
||||
|
||||
@@ -120,6 +120,10 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
|
||||
_tenantRecaptchaConfigs: Record<string, RecaptchaConfig> = {};
|
||||
_projectPasswordPolicy: PasswordPolicyInternal | null = null;
|
||||
_tenantPasswordPolicies: Record<string, PasswordPolicyInternal> = {};
|
||||
_resolvePersistenceManagerAvailable:
|
||||
| ((value: void | PromiseLike<void>) => void)
|
||||
| undefined = undefined;
|
||||
_persistenceManagerAvailable: Promise<void>;
|
||||
readonly name: string;
|
||||
|
||||
// Tracks the last notified UID for state change listeners to prevent
|
||||
@@ -139,6 +143,11 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
|
||||
) {
|
||||
this.name = app.name;
|
||||
this.clientVersion = config.sdkClientVersion;
|
||||
// TODO(jamesdaniels) explore less hacky way to do this, cookie authentication needs
|
||||
// persistenceMananger to be available. see _getFinalTarget for more context
|
||||
this._persistenceManagerAvailable = new Promise<void>(
|
||||
resolve => (this._resolvePersistenceManagerAvailable = resolve)
|
||||
);
|
||||
}
|
||||
|
||||
_initializeWithPersistence(
|
||||
@@ -160,6 +169,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
|
||||
this,
|
||||
persistenceHierarchy
|
||||
);
|
||||
this._resolvePersistenceManagerAvailable?.();
|
||||
|
||||
if (this._deleted) {
|
||||
return;
|
||||
@@ -524,10 +534,14 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
|
||||
}
|
||||
}
|
||||
|
||||
_getPersistence(): string {
|
||||
_getPersistenceType(): string {
|
||||
return this.assertedPersistence.persistence.type;
|
||||
}
|
||||
|
||||
_getPersistence(): PersistenceInternal {
|
||||
return this.assertedPersistence.persistence;
|
||||
}
|
||||
|
||||
_updateErrorMap(errorMap: AuthErrorMap): void {
|
||||
this._errorFactory = new ErrorFactory<AuthErrorCode, AuthErrorParams>(
|
||||
'auth',
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('core/auth/initialize', () => {
|
||||
sdkClientVersion: expectedSdkClientVersion,
|
||||
tokenApiHost: 'securetoken.googleapis.com'
|
||||
});
|
||||
expect(auth._getPersistence()).to.eq('NONE');
|
||||
expect(auth._getPersistenceType()).to.eq('NONE');
|
||||
});
|
||||
|
||||
it('should set persistence', async () => {
|
||||
@@ -179,7 +179,7 @@ describe('core/auth/initialize', () => {
|
||||
}) as AuthInternal;
|
||||
await auth._initializationPromise;
|
||||
|
||||
expect(auth._getPersistence()).to.eq('SESSION');
|
||||
expect(auth._getPersistenceType()).to.eq('SESSION');
|
||||
});
|
||||
|
||||
it('should set persistence with fallback', async () => {
|
||||
@@ -188,7 +188,7 @@ describe('core/auth/initialize', () => {
|
||||
}) as AuthInternal;
|
||||
await auth._initializationPromise;
|
||||
|
||||
expect(auth._getPersistence()).to.eq('SESSION');
|
||||
expect(auth._getPersistenceType()).to.eq('SESSION');
|
||||
});
|
||||
|
||||
it('should set resolver', async () => {
|
||||
|
||||
@@ -19,7 +19,8 @@ import { Persistence } from '../../model/public_types';
|
||||
export const enum PersistenceType {
|
||||
SESSION = 'SESSION',
|
||||
LOCAL = 'LOCAL',
|
||||
NONE = 'NONE'
|
||||
NONE = 'NONE',
|
||||
COOKIE = 'COOKIE'
|
||||
}
|
||||
|
||||
export type PersistedBlob = Record<string, unknown>;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getAccountInfo } from '../../api/account_management/account';
|
||||
import { ApiKey, AppName, AuthInternal } from '../../model/auth';
|
||||
import { UserInternal } from '../../model/user';
|
||||
import { PersistedBlob, PersistenceInternal } from '../persistence';
|
||||
@@ -66,8 +67,22 @@ export class PersistenceUserManager {
|
||||
}
|
||||
|
||||
async getCurrentUser(): Promise<UserInternal | null> {
|
||||
const blob = await this.persistence._get<PersistedBlob>(this.fullUserKey);
|
||||
return blob ? UserImpl._fromJSON(this.auth, blob) : null;
|
||||
const blob = await this.persistence._get<PersistedBlob | string>(
|
||||
this.fullUserKey
|
||||
);
|
||||
if (!blob) {
|
||||
return null;
|
||||
}
|
||||
if (typeof blob === 'string') {
|
||||
const response = await getAccountInfo(this.auth, { idToken: blob }).catch(
|
||||
() => undefined
|
||||
);
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
return UserImpl._fromGetAccountInfoResponse(this.auth, response, blob);
|
||||
}
|
||||
return UserImpl._fromJSON(this.auth, blob);
|
||||
}
|
||||
|
||||
removeCurrentUser(): Promise<void> {
|
||||
@@ -140,9 +155,24 @@ export class PersistenceUserManager {
|
||||
// persistence, we will (but only if that persistence supports migration).
|
||||
for (const persistence of persistenceHierarchy) {
|
||||
try {
|
||||
const blob = await persistence._get<PersistedBlob>(key);
|
||||
const blob = await persistence._get<PersistedBlob | string>(key);
|
||||
if (blob) {
|
||||
const user = UserImpl._fromJSON(auth, blob); // throws for unparsable blob (wrong format)
|
||||
let user: UserInternal;
|
||||
if (typeof blob === 'string') {
|
||||
const response = await getAccountInfo(auth, {
|
||||
idToken: blob
|
||||
}).catch(() => undefined);
|
||||
if (!response) {
|
||||
break;
|
||||
}
|
||||
user = await UserImpl._fromGetAccountInfoResponse(
|
||||
auth,
|
||||
response,
|
||||
blob
|
||||
);
|
||||
} else {
|
||||
user = UserImpl._fromJSON(auth, blob); // throws for unparsable blob (wrong format)
|
||||
}
|
||||
if (persistence !== selectedPersistence) {
|
||||
userToMigrate = user;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { UserInternal } from './user';
|
||||
import { ClientPlatform } from '../core/util/version';
|
||||
import { RecaptchaConfig } from '../platform_browser/recaptcha/recaptcha';
|
||||
import { PasswordPolicyInternal } from './password_policy';
|
||||
import { PersistenceInternal } from '../core/persistence';
|
||||
|
||||
export type AppName = string;
|
||||
export type ApiKey = string;
|
||||
@@ -71,6 +72,7 @@ export interface AuthInternal extends Auth {
|
||||
_canInitEmulator: boolean;
|
||||
_isInitialized: boolean;
|
||||
_initializationPromise: Promise<void> | null;
|
||||
_persistenceManagerAvailable: Promise<void>;
|
||||
_updateCurrentUser(user: UserInternal | null): Promise<void>;
|
||||
|
||||
_onStorageEvent(): void;
|
||||
@@ -86,7 +88,8 @@ export interface AuthInternal extends Auth {
|
||||
_key(): string;
|
||||
_startProactiveRefresh(): void;
|
||||
_stopProactiveRefresh(): void;
|
||||
_getPersistence(): string;
|
||||
_getPersistenceType(): string;
|
||||
_getPersistence(): PersistenceInternal;
|
||||
_getRecaptchaConfig(): RecaptchaConfig | null;
|
||||
_getPasswordPolicyInternal(): PasswordPolicyInternal | null;
|
||||
_updatePasswordPolicy(): Promise<void>;
|
||||
|
||||
@@ -341,8 +341,9 @@ export interface Persistence {
|
||||
* - 'SESSION' is used for temporary persistence such as `sessionStorage`.
|
||||
* - 'LOCAL' is used for long term persistence such as `localStorage` or `IndexedDB`.
|
||||
* - 'NONE' is used for in-memory, or no persistence.
|
||||
* - 'COOKIE' is used for cookie persistence, useful for server-side rendering.
|
||||
*/
|
||||
readonly type: 'SESSION' | 'LOCAL' | 'NONE';
|
||||
readonly type: 'SESSION' | 'LOCAL' | 'NONE' | 'COOKIE';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Persistence } from '../../model/public_types';
|
||||
import type { CookieChangeEvent } from 'cookie-store';
|
||||
|
||||
const POLLING_INTERVAL_MS = 1_000;
|
||||
|
||||
import {
|
||||
PersistenceInternal,
|
||||
PersistenceType,
|
||||
PersistenceValue,
|
||||
StorageEventListener
|
||||
} from '../../core/persistence';
|
||||
|
||||
// Pull a cookie value from document.cookie
|
||||
function getDocumentCookie(name: string): string | null {
|
||||
const escapedName = name.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
||||
const matcher = RegExp(`${escapedName}=([^;]+)`);
|
||||
return document.cookie.match(matcher)?.[1] ?? null;
|
||||
}
|
||||
|
||||
// Produce a sanitized cookie name from the persistence key
|
||||
function getCookieName(key: string): string {
|
||||
// __HOST- doesn't work in localhost https://issues.chromium.org/issues/40196122 but it has
|
||||
// desirable security properties, so lets use a different cookie name while in dev-mode.
|
||||
// Already checked isSecureContext in _isAvailable, so if it's http we're hitting local.
|
||||
const isDevMode = window.location.protocol === 'http:';
|
||||
return `${isDevMode ? '__dev_' : '__HOST-'}FIREBASE_${key.split(':')[3]}`;
|
||||
}
|
||||
|
||||
export class CookiePersistence implements PersistenceInternal {
|
||||
static type: 'COOKIE' = 'COOKIE';
|
||||
readonly type = PersistenceType.COOKIE;
|
||||
listenerUnsubscribes: Map<StorageEventListener, () => void> = new Map();
|
||||
|
||||
// used to get the URL to the backend to proxy to
|
||||
_getFinalTarget(originalUrl: string): URL | string {
|
||||
if (typeof window === undefined) {
|
||||
return originalUrl;
|
||||
}
|
||||
const url = new URL(`${window.location.origin}/__cookies__`);
|
||||
url.searchParams.set('finalTarget', originalUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
// To be a usable persistence method in a chain browserCookiePersistence ensures that
|
||||
// prerequisites have been met, namely that we're in a secureContext, navigator and document are
|
||||
// available and cookies are enabled. Not all UAs support these method, so fallback accordingly.
|
||||
async _isAvailable(): Promise<boolean> {
|
||||
if (typeof isSecureContext === 'boolean' && !isSecureContext) {
|
||||
return false;
|
||||
}
|
||||
if (typeof navigator === 'undefined' || typeof document === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
return navigator.cookieEnabled ?? true;
|
||||
}
|
||||
|
||||
// Set should be a noop as we expect middleware to handle this
|
||||
async _set(_key: string, _value: PersistenceValue): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to get the cookie from cookieStore, fallback to document.cookie
|
||||
async _get<T extends PersistenceValue>(key: string): Promise<T | null> {
|
||||
if (!this._isAvailable()) {
|
||||
return null;
|
||||
}
|
||||
const name = getCookieName(key);
|
||||
if (window.cookieStore) {
|
||||
const cookie = await window.cookieStore.get(name);
|
||||
return cookie?.value as T;
|
||||
}
|
||||
return getDocumentCookie(name) as T;
|
||||
}
|
||||
|
||||
// Log out by overriding the idToken with a sentinel value of ""
|
||||
async _remove(key: string): Promise<void> {
|
||||
if (!this._isAvailable()) {
|
||||
return;
|
||||
}
|
||||
// To make sure we don't hit signout over and over again, only do this operation if we need to
|
||||
// with the logout sentinel value of "" this can cause race conditions. Unnecessary set-cookie
|
||||
// headers will reduce CDN hit rates too.
|
||||
const existingValue = await this._get(key);
|
||||
if (!existingValue) {
|
||||
return;
|
||||
}
|
||||
const name = getCookieName(key);
|
||||
document.cookie = `${name}=;Max-Age=34560000;Partitioned;Secure;SameSite=Strict;Path=/;Priority=High`;
|
||||
await fetch(`/__cookies__`, { method: 'DELETE' }).catch(() => undefined);
|
||||
}
|
||||
|
||||
// Listen for cookie changes, both cookieStore and fallback to polling document.cookie
|
||||
_addListener(key: string, listener: StorageEventListener): void {
|
||||
if (!this._isAvailable()) {
|
||||
return;
|
||||
}
|
||||
const name = getCookieName(key);
|
||||
if (window.cookieStore) {
|
||||
const cb = ((event: CookieChangeEvent): void => {
|
||||
const changedCookie = event.changed.find(
|
||||
change => change.name === name
|
||||
);
|
||||
if (changedCookie) {
|
||||
listener(changedCookie.value as PersistenceValue);
|
||||
}
|
||||
const deletedCookie = event.deleted.find(
|
||||
change => change.name === name
|
||||
);
|
||||
if (deletedCookie) {
|
||||
listener(null);
|
||||
}
|
||||
}) as EventListener;
|
||||
const unsubscribe = (): void =>
|
||||
window.cookieStore.removeEventListener('change', cb);
|
||||
this.listenerUnsubscribes.set(listener, unsubscribe);
|
||||
return window.cookieStore.addEventListener('change', cb as EventListener);
|
||||
}
|
||||
let lastValue = getDocumentCookie(name);
|
||||
const interval = setInterval(() => {
|
||||
const currentValue = getDocumentCookie(name);
|
||||
if (currentValue !== lastValue) {
|
||||
listener(currentValue as PersistenceValue | null);
|
||||
lastValue = currentValue;
|
||||
}
|
||||
}, POLLING_INTERVAL_MS);
|
||||
const unsubscribe = (): void => clearInterval(interval);
|
||||
this.listenerUnsubscribes.set(listener, unsubscribe);
|
||||
}
|
||||
|
||||
_removeListener(_key: string, listener: StorageEventListener): void {
|
||||
const unsubscribe = this.listenerUnsubscribes.get(listener);
|
||||
if (!unsubscribe) {
|
||||
return;
|
||||
}
|
||||
unsubscribe();
|
||||
this.listenerUnsubscribes.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of {@link Persistence} of type `COOKIE`, for use on the client side in
|
||||
* applications leveraging hybrid rendering and middleware.
|
||||
*
|
||||
* @remarks This persistence method requires companion middleware to function, such as that provided
|
||||
* by {@link https://firebaseopensource.com/projects/firebaseextended/reactfire/ | ReactFire} for
|
||||
* NextJS.
|
||||
* @beta
|
||||
*/
|
||||
export const browserCookiePersistence: Persistence = CookiePersistence;
|
||||
@@ -81,6 +81,7 @@ class FailClass {
|
||||
|
||||
export const browserLocalPersistence = inMemoryPersistence;
|
||||
export const browserSessionPersistence = inMemoryPersistence;
|
||||
export const browserCookiePersistence = inMemoryPersistence;
|
||||
export const indexedDBLocalPersistence = inMemoryPersistence;
|
||||
export const browserPopupRedirectResolver = NOT_AVAILABLE_ERROR;
|
||||
export const PhoneAuthProvider = FailClass;
|
||||
|
||||
@@ -5875,6 +5875,11 @@ [email protected]:
|
||||
resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
|
||||
|
||||
[email protected]:
|
||||
version "4.0.0-next.4"
|
||||
resolved "https://registry.npmjs.org/cookie-store/-/cookie-store-4.0.0-next.4.tgz#8b13981bfd93e10e30694e9816928f8c478a326b"
|
||||
integrity sha512-RVcIK13cCiAa+rsxAbFhrIThn1eBcgt9WTyLq539zMafDnhdGb6u/O5JdMTC3/pcJVqqHJmctiWxAYPpwT/fxw==
|
||||
|
||||
[email protected]:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||
|
||||
Reference in New Issue
Block a user