---
title: OAuth and OIDC
path: concepts/oauth-and-oidc
status: published
---

# OAuth and OIDC

ScaiKey is a standard OpenID Connect provider. If your client library handles OIDC correctly, you mostly need to know two things: which discovery URL to point at, and which grant type fits your use case.

## Discovery URLs

| Use case | Discovery URL |
|---|---|
| `GLOBAL`-scoped app authenticating users across tenants | `$SCAIKEY/api/v1/platform/oauth/.well-known/openid-configuration` |
| `TENANT`-scoped app authenticating a single tenant's users | `$SCAIKEY/api/v1/auth/tenants/{slug}/.well-known/openid-configuration` |

Point your OIDC library at the URL that matches your app's scope. The library reads it once at startup; from then on it knows every endpoint, scope, and signing key URL it needs.

## Supported grants

### `authorization_code` — interactive user login

The standard browser-based flow. User clicks "Log in with ScaiKey", gets redirected to ScaiKey's hosted login page, authenticates (with MFA if required by the tenant), is redirected back to your app with a code, and your backend swaps the code for tokens.

**Required for SPAs and native apps:** PKCE. Public clients (`SPA`, `NATIVE` application types) **must** use `code_challenge` + `code_verifier`. Confidential clients (`WEB`, `SERVICE`) can use PKCE too — recommended.

### `client_credentials` — service-to-service, no user

Your backend exchanges its `client_id` + `client_secret` for an access token. There's no user, no `sub` claim from a person — `sub` is set to the `client_id`. Use this for backend integrations, scheduled jobs, internal tools.

Only `WEB` and `SERVICE` application types can use this grant (they're confidential clients with a secret). `SPA` and `NATIVE` cannot.

### `refresh_token` — extend a session without re-prompting

Standard refresh token grant. ScaiKey rotates refresh tokens on every use (one-time-use refresh tokens). The previous refresh token is invalidated atomically when the new one is issued.

Refresh tokens are issued only when:
- Your app is `WEB` or `NATIVE` (typically need long-lived sessions), or
- The original authorization request included the `offline_access` scope.

### `urn:ietf:params:oauth:grant-type:token-exchange` — RFC 8693

Exchange a token issued by one ScaiKey-registered application for a token audience-restricted to another. Common pattern: a frontend gets a user token for "MyApp", which needs to call "BackendService" — MyApp's server exchanges the user token for a BackendService-audience token before calling.

Constraints:
- Subject token must be a ScaiKey-issued access token (we don't accept foreign tokens).
- The target application must opt in (`token_exchange_allowed = true` on its row).
- Scopes can only narrow, never widen: granted = `subject_scopes ∩ target.allowed_scopes ∩ requested`.
- Self-exchange is rejected.
- The exchanged token's `expires_in` is the **target's** `token_lifetime`, not the subject token's remaining lifetime — exchange effectively "refreshes" the wall clock for the new audience.
- The new token carries an `act` claim recording who performed the exchange (delegation chain is preserved if subject already had an `act`).

See [Reference → OAuth endpoints → Token endpoint](/docs/scaikey/reference/oauth-endpoints) for the exact form parameters.

### `urn:ietf:params:oauth:grant-type:device_code` — RFC 8628

The "TV login" flow. Your device shows a short user code, the user types it in on a separate device's browser to approve, your device polls until approval and gets tokens. Use this for CLI tools, IoT devices, headless setups.

## When to use which

| Situation | Grant |
|---|---|
| Web app with a backend, users log in via browser | `authorization_code` |
| Single-page app (no backend) | `authorization_code` + PKCE |
| Mobile / native desktop app | `authorization_code` + PKCE |
| Backend service calling another backend service | `client_credentials` |
| Backend needs a token audience-restricted to a different downstream | Token Exchange |
| CLI or headless device | `device_code` |
| You already have a session and just need a fresh access token | `refresh_token` |

## What we don't support

- **`password` grant (Resource Owner Password Credentials, ROPC).** Deprecated and dangerous. Don't ask. Use `authorization_code` and direct users to the hosted login page.
- **`implicit` grant.** Deprecated in OAuth 2.1. Use `authorization_code` + PKCE instead.

## Scopes

`openid` is required for OIDC flows. `profile`, `email`, `groups` are the standard claims-bearing scopes. `offline_access` requests a refresh token. ScaiKey-specific admin scopes (`admin:read`, `admin:write`, `users:read`, `groups:read`, etc.) gate access to its own management API — see [Concepts → Tokens and scopes](/docs/scaikey/concepts/tokens-and-scopes).
