Skip to content

Authentication

Fireemu serves Firebase Authentication with the Identity Platform features: multi-factor authentication including TOTP, multi-tenancy, OIDC and SAML providers, and blocking functions.

The Authentication emulator listens on 127.0.0.1:9099 by default (--http-port or firebase.json changes it).

// Web SDK
import { getAuth, connectAuthEmulator } from 'firebase/auth';
const auth = getAuth(app);
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });

The Admin SDK reads FIREBASE_AUTH_EMULATOR_HOST. Under fireemu exec it is set to 127.0.0.1:9099 for your command; with fireemu up, set it yourself: FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099. See Connect your app and the environment variables.

Fireemu prints local email action links and SMS verification codes to the daemon’s standard output (and the Logging emulator) as they are issued, so you can complete sign-in and account-management flows without sending real messages. These lines contain credentials for the local emulator accounts; treat captured logs accordingly when sharing test output. A run prints, for example:

auth codes: email action links and SMS codes are printed here as they are issued (they are credentials: do not forward this output to CI or shared logs; auth.logActionCodes = false silences them)
auth: To verify the email address ada@example.test, follow this link: http://127.0.0.1:9099/emulator/action?mode=verifyEmail&lang=en&oobCode=oob-3fb5430a00cf9e1a0002&apiKey=fake-api-key
auth: To reset the password for ada@example.test, follow this link: http://127.0.0.1:9099/emulator/action?mode=resetPassword&lang=en&oobCode=oob-039b95aa9467d4c80003&apiKey=fake-api-key&newPassword=NEW_PASSWORD_HERE
auth: To verify the phone number +15555550100, use the code 551768.

A test reads the same values from the emulator’s REST routes instead of parsing the console. Each returns the codes issued so far for the project:

Terminal window
curl http://127.0.0.1:9099/emulator/v1/projects/demo-shop/oobCodes
# {"oobCodes":[{"email":"ada@example.test","oobCode":"oob-3fb5430a00cf9e1a0002","oobLink":"http://127.0.0.1:9099/emulator/action?mode=verifyEmail&...","requestType":"VERIFY_EMAIL"}, ...]}
curl http://127.0.0.1:9099/emulator/v1/projects/demo-shop/verificationCodes
# {"verificationCodes":[{"code":"551768","phoneNumber":"+15555550100","sessionInfo":"sms-4e2ee8b8ae04bce80004"}]}

auth.logActionCodes = false stops the console lines (for a shared or captured log); the REST routes keep working. --log-verbosity quiet also silences them.

The auth section of fireemu.toml declares the project configuration your application relies on, so a test does not have to set it through the Admin API every time.

Basic sign-in and accounts

What you can configure Settings
Enable or disable email/password, anonymous and phone sign-in auth.signIn
Authorized domains for action-link continue URLs auth.authorizedDomains
Accounts that share an email address auth.signIn.allowDuplicateEmails
Password requirements and enforcement at sign-in auth.passwordPolicy
End-user account creation and deletion auth.client.permissions
Email enumeration protection auth.improvedEmailPrivacy

Advanced security and multi-factor authentication

What you can configure Settings
Multi-factor authentication auth.mfa (and auth.totp in the emulator profile)
Public keys for verifying custom tokens and OIDC ID tokens auth.customTokenSigners, auth.idpSigners
Fixture identity-provider assertions, or signed assertions verified as production does auth.idpAssertions

Enterprise and multi-tenant

What you can configure Settings
Tenants and the multi-tenancy switch auth.tenants, auth.multiTenant
Per-project and per-tenant overrides auth.passwordPolicyOverrides, auth.configOverrides
Blocking functions auth.blockingFunctions
OIDC and SAML providers auth.providers.oidc, auth.providers.saml

Testing and mocks

What you can configure Settings
Test phone numbers and verification codes auth.signIn.phoneNumber.testPhoneNumbers
Unsigned emulator custom tokens (accepted by default; refused with productionExact) auth.allowUnsignedCustomTokens
The unsigned mock ID token fallback in Firestore, Storage and Realtime Database Security Rules (on by default; off with productionExact) auth.allowUnsignedIdTokens
Sign-up quota and local quota-exceeded simulation auth.quota, auth.quotaSimulation
Printing email action links and SMS codes on the console (on by default) auth.logActionCodes

This example enables email/password and phone sign-in, disables anonymous sign-in, requires passwords of at least 12 characters with uppercase, lowercase and numeric characters, and prevents users from deleting their own accounts:

#:schema https://raw.githubusercontent.com/fireemu/fireemu/main/spec/config/fireemu.schema.json
# Configuration schema version: 1.
schemaVersion = 1
# Compatibility profile: "strict" or "emulator".
profile = "strict"
[auth]
# Enable email enumeration protection: true or false.
improvedEmailPrivacy = true
[auth.signIn]
# Allow duplicate sign-in emails: true or false.
allowDuplicateEmails = false
[auth.signIn.email]
# Enable email sign-in: true or false.
enabled = true
# Require a password for email sign-in: true or false.
passwordRequired = true
[auth.signIn.anonymous]
# Enable anonymous sign-in: true or false.
enabled = false
[auth.signIn.phoneNumber]
# Enable phone number sign-in: true or false.
enabled = true
[auth.signIn.phoneNumber.testPhoneNumbers]
# Map a test phone number in E.164 format to a verification code string.
"+16505550101" = "123456"
[auth.passwordPolicy]
# Password policy enforcement: "OFF" or "ENFORCE".
enforcementState = "ENFORCE"
# Require policy upgrades on sign-in: true or false.
forceUpgradeOnSignin = true
[auth.passwordPolicy.constraints]
# Minimum password length: 6 to 30.
minLength = 12
# Require an uppercase character: true or false.
requireUppercase = true
# Require a lowercase character: true or false.
requireLowercase = true
# Require a numeric character: true or false.
requireNumeric = true
[auth.client.permissions]
# Disable client user sign-up: true or false.
disabledUserSignup = false
# Disable client user deletion: true or false.
disabledUserDeletion = true

For an existing fireemu.toml, add the settings you need to its auth section, and adjust the values to match your application’s requirements. See the configuration schema for the format and accepted values of each setting.

Declare auth.authorizedDomains, for example ["localhost", "app.test"], to set the initial project-wide list in either profile.

  • The list replaces the defaults, so include localhost if your tests use it. An empty list authorizes no host under strict; an absent or null setting keeps the existing defaults (localhost and the project’s firebaseapp.com and web.app domains).
  • Validation uses the Admin API’s rules for the selected profile, including its conversion of numeric and boolean entries to strings and omission of null entries; use strings when writing the list.
  • Fireemu intentionally keeps localhost in its defaults for development. The recorded production sandbox baseline does not include it; new production projects may also omit it. This default is a Fireemu-only convenience, and declaring the list replaces it.
  • In the strict profile an action link requested for a tenant checks its continueUrl host against this same project list as it is live (authorized domains belong to the project; a tenant has none of its own), and again when the request commits if a beforeSendEmail function ran. The emulator profile checks no domain. The tenant scope follows the resource model; production recorded this check for the project scope only.
  • In the strict profile GET /v1/projects for a request that names a tenant through the tenantId query reports the same live project list. The Firebase JS SDK checks its origin against this list and sends no tenant on that call. The emulator profile ignores the tenant there, as the official emulator does.
  • An Admin update changes the live list. A session reset restores the declared list, when present, and preserves an Admin-written list when no initial list was declared.
  • Projects created later receive the declaration word for word, independently of the default project’s live list; listing a particular project’s firebaseapp.com or web.app domain therefore gives later projects that same domain.
  • Clearing accounts leaves the live list alone. Auth export/import carries accounts without the domain list or its declaration.
  • The emulator profile accepts the setting and exposes the list, while continuing to allow action-link continue URLs outside it.

Use auth.passwordPolicyOverrides to specify different password policies for individual projects or tenants. Use auth.configOverrides to customize account creation and deletion permissions and email enumeration protection. These settings are separate from creating the project or tenant: naming a tenant in auth.configOverrides or auth.passwordPolicyOverrides does not create it (declare it in auth.tenants to have it created).

Declare project OIDC and SAML resources with auth.providers.oidc and auth.providers.saml, using the Admin v2 resource fields and an ID-only name such as oidc.fixture or saml.fixture. Full resource names are refused. This initializes an OIDC provider in both profiles:

schemaVersion = 1
[[auth.providers.oidc]]
# Provider id, in the form oidc.<id> (a full resource name is refused)
name = "oidc.fixture"
enabled = true
clientId = "local-client"
issuer = "https://issuer.test"
[auth.providers.oidc.responseType]
idToken = true
  • Fixture assertions (the default). Both profiles accept the fixture assertions the official emulator accepts (a JSON or unsigned ID token, a JSON SAML response, the sign-in widget). The emulator profile accepts them whether or not the provider is declared, and the strict profile refuses an oidc.* or saml.* provider that is not declared (or is disabled), as production does.
  • Signed assertions. With auth.idpAssertions set to "signed" (or productionExact, or auth.idpSigners present), strict OIDC sign-in needs matching public issuer keys in auth.idpSigners; no key is fetched. Strict SAML then uses the declared provider certificates and retains its existing assertion checks.
  • Declared arrays replace. Each declared array replaces that provider kind; [] explicitly starts it empty, while an absent or null kind declares no initial resources.
  • Live changes. Admin operations change live resources until a session reset restores the declared kinds. Later session and routed projects inherit the declaration independently of the default project’s live changes. Tenant resources and built-in providers remain separate.
  • Reset, snapshots and export. Clearing accounts preserves live resources; snapshots and Auth export/import transfer no provider resources or declarations.
  • Emulator profile. It continues to accept its unsigned fixture credentials without requiring configured or enabled provider resources.
  • Canonical files only. These declarations require a canonical document with schemaVersion = 1; a Firebase deployment auth.providers section without that version remains deployment data and installs no provider seeds.

The official Authentication emulator supports phone (SMS) second factors only. Fireemu supports TOTP and SMS second factors.

Under strict, TOTP is enabled only through the project’s MFA configuration, as in production. That configuration is on by default (SMS and TOTP enabled), so enrolling a second factor needs no console toggle, Admin API call or auth.mfa block. With productionExact a new project starts with MFA off, as production does, and enrollment answers OPERATION_NOT_ALLOWED until it is enabled. Declare the configuration with auth.mfa, in the shape Identity Platform uses, to mirror your project or to turn MFA off (auth.mfa.state = "DISABLED"); a declared block always decides:

# Configuration schema version: 1.
schemaVersion = 1
# Compatibility profile: "strict" or "emulator".
profile = "strict"
[auth.mfa]
# Project MFA state: "STATE_UNSPECIFIED", "DISABLED", "ENABLED", or "MANDATORY".
state = "ENABLED"
[[auth.mfa.providerConfigs]]
# Provider state: "STATE_UNSPECIFIED", "DISABLED", "ENABLED", or "MANDATORY".
state = "ENABLED"
[auth.mfa.providerConfigs.totpProviderConfig]
# Accepted adjacent TOTP intervals: 0 to 10.
adjacentIntervals = 1
  • The configuration applies when the daemon starts, in both profiles, and to every project created afterward (not the default project’s current value). It is validated as the Admin API validates an update of mfa, and a value production would refuse stops the daemon at startup.
  • Nothing else changes. An Admin API update replaces the current configuration, and disabling MFA refuses TOTP as production does. A tenant follows its own mfaConfig, not this setting. The accepted TOTP window stays auth.totp.windowSteps unless the configuration sets adjacentIntervals.
  • POST /v1/sessions/{session}/reset returns the project to the declared configuration, or to the default (SMS and TOTP enabled) when none is declared. Under productionExact without auth.mfa, a reset leaves a configuration set through the Admin API alone, as it does the rest of the project configuration. DELETE /emulator/v1/projects/{project}/accounts clears accounts only.
  • An export does not carry the project’s mfa configuration, so an imported project keeps its own auth.mfa or Admin-set configuration.
  • The emulator profile keeps auth.totp working. Under strict, auth.totp is enough by default; under productionExact, or with an auth.mfa that leaves TOTP off, it prints a startup warning naming auth.mfa.

A test gets the TOTP secret from the SDK itself: TotpMultiFactorGenerator.generateSecret(session) returns a TotpSecret whose secretKey is the base32 shared secret, with hashingAlgorithm, codeLength and codeIntervalSeconds. The SDK has no function that computes a code, so the test computes the RFC 6238 code with those values, as an authenticator app would. A sign-in hint (resolver.hints) holds the factor’s uid, displayName and enrollmentTime, never the secret.

Enrolling a second factor needs a verified email address, as in production (auth/unverified-email otherwise). The test verifies it with the action link the emulator issued, read from the oobCodes route (Local verification messages). This flow ran against a local fireemu up (0.48.0, strict profile, default MFA configuration) with firebase 12.18.0:

import { initializeApp } from 'firebase/app';
import {
getAuth, connectAuthEmulator, createUserWithEmailAndPassword, sendEmailVerification,
applyActionCode, reauthenticateWithCredential, EmailAuthProvider, multiFactor,
TotpMultiFactorGenerator, signInWithEmailAndPassword, getMultiFactorResolver, signOut,
} from 'firebase/auth';
import { createHmac } from 'node:crypto';
const BASE = 'http://127.0.0.1:9099';
const auth = getAuth(initializeApp({ projectId: 'demo-shop', apiKey: 'fake-api-key' }));
connectAuthEmulator(auth, BASE, { disableWarnings: true });
// RFC 6238 code from a TotpSecret (base32 key, period, digits, algorithm).
function totp(secret, atMs = Date.now()) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const bits = [...secret.secretKey.replace(/=+$/, '')]
.map((c) => alphabet.indexOf(c).toString(2).padStart(5, '0')).join('');
const key = Buffer.from(bits.match(/.{8}/g).map((b) => parseInt(b, 2)));
const counter = Buffer.alloc(8);
counter.writeBigUInt64BE(BigInt(Math.floor(atMs / 1000 / secret.codeIntervalSeconds)));
const h = createHmac(secret.hashingAlgorithm.toLowerCase(), key).update(counter).digest();
const o = h[h.length - 1] & 15;
const n = ((h[o] & 0x7f) << 24) | (h[o + 1] << 16) | (h[o + 2] << 8) | h[o + 3];
return String(n % 10 ** secret.codeLength).padStart(secret.codeLength, '0');
}
const email = 'grace@example.test';
const password = 'demo-pass-123';
const { user } = await createUserWithEmailAndPassword(auth, email, password);
// Verify the email with the action code the emulator issued.
await sendEmailVerification(user);
const { oobCodes } = await (await fetch(`${BASE}/emulator/v1/projects/demo-shop/oobCodes`)).json();
await applyActionCode(auth, oobCodes.findLast((c) => c.email === email && c.requestType === 'VERIFY_EMAIL').oobCode);
await reauthenticateWithCredential(user, EmailAuthProvider.credential(email, password));
// Enroll: the secret comes from the SDK, the code from the helper above.
const secret = await TotpMultiFactorGenerator.generateSecret(await multiFactor(user).getSession());
await multiFactor(user).enroll(
TotpMultiFactorGenerator.assertionForEnrollment(secret, totp(secret)), 'Authenticator');
await signOut(auth);
// The enrollment code used up its time step; sign in with a code from the next one.
const period = secret.codeIntervalSeconds * 1000;
await new Promise((r) => setTimeout(r, period - (Date.now() % period) + 500));
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (error) {
if (error.code !== 'auth/multi-factor-auth-required') throw error;
const resolver = getMultiFactorResolver(auth, error);
const hint = resolver.hints.find((h) => h.factorId === TotpMultiFactorGenerator.FACTOR_ID);
const credential = await resolver.resolveSignIn(
TotpMultiFactorGenerator.assertionForSignIn(hint.uid, totp(secret)));
console.log('signed in as', credential.user.email);
}

Successful TOTP enrollment consumes the accepted time step. Reusing that code to sign in in the same step is refused, so the sample waits for the next step. In a test with virtual time, advance the world clock by the secret’s actual period instead, and generate the sign-in code for that new instant; see the TOTP test recipe.

Multi-tenancy is on by default, so the Admin API creates tenants and auth.tenants loads without enabling anything first. With productionExact a new project starts as production does, with tenants refused until multiTenant.allowTenants is enabled through the Admin API or in the file, and auth.multiTenant.allowTenants: false turns it off in either case. Each entry of auth.tenants is an Admin v2 Tenant document plus its tenantId:

# Configuration schema version: 1.
schemaVersion = 1
# Compatibility profile: "strict" or "emulator".
profile = "strict"
[auth.multiTenant]
# Allow tenants in the project: true or false.
allowTenants = true
[[auth.tenants]]
# Tenant ID: starts with a letter; up to 63 letters, digits, or hyphens.
tenantId = "tenant-a"
# Tenant display name: 4 to 20 letters, digits, or hyphens, starting with a letter.
displayName = "acme"
# Allow password sign-up for this tenant.
allowPasswordSignup = true
[auth.tenants.mfaConfig]
# Tenant multi-factor state.
state = "ENABLED"
# Multi-factor providers enabled for the tenant.
enabledProviders = ["PHONE_SMS"]

By default a tenant ID can be any readable name that starts with a letter and holds letters, digits and hyphens (up to 63 characters), such as tenant-a. With productionExact or auth.tenantIdFormat: "generated", it must have the form production generates: the display name, a hyphen and five characters of a-z0-9, such as acme-x7k2q.

  • A document is read by the code the Admin tenant create route reads one with, so a value production refuses stops the daemon at startup. The tenants are created empty, in file order, and settings such as mfaConfig and passwordPolicyConfig come from the document. The file is stricter than the route in the emulator profile: it also refuses an unknown member and a missing or invalid displayName, as production does.
  • A declared tenant has what an Admin create with that document gives: an omitted allowPasswordSignup, enableEmailLinkSignin or enableAnonymousUser is off and multi-factor is off. A tenant the emulator profile creates on the way for a request that names an unknown id gets the official emulator’s defaults instead (all three on, PHONE_SMS multi-factor), so declare what your test needs.
  • Under the strict profile tenants are created only in a project that allows them, as in production. A new project allows them by default; with productionExact, or with allowTenants: false declared, auth.tenants needs auth.multiTenant.allowTenants: true (the daemon refuses to start otherwise). The emulator profile takes tenants without the switch.
  • The default project and every session project (POST /v1/sessions) start with the declared switch and tenants. POST /v1/sessions/{session}/reset (the default session’s included) creates them again, empty, and returns the switch to the declared value; without these keys a reset leaves an Admin-set switch alone. A namespace the emulator profile creates for an unknown project id (as the official emulator does) starts with the declared switch and tenants too: it is created by the requests that already created one (a user, a provider or a project setting written there) and by a request that writes a tenant there (creating one, or naming one that does not exist yet), not by a read alone. A session created for that project id (POST /v1/sessions) replaces the namespace, tenants included, and starts with the declared ones.
  • With --import, an imported tenant is authoritative for its id, and a declared tenant the import did not carry is still created. If you keep re-importing your own export (--import dir --export-on-exit dir), an edit to a declared tenant is therefore ignored once its exported copy exists: the daemon prints a warning naming the tenant when the imported tenant’s document differs from the declared one (every setting the file can declare is compared). The switch is the other way round: a declared allowTenants wins over the import. After a reset the declared version of a tenant comes back, not the imported one.

In the emulator profile, Fireemu serves Identity Toolkit and Secure Token client requests without an API key or Authorization header as a local extension; such requests cannot implicitly create a missing tenant. Set auth.requireClientApiKey to true to return the official emulator’s 403 for these requests. The default is false; the strict profile is unchanged.

Under the emulator profile, a request with an API key or Authorization header that names a tenant the project does not have creates that tenant, and the number of tenants is not limited, as in the official emulator. strict refuses such a request, as production does. A request that Fireemu refuses (a wrong credential, a browser page without the control token on the emulator routes, App Check, an unknown API key) creates nothing. As in the official emulator, a plain GET without an Origin header to a tenant route of 127.0.0.1 creates the tenant, so any web page you visit while the emulator runs can add tenants to it (not read them); use the strict profile where that matters.

See the compatibility documentation for the supported Admin API scope.

Fireemu accepts a few unsigned tokens by default, so tests need no signing keys. Each default has its own setting, and productionExact switches them all to production’s behavior:

Development default Setting With productionExact
Unsigned custom tokens from the Admin SDK in emulator mode are accepted auth.allowUnsignedCustomTokens Refused; configure auth.customTokenSigners with your service accounts’ public keys
@firebase/rules-unit-testing’s unsigned mock ID token is accepted by Firestore, Storage and Realtime Database Security Rules auth.allowUnsignedIdTokens The mock-token fallback is disabled, as production refuses an unsigned token; fireemu-issued tokens that pass normal verification are still accepted
Identity-provider sign-in accepts fixture assertions and the sign-in widget auth.idpAssertions ("fixture") "signed": OIDC ID tokens must verify with an auth.idpSigners key, and SAML responses with the provider’s certificates

Fixture assertions skip only the signature check. Under strict, sign-in through an oidc.* or saml.* provider is still refused when the provider has no configuration (declare it in auth.providers, or create it through the Admin API) or is disabled, with production’s OPERATION_NOT_ALLOWED : The identity provider configuration is not found. or ... is disabled.. Built-in providers such as google.com sign in without a console configuration; see Other development conveniences.

Unsigned custom tokens minted by the Firebase Admin SDK in emulator mode are accepted by default, including with the recommended strict profile and with auth.customTokenSigners configured. To require signed custom tokens, set auth.allowUnsignedCustomTokens to false and configure auth.customTokenSigners with the service accounts’ public JWK sets. The setting applies only to custom-token sign-in; strict claim checks and signed-token verification still apply. In the emulator profile, setting it to false also disables JSON fake custom tokens.

The unsigned mock ID token of @firebase/rules-unit-testing is accepted by default when Firestore, Storage and Realtime Database Security Rules are evaluated, including with the strict profile. This is the token authenticatedContext() and the client SDKs’ mockUserToken option send (createMockUserToken in @firebase/util).

  • Normal verification runs first. Only after it fails can the mock-token fallback accept a token.
  • Under strict that fallback accepts only the exact SDK shape: a JWT whose header is exactly {"alg":"none","type":"JWT"} and whose signature is empty. request.auth is built from the token’s claims, as the official emulator builds it, when the token names a subject (sub) and its audience (aud) is the project the request goes to. Its exp is not checked, because the SDK sets iat: 0 and exp: 3600 by default.
  • Every other token is verified as before, expiry included. That covers the unsigned ID tokens fireemu itself issues (their header carries typ, not type), and a token that claims a signature that does not verify is still refused.
  • The emulator profile accepts any unsigned token, as the official emulator does, and its Storage also accepts another project’s token.
  • Set auth.allowUnsignedIdTokens to false (in either profile) or set productionExact to disable this fallback. Neither setting refuses fireemu-issued unsigned ID tokens that pass normal verification. A session with auth.idTokenSigning = "session-rsa" accepts no unsigned token.
  • The setting does not affect callable Functions, Identity Toolkit or App Check.

Changing only an issued token’s header to the SDK shape can still pass normal verification of its payload; it is not a refusal test. For a refusal test, use createMockUserToken with its default iat: 0 / exp: 3600, or construct that exact header and empty signature with those times, a subject and the routed project as audience, and run with a clock after expiry (including Firestore’s 30-second allowance).

The Admin SDK’s verifyIdToken and verifySessionCookie inside a function you run here skip signature verification, because the SDK accepts only unsigned tokens whenever FIREBASE_AUTH_EMULATOR_HOST is set (as against the official emulator). A handler that authenticates callers that way therefore accepts a hand-made unsigned token with the right claims, which production refuses, and auth.idTokenSigning = "session-rsa" cannot fix it because the SDK then refuses the signed tokens too. Do not rely on a local verifyIdToken call to prove that forged tokens are refused; test that path against a deployed function.

beforeUserCreated and beforeUserSignedIn (and their v1 forms) run inside the request, as in production. Under strict, beforeEmailSent also runs before a password reset or email sign-in mail. They are registered from the loaded Functions codebase. auth.blockingFunctions is optional: it selects which discovered function handles beforeCreate and beforeSignIn, and which credentials are forwarded to it. Giving its triggers map turns beforeEmailSent off.

To test sign-up quota errors, set auth.quotaSimulation:

schemaVersion = 1
[auth.quotaSimulation]
# Sign-up quota simulation: "off", "observe", or "enforce".
mode = "enforce"
# Default hourly sign-up quota: 0 to 1,000,000.
defaultQuotaPerHour = 5

This is a local fixed-window simulation, not Firebase’s full quota accounting or abuse prevention.

Sign-in flows that talk to external identity providers, hosted authorization-code exchange and live key discovery are outside the supported scope. Signed OIDC and SAML assertions of providers you configure are verified locally; see OIDC and SAML providers. See Limitations and the configuration schema for every setting.