Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 4 additions & 1 deletion common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ export function connectAuthEmulator(auth: Auth, url: string, options?: {
disableWarnings: boolean;
}): void;

// @public
export const cookiePersistence: Persistence;

// @public
export function createUserWithEmailAndPassword(auth: Auth, email: string, password: string): Promise<UserCredential>;

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
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 { cookiePersistence } 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,
cookiePersistence,
browserSessionPersistence,
indexedDBLocalPersistence,
PhoneAuthProvider,
Expand Down
20 changes: 17 additions & 3 deletions packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ 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';

export const enum HttpMethod {
POST = 'POST',
Expand Down Expand Up @@ -241,6 +242,9 @@ export async function _performSignInRequest<T, V extends IdTokenResponse>(
request?: T,
customErrorMap: Partial<ServerErrorMap<ServerError>> = {}
): Promise<V> {
// TODO(jamedaniels) if auth persistence is cookie, proxy through the server endpoint
// Q, do we want to allow signIn/Out to work normally on the server if cookie
// persistence is set and FirebaseServerApp authIdToken is not?
const serverResponse = await _performApiRequest<T, V | IdTokenMfaResponse>(
auth,
method,
Expand All @@ -265,11 +269,21 @@ export function _getFinalTarget(
): string {
const base = `${host}${path}?${query}`;

if (!(auth as AuthInternal).config.emulator) {
return `${auth.config.apiScheme}://${base}`;
const finalTarget = (auth as AuthInternal).config.emulator
? _emulatorUrl(auth.config as ConfigInternal, base)
: `${auth.config.apiScheme}://${base}`;

// TODO get the exchange URL from the persistence method
// don't use startsWith v1/accounts...
if (
(auth as AuthInternal)._getPersistence() === PersistenceType.COOKIE &&
(path.startsWith('/v1/accounts:signIn') || path === Endpoint.TOKEN)
) {
const params = new URLSearchParams({ finalTarget });
return `${window.location.origin}/__cookies__?${params.toString()}`;
}

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

export function _parseEnforcementState(
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
}

async _getAppCheckToken(): Promise<string | undefined> {
// @ts-ignore
if (_isFirebaseServerApp(this.app) && this.app.settings.appCheckToken) {
return this.app.settings.appCheckToken;
}
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
32 changes: 28 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,21 @@
}

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 });
return await UserImpl._fromGetAccountInfoResponse(

Check failure on line 78 in packages/auth/src/core/persistence/persistence_user_manager.ts

View workflow job for this annotation

GitHub Actions / Lint

Redundant use of `await` on a return value
this.auth,
response,
blob
);
}
return UserImpl._fromJSON(this.auth, blob);
}

removeCurrentUser(): Promise<void> {
Expand Down Expand Up @@ -140,9 +154,19 @@
// 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 });
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
2 changes: 2 additions & 0 deletions packages/auth/src/core/user/user_credential_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export class UserCredentialImpl
this.operationType = params.operationType;
}

// TODO(jamesdaniels) fetch the user credential from the cookie and response returned from the
// proxy endpoint
static async _fromIdTokenResponse(
auth: AuthInternal,
operationType: OperationType,
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 cookies, useful for server-side rendering.
*/
readonly type: 'SESSION' | 'LOCAL' | 'NONE';
readonly type: 'SESSION' | 'LOCAL' | 'NONE' | 'COOKIE';
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/model/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export interface UserInternal extends User {

auth: AuthInternal;
providerId: ProviderId.FIREBASE;
// TODO(jamesdaniels): refreshToken should either be optional or a sentinel value for COOKIE
// persistence, if refresh token has an identifier maybe that?
refreshToken: string;
emailVerified: boolean;
tenantId: string | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @license
* Copyright 2019 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 {
PersistenceInternal,
PersistenceType,
PersistenceValue,
StorageEventListener
} from '../../core/persistence';

export class CookiePersistence implements PersistenceInternal {
static type: 'COOKIE' = 'COOKIE';
readonly type = PersistenceType.COOKIE;
listeners: Map<StorageEventListener, (e: any) => void> = new Map();

Check failure on line 30 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

async _isAvailable(): Promise<boolean> {
return navigator.hasOwnProperty('cookieEnabled') ?
navigator.cookieEnabled :
true;
}

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

async _get<T extends PersistenceValue>(key: string): Promise<T | null> {
const cookie = await (window as any).cookieStore.get(key);

Check failure on line 43 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
return cookie?.value;
}

async _remove(key: string): Promise<void> {
const cookie = await (window as any).cookieStore.get(key);

Check failure on line 48 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
if (!cookie) {
return;
}
await (window as any).cookieStore.set({ ...cookie, value: "" });

Check failure on line 52 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
await fetch(`/__cookies__`, { method: 'DELETE' }).catch(() => undefined);
}

_addListener(_key: string, _listener: StorageEventListener): void {
// TODO fallback to polling if cookieStore is not available
const cb = (event: any) => {

Check failure on line 58 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

Check failure on line 58 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Missing return type on function
const cookie = event.changed.find((change: any) => change.name === _key);

Check failure on line 59 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
if (cookie) {
_listener(cookie.value);
}
};
this.listeners.set(_listener, cb);
(window as any).cookieStore.addEventListener('change', cb);

Check failure on line 65 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
}

_removeListener(_key: string, _listener: StorageEventListener): void {
const cb = this.listeners.get(_listener);
if (!cb) {
return;
}
(window as any).cookieStore.removeEventListener('change', cb);

Check failure on line 73 in packages/auth/src/platform_browser/persistence/cookie_storage.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
}
}

/**
* An implementation of {@link Persistence} of type 'NONE'.
*
* @public
*/
export const cookiePersistence: Persistence = CookiePersistence;
1 change: 1 addition & 0 deletions packages/auth/src/platform_node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class FailClass {

export const browserLocalPersistence = inMemoryPersistence;
export const browserSessionPersistence = inMemoryPersistence;
export const cookiePersistence = inMemoryPersistence;
export const indexedDBLocalPersistence = inMemoryPersistence;
export const browserPopupRedirectResolver = NOT_AVAILABLE_ERROR;
export const PhoneAuthProvider = FailClass;
Expand Down
2 changes: 1 addition & 1 deletion packages/rules-unit-testing/api-extractor.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "../../config/api-extractor.json",
// Point it to your entry point d.ts file.
"mainEntryPointFilePath": "<projectFolder>/dist/rules-unit-testing/index.d.ts"
"mainEntryPointFilePath": "<projectFolder>/dist/index.d.ts"
}
Loading