AUTH4D-147 Epic Games sign-in

Epic Account Services documents the OpenID Connect Core authorization code flow for websites, so Epic Games is a fourth preset of the AUTH4D-146 (repository file docs/api-spec/auth4d-146.md) OpenID Connect relying party, named epic. Start, callback, state, PKCE, nonce, proofs, guest linking, device sign-in and project settings all work as AUTH4D-146 describes. This fragment lists only what differs.

Each project registers its own Epic Account Services application and client in the Epic Developer Portal and stores the client ID and secret with auth4.dev. auth4.dev reads no Epic friends, presence, country or other Epic Account Services data.

Preset Issuer accepted in the ID token Scopes Response mode
epic https://api.epicgames.dev/epic/oauth/v2 openid basic_profile query

The endpoints are the ones Epic publishes in https://api.epicgames.dev/epic/oauth/v2/.well-known/openid-configuration:

Endpoint URL
Authorization https://www.epicgames.com/id/authorize
Token https://api.epicgames.dev/epic/oauth/v2/token
JWKS https://api.epicgames.dev/epic/oauth/v2/.well-known/jwks.json

Routes

  • GET /t/{tenantId}/identity/epic/start?client_id={clientId}&browser_flow_id={flowId}
  • GET /t/{tenantId}/identity/epic/callback

Both behave exactly as the Google and Microsoft routes in AUTH4D-146. There is no POST callback.

Differences from the other presets

  • Scopes. basic_profile is required for every Epic Account Services application; openid asks for the ID token. No other Epic permission is requested.
  • Account prompt. Epic gets no prompt parameter. The redirect carries response_type=code, client_id, redirect_uri, scope, state, nonce, code_challenge and code_challenge_method=S256.
  • Client authentication. Epic’s token endpoint takes the client credentials in an HTTP Basic Authorization header (client_secret_basic), not in the form. The form carries grant_type=authorization_code, code, redirect_uri and code_verifier. Epic does not document PKCE; auth4.dev sends the verifier anyway (a server ignores parameters it does not use, RFC 6749 §3.2), and the nonce and the one-time state protect the exchange either way.
  • ID token. The signature must verify with RS256 against Epic’s JWKS, iss must be exactly https://api.epicgames.dev/epic/oauth/v2, aud the project’s Epic client ID, and exp, iat, sub and nonce are checked as for the other presets. Epic’s own claims (appid, dn, pfpid, pfsid, pfdid and the optional cty, eat, eadn and pltfm) are not stored.
  • Identity. The identity key is the ID token sub, the player’s Epic Account ID, stored per tenant as (tenant_id, 'epic', sub). Epic sends no email address. Any email claim is ignored, so an Epic identity never carries one, and the database refuses an Epic identity with an email. A player who signs up with Epic Games starts without an email address and can add one later (AUTH4D-149 (repository file docs/api-spec/auth4d-149.md)). Accounts are never merged.
  • Errors. A cancelled consent arrives as access_denied. Other errors are temporarily_unavailable, as for the other presets.

Proofs and login methods

The proof purpose is epic_login. POST /oauth/authorize/continue, the guest upgrade endpoints, POST /device/sign-in/resume and the passkey registration options accept it exactly like microsoft_login. Guest upgrades report identity.provider epic, and the guest.upgraded flow event reports method epic.

login_methods in GET /t/{tenantId}/login/context and in the login_required answer of GET /device/verify gains an optional epic key, present and true only when Epic Games is enabled and configured:

{ "email": true, "discord": false, "steam": true, "epic": true }

Provider hint

provider_hint=epic on GET /oauth/authorize (AUTH4D-125 (repository file docs/api-spec/auth4d-125.md)) and provider=epic on /login/upgrade offer Epic Games first. The browser SDK sends them with login({ provider: "epic" }) and upgradeGuest({ provider: "epic" }); the Unity package with UpgradeGuestAsync(Auth4Provider.Epic, …). As before, a hint for a provider the application has turned off shows the usual list.

Project settings

PATCH /management/tenants/{tenantId} accepts providers.epic. Every field is optional; unknown fields are 400.

Object Fields
epic enabled, clientId (16 to 64 letters and digits, from the portal’s Clients tab), clientSecret
{
  "providers": {
    "epic": { "enabled": true, "clientId": "xyza7891lhxMVYGCON7LgnKZZ8HQGD5H", "clientSecret": "…" }
  }
}

The rules for first configuration, changing the client ID and enabling are those of AUTH4D-146. The secret is write-only, encrypted under the envelope purpose provider:epic:client_secret and stored in oidc_provider_settings. The project view adds:

{
  "epic": {
    "enabled": true,
    "clientId": "xyza7891lhxMVYGCON7LgnKZZ8HQGD5H",
    "clientSecretConfigured": true,
    "clientSecretUpdatedAt": "2026-10-07T09:00:00.000Z"
  }
}

Each change writes a management.provider.updated audit event with provider epic and status.

Storage

Migration 0025_epic_games.sql rebuilds identities to allow provider = 'epic' with no email, and rebuilds oidc_provider_settings to allow provider = 'epic'. Every stored row, key, index and the password trigger are kept. See docs/operations/d1-schema.md.

Local development and tests

OIDC_LOCAL_MOCK_ORIGIN covers Epic too, with epic as the first path segment, for example http://127.0.0.1:9100/epic/epic/oauth/v2/token. Tests never contact Epic.

  • packages/provider-oidc/src/index.test.ts: the Epic authorization URL, the Basic header, the exact issuer, and that an email claim is dropped.
  • packages/db/src/epic-migration.test.ts: migration 0025 keeps every row and enforces the Epic rules.
  • tests/integration/auth4d-147.epic.test.ts: a local mock of Epic’s token endpoint that requires the Basic header and PKCE. Covers browser sign-in, settings and secrets, the provider hint, guest upgrade and device sign-in.
  • tests/e2e/auth4d-147.*.spec.ts (-c tests/e2e/auth4d-147.playwright.config.ts): the hosted login, provider hint, device page and guest upgrade buttons, and the console card, with a mocked API.
pnpm --filter @auth4/provider-oidc test
cd tests/integration && pnpm exec vitest run auth4d-147.epic.test.ts

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