Skip to content

Throw Operation Not Allowed for Invalid Auth Endpoint #9013

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface Auth {
setPersistence(persistence: Persistence): Promise<void>;
readonly settings: AuthSettings;
signOut(): Promise<void>;
readonly tenantConfig?: TenantConfig;
tenantId: string | null;
updateCurrentUser(user: User | null): Promise<void>;
useDeviceLanguage(): void;
Expand Down
11 changes: 11 additions & 0 deletions docs-devsite/auth.auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>Auth</code> 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
Expand Down Expand Up @@ -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.

<b>Signature:</b>

```typescript
readonly tenantConfig?: TenantConfig;
```

## Auth.tenantId

The [Auth](./auth.auth.md#auth_interface) instance's tenant ID.
Expand Down
39 changes: 38 additions & 1 deletion packages/auth/src/api/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +38,7 @@ import {
_performApiRequest,
DEFAULT_API_TIMEOUT_MS,
Endpoint,
RegionalEndpoint,
HttpHeader,
HttpMethod,
_addTidIfNecessary
Expand All @@ -55,9 +60,11 @@ describe('api/_performApiRequest', () => {
};

let auth: TestAuth;
let regionalAuth: TestAuth;

beforeEach(async () => {
auth = await testAuth();
regionalAuth = await regionalTestAuth();
});

afterEach(() => {
Expand Down Expand Up @@ -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<typeof request, typeof serverResponse>(
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<typeof request, typeof serverResponse>(
regionalAuth,
HttpMethod.POST,
Endpoint.SIGN_UP,
request
)
).to.be.rejectedWith(
FirebaseError,
'Firebase: Operations not allowed for the auth object initialized. (auth/operation-not-allowed).'
);
});
});
});
26 changes: 24 additions & 2 deletions packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand All @@ -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,
Expand Down Expand Up @@ -139,10 +144,11 @@ export function _addTidIfNecessary<T extends { tenantId?: string }>(
export async function _performApiRequest<T, V>(
auth: Auth,
method: HttpMethod,
path: Endpoint,
path: Endpoint | RegionalEndpoint,
request?: T,
customErrorMap: Partial<ServerErrorMap<ServerError>> = {}
): Promise<V> {
_assertValidEndpointForAuth(auth, path);
return _performFetchWithErrorHandling(auth, customErrorMap, async () => {
let body = {};
let params = {};
Expand Down Expand Up @@ -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<T> {
// Node timers and browser timers are fundamentally incompatible, but we
// don't care about the value here
Expand Down
8 changes: 6 additions & 2 deletions packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ import {
ErrorFn,
NextFn,
Unsubscribe,
PasswordValidationStatus
PasswordValidationStatus,
TenantConfig
} from '../../model/public_types';
import {
createSubscribe,
Expand Down Expand Up @@ -126,6 +127,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
| undefined = undefined;
_persistenceManagerAvailable: Promise<void>;
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
Expand All @@ -140,7 +142,8 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
public readonly app: FirebaseApp,
private readonly heartbeatServiceProvider: Provider<'heartbeat'>,
private readonly appCheckServiceProvider: Provider<AppCheckInternalComponentName>,
public readonly config: ConfigInternal
public readonly config: ConfigInternal,
tenantConfig?: TenantConfig
) {
this.name = app.name;
this.clientVersion = config.sdkClientVersion;
Expand All @@ -149,6 +152,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
this._persistenceManagerAvailable = new Promise<void>(
resolve => (this._resolvePersistenceManagerAvailable = resolve)
);
this.tenantConfig = tenantConfig;
}

_initializeWithPersistence(
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/core/auth/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export function registerAuth(clientPlatform: ClientPlatform): void {
app,
heartbeatServiceProvider,
appCheckServiceProvider,
config
config,
tenantConfig
);
_initializeAuthInstance(authInstance, deps);

Expand Down
10 changes: 10 additions & 0 deletions packages/auth/src/core/util/assert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/auth/src/model/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
PasswordPolicy,
PasswordValidationStatus,
PopupRedirectResolver,
TenantConfig,
User
} from './public_types';
import { ErrorFactory } from '@firebase/util';
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions packages/auth/src/model/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
20 changes: 20 additions & 0 deletions packages/auth/test/helpers/mock_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ export async function testAuth(
return auth;
}

export async function regionalTestAuth(): Promise<TestAuth> {
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,
Expand Down
Loading