AppAIGatewayDocs
ApplicationsAuth policy

User authentication

Tie requests to signed-in users from Firebase, Supabase, Auth0, Clerk or your own issuer, or count each install as a user.

User authentication is the second level of the Auth policy page. It decides whether requests belong to a user and how the gateway knows who that is. Identifying users is what makes per-user limits, per-user usage and blocking possible.

The state line says which choice is active: Signed-in users only, Identity provider not finished, Your backend names the user, Header name missing, Unauthenticated users allowed, or No user identity.

The choices

ChoiceApp typeUser ID
Signed-in users onlyiOS and serverA claim from a verified sign-in token, sub by default
Unauthenticated usersiOSThe App Attest key ID of the install
Your backend sends the user idServerThe value of a header your backend sends
No user identityServerNone

Signed-in users only

Your app sends a signed JWT from its identity provider, with the issuer and audience you configure below. The gateway verifies the signature against the provider's public keys, checks the issuer and audience against what you configured, and takes the user ID from the token.

Pick your provider under Identity provider. The preset fills in the verification settings from the values you enter.

PresetYou enterWhat it configures
Firebase AuthenticationFirebase project id (from Project settings, not the project number)Google's shared key set, issuer https://securetoken.google.com/<project-id>, audience <project-id>
Supabase AuthProject ref (the subdomain of your project URL)Your project's JWKS, issuer https://<ref>.supabase.co/auth/v1, audience authenticated
Auth0Auth0 domain, API identifierYour Auth0 domain's JWKS, issuer https://<domain>/, audience = the API identifier
ClerkFrontend API host, AudienceYour Frontend API JWKS, issuer https://<host>, audience = what your JWT template sets
Custom issuerJWKS URL, Issuer (iss), Audience (aud)Exactly what you enter

Things each provider needs you to know:

  • Firebase signs every project's tokens with one shared key set, so the key set alone identifies nobody. The issuer and audience are what scope the app to your project. Without them a token from any Firebase project in the world would be accepted, which is why both are required.
  • Supabase must be on asymmetric JWT signing keys. A legacy HS256 project publishes no public keys, so every token is rejected.
  • Auth0 puts a trailing slash in iss. The preset writes it. Do not remove it. With the API identifier as audience, send a JWT access token requested for that API, not an ID token whose audience is your client ID. See Get Access Tokens.
  • Clerk's default session token has no aud. Add one in your JWT template, because the gateway requires it. Clerk's own guidance also checks azp against permitted origins; native apps have no origin, so the preset omits it.
  • Sign in with Apple works as a custom issuer: issuer https://appleid.apple.com, audience your bundle ID, JWKS URL https://appleid.apple.com/auth/keys.

Issuer and audience each accept one value or a list, so a migration between two projects or bundle IDs is a configuration edit rather than a cut-over.

A stored configuration missing either value does not run: the console reports it as unfinished, and token exchanges fail until it is fixed.

What the app sends

The app exchanges its proof and the ID token for a gateway token at POST /v1/apps/{app}/auth/token, then uses the gateway token on requests. The Swift package does this for you. See iOS with the Swift package and Server applications.

A refused exchange answers with one of three codes, chosen by what the app should do next:

CodeMeaningThe app should
403 issuer_token_rejectedThe token did not verify: bad signature, expired, wrong issuer or audience, or no user ID claimGet a fresh token from the identity provider, retry once, then fail
403 issuer_claims_missingThe token verified, but a required claim from the subscription check is not there yetWait and retry. Do not sign the user in again
503 issuer_verification_unavailableThe gateway could not reach the provider's key set, so the token was never judgedRetry with backoff

Every outcome is listed on the app's Auth & Errors page.

In the configuration

"end_user": {
  "source": "issuer",
  "issuer": {
    "jwks_url": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
    "issuer": "https://securetoken.google.com/my-app-1a2b3",
    "audience": "my-app-1a2b3",
    "user_id_claim": "sub",
    "required_claims": [],
    "max_token_lifetime_seconds": 86400,
    "provider": "firebase"
  }
}

user_id_claim names the claim used as the user ID. max_token_lifetime_seconds rejects a token whose own lifetime, expiry minus issue time, is longer than that, so a provider misconfigured to issue week-long tokens cannot widen the window. provider and entitlement are bookkeeping for the console so it can reopen the same preset; the gateway ignores them. required_claims is the subscription check.

Unauthenticated users

For an iOS app with no sign-in. The App Attest key identifies the install, so per-install limits, blocks and usage all work with nothing self-reported. The app sends no ID token, and one sent anyway is refused rather than ignored.

The key lives in the Secure Enclave for one install of one app on one device. It does not survive reinstalling the app, clearing its data, or moving to a new phone, so a user can shed the limits and blocks attached to an install by starting over. And there is no token to inspect, so the subscription check is unavailable.

"end_user": { "source": "app_install" }

Your backend sends the user id

For a server that fronts real users and knows who they are. Your backend names the user in a header on every request, and the gateway takes the value on trust.

Header name defaults to x-end-user-id. It is matched case-insensitively and removed before the request reaches the provider. Values are 1 to 128 printable ASCII characters.

The header is required once configured. A request without it is refused with 400 invalid_request, because an app that tolerated its absence would silently meter part of its traffic as nobody.

This belongs only on a server you control. A client an end user runs could send any ID it likes and bypass per-user limits. Block these users in your backend; gateway blocking requires an authenticated user or install.

"end_user": { "source": "header", "header": "x-end-user-id" }

No user identity

For a server with no users to speak of, such as a batch job. Requests record no user, per-user limits cannot be set, and GET /v1/apps/{app}/me answers 404. Use the application limits instead.

end_user is required on an api_key app, so an app with no users says so with the none source. Naming any other source is how an app opts into having users at all.

"end_user": { "source": "none" }

On this page