Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/orange-turtles-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'firebase': minor
'@firebase/auth': minor
---

Adding Persistence.COOKIE for use in frameworks that utilize hybrid rendering
5 changes: 4 additions & 1 deletion common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -596,7 +599,7 @@ export interface PasswordValidationStatus {

// @public
export interface Persistence {
readonly type: 'SESSION' | 'LOCAL' | 'NONE';
readonly type: 'SESSION' | 'LOCAL' | 'NONE' | 'COOKIE';
}

// @public
Expand Down
16 changes: 16 additions & 0 deletions docs-devsite/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions docs-devsite/auth.persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
```
2 changes: 2 additions & 0 deletions packages/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -83,6 +84,7 @@ import { getAuth } from './src/platform_browser';

export {
browserLocalPersistence,
browserCookiePersistence,
browserSessionPersistence,
indexedDBLocalPersistence,
PhoneAuthProvider,
Expand Down
1 change: 1 addition & 0 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI using this dev-dep for types, somewhere else I tried this ponyfill and found it unsuitable for prod as it can't be webpacked.

"rollup": "2.79.2",
"rollup-plugin-sourcemaps": "0.6.3",
"rollup-plugin-typescript2": "0.36.0",
Expand Down
36 changes: 32 additions & 4 deletions packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
* limitations under the License.
*/

import { FirebaseError, isCloudflareWorker, querystring } from '@firebase/util';
import {
FirebaseError,
isCloudflareWorker,
querystring
} from '@firebase/util';

import { AuthErrorCode, NamedErrorParams } from '../core/errors';
import {
Expand All @@ -31,6 +35,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',
Expand Down Expand Up @@ -73,6 +79,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',
Expand Down Expand Up @@ -265,11 +280,24 @@ export function _getFinalTarget(
): 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 (
authInternal._getPersistenceType() === PersistenceType.COOKIE &&
CookieAuthProxiedEndpoints.includes(path)
) {
const cookiePersistence =
authInternal._getPersistence() as CookiePersistence;
return cookiePersistence._getFinalTarget(finalTarget).toString();
}

return _emulatorUrl(auth.config as ConfigInternal, base);
return finalTarget;
}

export function _parseEnforcementState(
Expand Down
6 changes: 5 additions & 1 deletion packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,10 +524,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',
Expand Down
6 changes: 3 additions & 3 deletions packages/auth/src/core/auth/initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/core/persistence/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
38 changes: 34 additions & 4 deletions packages/auth/src/core/persistence/persistence_user_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/auth/src/model/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -86,7 +87,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>;
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/model/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

/**
Expand Down
Loading
Loading