AUTH4D-236 First-party applications

The hosted login asks a player to allow an application after they sign in. A site you run yourself does not need that step, so a browser application can be marked first-party. The setting is off by default, and every application that existed before migration 0036_first_party_applications.sql is off.

The hosted login continues without the consent screen only when all of these hold:

  • the application has firstParty on;
  • it is a browser application;
  • the request’s redirect_uri is https on a public DNS name. Loopback and local hosts (localhost, *.localhost, 127.0.0.0/8, ::1), every IP address, single-label names, reserved names such as .local, .internal and .test, http URIs and custom schemes do not qualify;
  • the redirect URI is not on the auth origin, so a guest-linking flow, which returns to the hosted login itself, is unchanged;
  • the request does not have prompt=consent.

Native and server applications, the device approval page (its confirm-the-code step guards against device-code phishing), guest sign-in (GET /t/{tenantId}/guest/authorize) and guest upgrades are unchanged.

When the screen is skipped, POST /oauth/authorize/continue records the grant exactly as if the player had chosen Allow: the same consent row, with the union of the scopes already granted and the requested scopes. prompt=none, consent revocation, refresh and the console consent views keep working. If the grant adds scopes (a new player, new scopes, or consent that was revoked), the same D1 batch writes a tenant audit event:

Field Value
event oauth.consent.granted
actor_id the player’s user id
target_type client, with target_id the application id
metadata {"consent_screen": "skipped", "scope_count": n}

A returning player whose grant already covers the requested scopes adds no event. Grants made by choosing Allow on the consent screen are recorded as before, without this event.

prompt=consent

GET /oauth/authorize now accepts prompt=consent as well as prompt=none. It always shows the consent screen, also for a first-party application. Other values and lists are still invalid_request. Discovery advertises "prompt_values_supported": ["none", "consent"].

Login context

GET /t/{tenantId}/login/context?client_id=…&flow_id=… adds "skip_consent": true when the flow meets every condition above. The key is absent otherwise, and without flow_id. The hosted login then sends the approve decision itself and shows “Signing in to {application}…”. If that request fails in transit, the page shows the error and a “Try again” button; any other failure shows the expired-session screen, as the consent screen does. A player who already has a hosted-login session also returns at once, so the consent screen’s “Use a different account” link is not offered; an app that wants the player to choose can send prompt=consent.

Management API

PATCH /management/tenants/{tenantId}/clients/{clientId} accepts firstParty (boolean). Only project owners and admins can change it; viewers get 403 insufficient_role. Game (native) applications get 400 first_party_unsupported, with a detail for the firstParty field, and a value that is not a boolean is 400 validation_failed. A server client answers 400 validation_failed: since AUTH4D-305 (repository file docs/api-spec/auth4d-305.md) its PATCH takes only displayName and enabled.

{ "firstParty": true }

GET and PATCH on .../clients/{clientId} return firstParty: true or false for browser applications and null for the others. A change writes the clients.first_party column and a tenant audit event in one D1 batch. A PATCH that does not change the value writes nothing.

Field Value
event management.client.first_party.updated
actor_id the developer
target_type client, with target_id the application id
metadata {"client_type": "browser", "first_party": "enabled"} or "disabled"

Console

The application screen of a browser application has a Consent screen section with a First-party application switch. Its help text says when consent is still shown. Viewers see the switch read-only.

Storage

0036_first_party_applications.sql adds clients.first_party (0 or 1, default 0). It is additive; code deployed before the migration reads a missing column as off.

Tests

  • tests/integration/auth4d-236.first-party.test.ts: the skip with an audited consent row, prompt=none afterwards, loopback and http redirect URIs, prompt=consent, applications that are not first-party, hosted guest sign-in, device approval, the redirect URI rule, and the management API (owners and admins, viewers refused, audit events, game applications refused).
  • tests/e2e/auth4d-236.first-party-login.spec.ts and auth4d-236.first-party-console.spec.ts (tests/e2e/auth4d-236.playwright.config.ts): the hosted login without the consent screen, the retry path, and the console switch by keyboard, read-only for viewers, at 320px and 1280px.

Source: docs/api-spec/auth4d-236.md