diff --git a/common/api-review/auth.api.md b/common/api-review/auth.api.md index aed0fb619da..27203e95f3a 100644 --- a/common/api-review/auth.api.md +++ b/common/api-review/auth.api.md @@ -95,6 +95,7 @@ export interface Auth { setPersistence(persistence: Persistence): Promise; readonly settings: AuthSettings; signOut(): Promise; + readonly tenantConfig?: TenantConfig; tenantId: string | null; updateCurrentUser(user: User | null): Promise; useDeviceLanguage(): void; diff --git a/docs-devsite/auth.auth.md b/docs-devsite/auth.auth.md index cbbc7a9ceb0..1f96bd23881 100644 --- a/docs-devsite/auth.auth.md +++ b/docs-devsite/auth.auth.md @@ -31,6 +31,7 @@ export interface Auth | [languageCode](./auth.auth.md#authlanguagecode) | string \| null | The [Auth](./auth.auth.md#auth_interface) instance's language code. | | [name](./auth.auth.md#authname) | string | The name of the app associated with the Auth service instance. | | [settings](./auth.auth.md#authsettings) | [AuthSettings](./auth.authsettings.md#authsettings_interface) | The [Auth](./auth.auth.md#auth_interface) instance's settings. | +| [tenantConfig](./auth.auth.md#authtenantconfig) | [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface) | The [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface) used to initialize a Regional Auth. This is only present if regional auth is initialized and backend endpoint is used. | | [tenantId](./auth.auth.md#authtenantid) | string \| null | The [Auth](./auth.auth.md#auth_interface) instance's tenant ID. | ## Methods @@ -120,6 +121,16 @@ This is used to edit/read configuration related options such as app verification readonly settings: AuthSettings; ``` +## Auth.tenantConfig + +The [TenantConfig](./auth.tenantconfig.md#tenantconfig_interface) used to initialize a Regional Auth. This is only present if regional auth is initialized and backend endpoint is used. + +Signature: + +```typescript +readonly tenantConfig?: TenantConfig; +``` + ## Auth.tenantId The [Auth](./auth.auth.md#auth_interface) instance's tenant ID. diff --git a/packages/auth/src/api/index.test.ts b/packages/auth/src/api/index.test.ts index 02042fce429..87f674807c0 100644 --- a/packages/auth/src/api/index.test.ts +++ b/packages/auth/src/api/index.test.ts @@ -25,7 +25,11 @@ import { FirebaseError, getUA } from '@firebase/util'; import * as utils from '@firebase/util'; import { mockEndpoint } from '../../test/helpers/api/helper'; -import { testAuth, TestAuth } from '../../test/helpers/mock_auth'; +import { + regionalTestAuth, + testAuth, + TestAuth +} from '../../test/helpers/mock_auth'; import * as mockFetch from '../../test/helpers/mock_fetch'; import { AuthErrorCode } from '../core/errors'; import { ConfigInternal } from '../model/auth'; @@ -34,6 +38,7 @@ import { _performApiRequest, DEFAULT_API_TIMEOUT_MS, Endpoint, + RegionalEndpoint, HttpHeader, HttpMethod, _addTidIfNecessary @@ -55,9 +60,11 @@ describe('api/_performApiRequest', () => { }; let auth: TestAuth; + let regionalAuth: TestAuth; beforeEach(async () => { auth = await testAuth(); + regionalAuth = await regionalTestAuth(); }); afterEach(() => { @@ -595,4 +602,34 @@ describe('api/_performApiRequest', () => { .and.not.have.property('tenantId'); }); }); + + context('throws Operation not allowed exception', () => { + it('when tenantConfig is not initialized and Regional Endpoint is used', async () => { + await expect( + _performApiRequest( + auth, + HttpMethod.POST, + RegionalEndpoint.EXCHANGE_TOKEN, + request + ) + ).to.be.rejectedWith( + FirebaseError, + 'Firebase: Operations not allowed for the auth object initialized. (auth/operation-not-allowed).' + ); + }); + + it('when tenantConfig is initialized and default Endpoint is used', async () => { + await expect( + _performApiRequest( + regionalAuth, + HttpMethod.POST, + Endpoint.SIGN_UP, + request + ) + ).to.be.rejectedWith( + FirebaseError, + 'Firebase: Operations not allowed for the auth object initialized. (auth/operation-not-allowed).' + ); + }); + }); }); diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index af9b3c63bf1..a1480803449 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -26,6 +26,7 @@ import { AuthErrorCode, NamedErrorParams } from '../core/errors'; import { _createError, _errorWithCustomMessage, + _operationNotSupportedForInitializedAuthInstance, _fail } from '../core/util/assert'; import { Delay } from '../core/util/delay'; @@ -53,7 +54,7 @@ export const enum HttpHeader { X_FIREBASE_APP_CHECK = 'X-Firebase-AppCheck' } -export const enum Endpoint { +export enum Endpoint { CREATE_AUTH_URI = '/v1/accounts:createAuthUri', DELETE_ACCOUNT = '/v1/accounts:delete', RESET_PASSWORD = '/v1/accounts:resetPassword', @@ -80,6 +81,10 @@ export const enum Endpoint { REVOKE_TOKEN = '/v2/accounts:revokeToken' } +export enum RegionalEndpoint { + EXCHANGE_TOKEN = 'v2/${body.parent}:exchangeOidcToken' +} + const CookieAuthProxiedEndpoints: string[] = [ Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, Endpoint.SIGN_IN_WITH_EMAIL_LINK, @@ -139,10 +144,11 @@ export function _addTidIfNecessary( export async function _performApiRequest( auth: Auth, method: HttpMethod, - path: Endpoint, + path: Endpoint | RegionalEndpoint, request?: T, customErrorMap: Partial> = {} ): Promise { + _assertValidEndpointForAuth(auth, path); return _performFetchWithErrorHandling(auth, customErrorMap, async () => { let body = {}; let params = {}; @@ -322,6 +328,22 @@ export function _parseEnforcementState( } } +function _assertValidEndpointForAuth( + auth: Auth, + path: Endpoint | RegionalEndpoint +): void { + if ( + !auth.tenantConfig && + Object.values(RegionalEndpoint).includes(path as RegionalEndpoint) + ) { + throw _operationNotSupportedForInitializedAuthInstance(auth); + } + + if (auth.tenantConfig && Object.values(Endpoint).includes(path as Endpoint)) { + throw _operationNotSupportedForInitializedAuthInstance(auth); + } +} + class NetworkTimeout { // Node timers and browser timers are fundamentally incompatible, but we // don't care about the value here diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index 14c23ae28f3..d21cfdd0214 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -36,7 +36,8 @@ import { ErrorFn, NextFn, Unsubscribe, - PasswordValidationStatus + PasswordValidationStatus, + TenantConfig } from '../../model/public_types'; import { createSubscribe, @@ -126,6 +127,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | undefined = undefined; _persistenceManagerAvailable: Promise; readonly name: string; + readonly tenantConfig?: TenantConfig; // Tracks the last notified UID for state change listeners to prevent // repeated calls to the callbacks. Undefined means it's never been @@ -140,7 +142,8 @@ export class AuthImpl implements AuthInternal, _FirebaseService { public readonly app: FirebaseApp, private readonly heartbeatServiceProvider: Provider<'heartbeat'>, private readonly appCheckServiceProvider: Provider, - public readonly config: ConfigInternal + public readonly config: ConfigInternal, + tenantConfig?: TenantConfig ) { this.name = app.name; this.clientVersion = config.sdkClientVersion; @@ -149,6 +152,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { this._persistenceManagerAvailable = new Promise( resolve => (this._resolvePersistenceManagerAvailable = resolve) ); + this.tenantConfig = tenantConfig; } _initializeWithPersistence( diff --git a/packages/auth/src/core/auth/register.ts b/packages/auth/src/core/auth/register.ts index 52493945abc..efe1d63ab16 100644 --- a/packages/auth/src/core/auth/register.ts +++ b/packages/auth/src/core/auth/register.ts @@ -92,7 +92,8 @@ export function registerAuth(clientPlatform: ClientPlatform): void { app, heartbeatServiceProvider, appCheckServiceProvider, - config + config, + tenantConfig ); _initializeAuthInstance(authInstance, deps); diff --git a/packages/auth/src/core/util/assert.ts b/packages/auth/src/core/util/assert.ts index 51dff0793e2..eb497fba8aa 100644 --- a/packages/auth/src/core/util/assert.ts +++ b/packages/auth/src/core/util/assert.ts @@ -112,6 +112,16 @@ export function _serverAppCurrentUserOperationNotSupportedError( ); } +export function _operationNotSupportedForInitializedAuthInstance( + auth: Auth +): FirebaseError { + return _errorWithCustomMessage( + auth, + AuthErrorCode.OPERATION_NOT_ALLOWED, + 'Operations not allowed for the auth object initialized.' + ); +} + export function _assertInstanceOf( auth: Auth, object: object, diff --git a/packages/auth/src/model/auth.ts b/packages/auth/src/model/auth.ts index a88430fd5df..60faedef4e6 100644 --- a/packages/auth/src/model/auth.ts +++ b/packages/auth/src/model/auth.ts @@ -23,6 +23,7 @@ import { PasswordPolicy, PasswordValidationStatus, PopupRedirectResolver, + TenantConfig, User } from './public_types'; import { ErrorFactory } from '@firebase/util'; @@ -100,6 +101,7 @@ export interface AuthInternal extends Auth { readonly name: AppName; readonly config: ConfigInternal; + readonly tenantConfig?: TenantConfig; languageCode: string | null; tenantId: string | null; readonly settings: AuthSettings; diff --git a/packages/auth/src/model/public_types.ts b/packages/auth/src/model/public_types.ts index ea997c7855f..bd6c9cc2b8c 100644 --- a/packages/auth/src/model/public_types.ts +++ b/packages/auth/src/model/public_types.ts @@ -183,6 +183,12 @@ export interface Auth { readonly name: string; /** The {@link Config} used to initialize this instance. */ readonly config: Config; + /** + * The {@link TenantConfig} used to initialize a Regional Auth. This is only present + * if regional auth is initialized and {@link DefaultConfig.REGIONAL_API_HOST} + * backend endpoint is used. + */ + readonly tenantConfig?: TenantConfig; /** * Changes the type of persistence on the `Auth` instance. * diff --git a/packages/auth/test/helpers/mock_auth.ts b/packages/auth/test/helpers/mock_auth.ts index e5e30fa1384..15c03dc42c1 100644 --- a/packages/auth/test/helpers/mock_auth.ts +++ b/packages/auth/test/helpers/mock_auth.ts @@ -116,6 +116,26 @@ export async function testAuth( return auth; } +export async function regionalTestAuth(): Promise { + const tenantConfig = { 'location': 'us', 'tenantId': 'tenant-1' }; + const auth: TestAuth = new AuthImpl( + FAKE_APP, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER, + FAKE_APP_CHECK_CONTROLLER_PROVIDER, + { + apiKey: TEST_KEY, + authDomain: TEST_AUTH_DOMAIN, + apiHost: TEST_HOST, + apiScheme: TEST_SCHEME, + tokenApiHost: TEST_TOKEN_HOST, + clientPlatform: ClientPlatform.BROWSER, + sdkClientVersion: 'testSDK/0.0.0' + }, + tenantConfig + ) as TestAuth; + return auth; +} + export function testUser( auth: AuthInternal, uid: string,