Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

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 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, applications:write) gate access to its own management API — see Concepts → Tokens and scopes.

A previous version of this page listed users:read / groups:read as separate scopes. They do not exist — the :read / :write split applies to admin:* only. The groups scope (no suffix) controls whether the groups claim is emitted on user-shaped tokens.

The groups claim#

When the groups scope is requested, ScaiKey emits a groups claim:

json
1
"groups": ["grp_a1b2c3d4e5f6g7h8", "grp_z9y8x7w6v5u4t3s2"]

Two properties make this claim safe to authorize from on the hot path:

  • Stable IDs, not names. Entries are the grp_-prefixed primary keys. Renaming a group does not change what the token says. Display names should be resolved through the directory API at presentation time.
  • Transitively expanded. Direct memberships plus every ancestor group via nested-group containment, walked cycle-safely.

Soft-deleted groups are filtered. The claim is omitted entirely if the user has no group memberships. Re-evaluated on every token mint, including refresh-token exchange. For tighter invalidation, subscribe to the group.member_added / group.member_removed webhook events.

The claim includes both SECURITY and DISTRIBUTION groups. Filter on the consumer side if you only want one type.

Partner-scoped tokens#

Service-to-service tokens (the client_credentials grant) can carry a partner_ids claim listing which partners the token is allowed to act on. Downstream resource servers use this claim to refuse cross-partner requests at the request boundary.

The claim is only emitted when the calling application is partner-scoped:

Application scope partner_ids claim
TENANT (default) omitted — token has no partner restriction
PARTNER, one partner ["prt_partner"]
PARTNER, multi-partner ["prt_a", "prt_b", "prt_c"] (deduped, sorted)
GLOBAL (operator-only) omitted — token is platform-wide

The shape is always an array, even for single-partner tokens, so consumers can handle one and many uniformly. When the claim is absent, the token has no partner restriction — treat it the same as the legacy unscoped behavior. Multi-partner is configured on the application itself (partner_id + allowed_partner_ids); only super-admins can grant additional partners.

The created_at claim#

User-shaped tokens (authorization_code, refresh_token, and federation flows) carry a created_at claim — Unix timestamp of when the user account was first created.

json
1
"created_at": 1777035640

Used by registration-flow consumers to distinguish a freshly-registered user from a returning user without an admin-API round-trip. now - created_at < ~5 min is a near-zero-false-positive "just signed up" check. Stable across the token's lifetime; the same claim value appears on every token a user ever mints, so it's safe to use as a registration-tracking signal exactly once at first sight.

Authorize-endpoint parameters (extensions)#

In addition to the standard OIDC parameters, ScaiKey honours:

  • idp_hint=<idp_id> — skip the IdP picker and route straight to a federated login. See Federation.
  • login_hint=<email> — pre-fill the email field on the login page. Standard OIDC.
  • login_hint_required=trueScaiKey-specific extension. When set alongside login_hint, the email field is rendered disabled so a user can't substitute a different address by typing into the form. This is a UX assist, not a security boundary. ScaiKey does not currently enforce the pin server-side at the credential-verify step — a client with JavaScript disabled, or a hand-crafted POST to /auth/login, can still authenticate with a different email. Callers using login_hint to bind an invitation flow MUST validate the final identity in their callback (e.g. comparing the ID token's email claim against the pinned address) and reject mismatches there. We may add server-side enforcement in a future release; this paragraph will be updated when it lands.
Updated 2026-06-28 17:04:29 View source (.md) rev 3