---
title: Setting Up Twitch OIDC Login (Backend)
description: "This walks through configuring Twitch as an [OIDC](https://namazustudios.com/glossary/oidc/) provider for the browser-redirect login flow (see [OIDC Login for Thick Clients (Browser Redirect Flow](https://namazustudios.com/docs/namazu-elements-core/user-authentication-sign-in/oidc-login-for-thick-clients-browser-redirect-flow/) for the client-side sequence). It covers registering the app with Twitch and creating the `OidcProviderConfiguration` on the…"
url: "https://namazustudios.com/docs/namazu-elements-core/user-authentication-sign-in/setting-up-twitch-oidc-login-backend/"
updated: "2026-08-13"
category: Namazu Elements Core Features / User Authentication / Sign In
---

# Setting Up Twitch OIDC Login (Backend)

This walks through configuring Twitch as an [OIDC](https://namazustudios.com/glossary/oidc/) provider for the browser-redirect login
flow (see [OIDC Login for Thick Clients (Browser Redirect Flow](https://namazustudios.com/docs/namazu-elements-core/user-authentication-sign-in/oidc-login-for-thick-clients-browser-redirect-flow/) for the client-side sequence). It covers registering the app with Twitch and creating the `OidcProviderConfiguration` on the Elements server.

This is [Distinct](https://namazustudios.com/glossary/distinct/) from the older, direct id_token validation path (`OidcAuthScheme`, seeded
by `DefaultOidcSchemeConfiguration`) which just validates a [JWT](https://namazustudios.com/glossary/jwt/) you already possess.
This one drives the full authorization-code flow: opening Twitch's login page, handling the
redirect, and exchanging the code for tokens.

## Step 1: Determine your Elements callback URL

Before registering anything with Twitch, figure out the exact URL Elements will use as the
OAuth redirect target. It's always:

```
{API_OUTSIDE_URL}/oidc/twitch/callback
```

`API_OUTSIDE_URL` is this server's configured public base URL (the `dev.getelements.elements.api.url`
named config value; defaults to `http://localhost:8080/api/REST` if unset). For a default
local dev setup, the callback URL is:

```
http://localhost:8080/api/rest/oidc/twitch/callback
```

**This must match what you register with Twitch byte-for-byte** including scheme, host, port, path,
and trailing slash all count. Twitch rejects any mismatch with `error=redirect_mismatch`, otherwise.

Alternatively, you can always find the URL base by looking at the host part in the admin URL for your instance of Namazu Elements.

![](https://namazustudios.com/wp-content/uploads/2026/08/image-1024x771.png)

## Step 2: Register an app in the Twitch Developer Console

1. Go to the [Twitch Developer Console](https://dev.twitch.tv) and create a new
   [Application](https://namazustudios.com/glossary/application/).
2. Under **OAuth Redirect URLs**, add the exact callback URL from Step 1.
3. Specify **Category** that best relates to your particular project or game.
4. Select **Client Type** as **Confidential**.
5. Select **Organization** as it applies to your particular project or game.

![](https://namazustudios.com/wp-content/uploads/2026/08/image-1-1024x685.png)

At this point, Twitch will direct you back to the listing of all Applications you've created.

1. Click the **Manage** button on the [Application](https://namazustudios.com/glossary/application/) you just created.
2. If there is no secret yet, then click **"New Secret"**
   - **Note:** Regenerating an old secret will cause existing applications to immediately cease to function. This may cause downtime on an existin [Application](https://namazustudios.com/glossary/application/).

![](https://namazustudios.com/wp-content/uploads/2026/08/image-2-1024x882.png)

## Step 3: Create the provider configuration in Elements

This requires a SUPERUSER [Session](https://namazustudios.com/glossary/session/) in the admin console.

1. In the left sidebar, open the **Auth** category and select **[OIDC](https://namazustudios.com/glossary/oidc/) Providers**.
2. Click **+ Create [OIDC](https://namazustudios.com/glossary/oidc/) Provider** to open the creation dialog.
3. Fill in the fields:
   - **[name](https://namazustudios.com/glossary/name/)**: `twitch` this becomes part of the callback URL (`/OIDC/twitch/callback`) and
     the key used for `User.linkedAccountProfiles`.
   - **[name](https://namazustudios.com/glossary/name/)**: `twitch` this becomes part of the callback URL (`/OIDC/twitch/callback`) and
     the key used for `User.linkedAccountProfiles`.
   - **discoveryUrl**: `https://id.twitch.tv/OAuth2/.well-known/openid-configuration`
   - **clientId**: your Twitch Client [id](https://namazustudios.com/glossary/id/)
   - **clientSecret**: your Twitch Client Secret. This field is never pre-filled; the API
     never echoes a saved secret back, so it always shows blank if you reopen this
     configuration to edit it later. Leave it blank on an edit to keep the existing secret.
   - **scopes**: a tag input. Type `openid`, press Space or Enter, then type
     `User:read:email` and press Space or Enter. Both scopes are required; see below.
   - **redirectUri**: leave **Use built-in Elements redirect** checked so Elements
     auto-computes the callback URL from Step 1. Only uncheck it and enter a URL manually if
     you registered a *different* callback URL with Twitch; if you do, it must match that
     registered value byte-for-byte, same rule as above.
   - **extraAuthorizeParams**: a raw JSON textarea. Paste:
     `json { "claims": "{"id_token":{"email":null,"email_verified":null,"preferred_username":null}}" }`
   - **tokenEndpointAuthMethod**: select **Client Secret Post** from the dropdown. Do not
     leave this at the default **Client Secret Basic** — see below.
   - **successRedirectUrl** / **errorRedirectUrl**: optional, only needed for the
     browser-redirect thick-client flow.
4. **Save.** Elements resolves Twitch's discovery document immediately and auto-provisions the matching `OidcAuthScheme` by issuer no separate manual step needed.

### Field values that matter more than they look like they should

- **`scopes` must include `"openid"`.** Twitch's token endpoint only returns an `id_token` if
  the authorization request included the `openid` scope. Without it, you'll get a `200` from
  the token exchange but no `id_token` in the response, which Elements reports as `Token endpoint response did not contain an id_token`.
- **`tokenEndpointAuthMethod` must be `CLIENT_SECRET_POST`.** This field defaults to
  `CLIENT_SECRET_BASIC` (HTTP Basic auth) if omitted, which is legal per RFC 6749 — but
  Twitch's token endpoint doesn't read credentials from the `Authorization` header at all. If
  left at the default, token exchange fails with `{"status":400,"message":"missing client secret"}` (or `missing client id`) even though the credentials were sent, just in the wrong
  place as far as Twitch is concerned.
- **Getting `email` requires two separate things, not one.** The `User:read:email` scope is a
  *prerequisite* per Twitch's docs, but it is not sufficient by itself — Twitch's id_token only
  ever carries a minimal default claim set (`aud`, `azp`, `exp`, `iat`, `iss`, `sub`) unless you
  *also* request extra claims via the non-standard `claims` authorize parameter, set through
  `extraAuthorizeParams` as shown above. Skip either one and `email` simply won't be in the
  id_token, and Elements will have nothing to link. Elements trusts any `email` claim returned
  by a configured provider as already verified — it does **not** check `email_verified` at all
  (some providers omit that claim, or encode it as a non-boolean type). The example above still
  requests `email_verified` for completeness, but it's informational only; Elements ignores it.

### [Profile](https://namazustudios.com/glossary/profile/) claims: what gets linked onto the `User` record

Beyond `sub` (linked as a `UserUid`) and `email` (linked as a `UserUid` + copied to `User.email`),
Elements also captures standard [OIDC](https://namazustudios.com/glossary/oidc/) `Profile`-scope claims — `given_name`, `family_name`,
`preferred_username`, and others — whenever a provider returns them:

- On a **new anonymous login** (`AnonOidcAuthService`, the flow this doc walks through), any of
  `preferred_username` → `User.preferredUsername`, `given_name` → `User.firstName`, and
  `family_name` → `User.lastName` that are present get set on the new [User](https://namazustudios.com/glossary/user/) directly. On a
  **returning** [User](https://namazustudios.com/glossary/user/), the same claims only fill in a field if it's currently blank — an existing
  value (set by an admin, the [User](https://namazustudios.com/glossary/user/), or an earlier login) is never overwritten.
- Every provider's full set of returned [Profile](https://namazustudios.com/glossary/profile/) claims is also snapshotted, as-is, into
  `User.linkedAccountProfiles`, keyed by the [OIDC](https://namazustudios.com/glossary/oidc/) scheme's `name` — this is a per-provider audit
  trail (visible in the admin console as a breakout view on the [User](https://namazustudios.com/glossary/user/) record), not subject to the
  fill-only-if-blank rule above, and is captured on both new and returning logins, and when
  linking an additional scheme to an already-authenticated [User](https://namazustudios.com/glossary/user/) (`UserOidcAuthService`) — though
  that linking path does **not** touch the flat `preferredUsername`/`firstName`/`lastName` fields,
  only `linkedAccountProfiles`.

For Twitch specifically: request `preferred_username` via the `claims` extra authorize parameter
as shown in Step 3 to get it linked. Twitch has no "real [name](https://namazustudios.com/glossary/name/)" concept in its public API/[OIDC](https://namazustudios.com/glossary/oidc/)
surface (only username/display [name](https://namazustudios.com/glossary/name/)), so `given_name`/`family_name`/`name` will never be present
regardless of configuration. `Profile.displayName` is unrelated to all of this — it's always a
randomly generated [name](https://namazustudios.com/glossary/name/) unless set explicitly via the [Profile](https://namazustudios.com/glossary/profile/) API.

## Step 4: Verify in the admin console

The **[OIDC](https://namazustudios.com/glossary/oidc/) Providers** resource in the admin console (under the Auth category) shows the
saved configuration. `clientSecret` is write-only — it's never echoed back by the API, so the
console always shows it blank on edit; leave it blank when editing to keep the existing
secret, or type a new one to rotate it.

## Step 5: Test the login

Using the thick-client sequence (see [OIDC Login for Thick Clients](https://namazustudios.com/docs/namazu-elements-core/user-authentication-sign-in/oidc-login-for-thick-clients-browser-redirect-flow/) for full detail):

```
curl -X POST http://localhost:8080/api/rest/oidc/session 
  -H 'Content-Type: application/json' 
  -d '{"provider": "twitch"}'
```

Open the returned `authorizeUrl` in a browser, complete the Twitch login, then poll:

```
curl http://localhost:8080/api/rest/oidc/session/{handle}
```

until `status` is `COMPLETE` (with the [Session](https://namazustudios.com/glossary/session/)) or `FAILED`. Two things to know about polling:
`COMPLETE` is only returned once, on the poll that first observes it — a second poll for the same
handle returns HTTP `404`, not another `COMPLETE` body. An unknown or expired handle also returns
`404` rather than a body with `status: EXPIRED`, so a `404` on its own doesn't necessarily mean the
login failed; check whether you'd already consumed a `COMPLETE` response before treating it as one.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| Browser lands on `...?error=redirect_mismatch` | The `redirect_uri` Elements sent doesn't exactly match what's registered in Twitch's console | Compare the exact `redirectUri` on the provider config against Twitch's registered OAuth Redirect URL; they must match byte-for-byte |
| Token exchange fails: `{"status":400,"message":"missing client id"}` or `"missing client secret"` | `tokenEndpointAuthMethod` is `CLIENT_SECRET_BASIC` (the default); Twitch ignores the `Authorization` header | Set `tokenEndpointAuthMethod` to `CLIENT_SECRET_POST` on the provider config |
| `ForbiddenException: Token endpoint response did not contain an id_token` | `scopes` doesn't include `openid` | Add `"openid"` to the provider config's `scopes` |
| `Token exchange failed with status 400` with no other detail returned to the caller | Expected — the actual provider error is intentionally not exposed to the API caller | Check the server logs; the token endpoint's error body is logged at `error` level (`OidcLoginAttemptOperations.exchangeCodeForIdToken`) |
| Logged in, but no email `UserUid` and `User.email` is empty | `email` missing from the id_token — either `User:read:email` scope is missing or the `claims` extra authorize param isn't set (`email_verified` is not required; Elements doesn't check it) | Add `User:read:email` to `scopes` and set `extraAuthorizeParams.claims` as shown above |
| Logged in, but `User.preferredUsername` wasn't set | `preferred_username` missing from the id_token, the [User](https://namazustudios.com/glossary/user/) already had a `preferredUsername` set (fill-only-if-blank, never overwritten), or you're testing the link-account flow rather than login (that path only writes `linkedAccountProfiles`, not the flat field) | Confirm `extraAuthorizeParams.claims` requests `preferred_username` under `id_token`; check `User.linkedAccountProfiles["twitch"]` to see exactly what the token returned |
