Plattform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Modelle Tools & Services
Lösungen
Organisationen Entwickler Internet Service Provider Managed Service Provider AI-in-a-Box
Ressourcen
Support Documentation Blog Downloads
Unternehmen
Über uns Forschung Karriere Investieren Kontakt
Anmelden

Integrate an external application with ScaiKey (OIDC / OAuth 2.0)

This is the canonical integration guide. ScaiKey is a standards-compliant OpenID Connect / OAuth 2.0 provider. If your app or its library supports "generic OIDC" or "OAuth 2.0 / OpenID Connect SSO", it works with ScaiKey.

Rule #1 — always start from the discovery document. Do not hardcode endpoint URLs; read them from the well-known document below and let your OIDC library configure itself. Endpoint paths and the issuer value are stable, but discovery is the single source of truth.

Live host: https://scaikey.scailabs.ai (replace with your ScaiKey host if self-hosted).


1. Pick a surface: platform vs tenant#

ScaiKey serves two OIDC "front doors". Choose based on whether your app knows the user's tenant before login.

Platform (recommended default) Tenant-scoped
Use when You don't know the user's tenant — resolve it from their email at login (home-realm discovery) Your app is pinned to exactly one tenant

Paths below are relative to the live host, https://scaikey.scailabs.ai.

  • Platform (recommended default)
    • Discovery: /api/v1/platform/.well-known/openid-configuration
    • issuer: https://scaikey.scailabs.ai/platform
  • Tenant-scoped
    • Discovery: /api/v1/auth/tenants/{slug}/.well-known/openid-configuration
    • issuer: https://scaikey.scailabs.ai/tenants/{slug}

Validate the iss of received tokens against the issuer from the discovery document you used — do not assume the bare host. (Platform tokens carry the /platform suffix; tenant tokens carry /tenants/{slug}.)

The endpoints below are shown for the platform surface (paths relative to the host); for the tenant surface, use the URLs from that tenant's discovery document (they live under /api/v1/auth/tenants/{slug}/…).

Purpose Platform endpoint (path)
authorize /api/v1/platform/oauth/authorize
token /api/v1/platform/oauth/token
userinfo /api/v1/platform/oauth/userinfo
jwks /api/v1/platform/.well-known/jwks.json
end session (logout) /api/v1/platform/oauth/logout
device authorization /api/v1/platform/oauth/device/authorize

2. Register your application#

Registration is operator-driven (there is no public dynamic-registration endpoint). Ask the ScaiKey team — or use the admin API / scaikey CLI — to register an application, choosing a client type:

Type For Secret? Auth
WEB Server-side web apps yes (confidential) client secret + (recommended) PKCE
SPA Browser single-page apps no (public) PKCE required
NATIVE Desktop / mobile no (public) PKCE required, private-use-scheme redirect
SERVICE Machine-to-machine, no user yes client_credentials

You provide, and receive back:

  • Provide: redirect_uris (exact-match, incl. path), post-logout URIs (or rely on same-origin-as-a-redirect-URI matching), the scopes you need.
  • Receive: client_id (and client_secret for WEB/SERVICE, shown once).

Scopes for user login: openid profile email (+ groups for the group claim, offline_access for a refresh token). For directory read/sync use directory:readnot admin:read (that's super-admin). users:read / groups:read do not exist.


3. Authorization Code + PKCE (the login flow)#

Response type is code only (implicit is not supported). Always use PKCE (S256) — it is mandatory for public clients (SPA/NATIVE) and recommended for WEB.

  1. Generate a code_verifier and code_challenge = BASE64URL(SHA256(verifier)).
  2. Redirect the user to the authorize endpoint:
    carbon
    1
    2
    3
    4
    5
    6
    7
    8
    GET https://scaikey.scailabs.ai/api/v1/platform/oauth/authorize
        ?response_type=code
        &client_id=<your client_id>
        &redirect_uri=<one of your registered redirect_uris>
        &scope=openid%20profile%20email
        &state=<csrf-random>
        &code_challenge=<challenge>
        &code_challenge_method=S256
    
  3. User authenticates (email → tenant resolved on the platform surface → password/MFA). ScaiKey redirects back to your redirect_uri with ?code=…&state=…. Verify state matches.
  4. Exchange the code at the token endpoint:
    bash
    1
    2
    3
    4
    5
    6
    7
    8
    curl -X POST https://scaikey.scailabs.ai/api/v1/platform/oauth/token \
      -d grant_type=authorization_code \
      -d code=<code> \
      -d redirect_uri=<same redirect_uri as step 2> \
      -d client_id=<your client_id> \
      -d code_verifier=<verifier> \
      # WEB/SERVICE clients also send: -d client_secret=<secret>
      # (public SPA/NATIVE clients send NO secret)
    
    { access_token, id_token, refresh_token?, token_type, expires_in, scope }.

The redirect_uri must be byte-identical at authorize and token (exact-match, no normalization).


4. Validate tokens#

  • Fetch signing keys from jwks_uri (cache them; honour key rotation).
  • On the ID token, verify: signature (via JWKS), iss == the discovery issuer, aud == your client_id, exp, and nonce if you sent one.
  • Access tokens are self-contained JWTs (same JWKS). Note they are not revocation-checked at validation time — a logged-out user's access token stays valid until exp (default 1 h). Keep access-token lifetime in mind for your own session model.

4a. GLOBAL applications: one app, many issuers (important)#

A GLOBAL application (one registration that serves users from many tenants via the platform realm) does not see a single iss. This is by design, and a relying party cannot discover it from the platform discovery document — so it's stated here explicitly:

  • User tokens are tenant-issued. After home-realm discovery resolves the user to their tenant, the id_token and access_token carry iss = https://scaikey.scailabs.ai/tenants/{slug} — the user's own tenant — even though the flow ran through the platform realm. Each token also carries a tenant_slug claim; verify iss == …/tenants/{tenant_slug} to bind the issuer to the tenant the token claims.
  • Client-credentials tokens are platform-issued. A client_credentials token minted by the same GLOBAL app carries iss = https://scaikey.scailabs.ai/platform (it has no user, so no tenant).
  • The platform discovery document advertises issuer = …/platform. That value matches only the client-credentials tokens, not user logins. A stock OIDC RP configured solely from platform discovery and pinning a single iss will reject every user token. That is the trap; the rule below is the way through it.

The signing keys (JWKS) are global — there is only one key set. Every realm's jwks_uri (platform and every tenant) returns the same keys. So you do not need one JWKS per tenant: fetch one JWKS, verify the signature once, then check the iss string against an allow-list you maintain:

  1. Verify the token signature against the (single, shared) JWKS. Any realm's jwks_uri works; the platform one is a fine choice.
  2. Accept the token only if its iss is in your allow-list: the platform issuer (…/platform, for your own client-credentials tokens) plus the tenant issuer (…/tenants/{slug}) of each tenant your app serves.
  3. For a user token, additionally require iss == …/tenants/{tenant_slug} using the token's own tenant_slug claim.

Restricting which tenants may log in. There is currently no per-tenant allow-list on a GLOBAL application in ScaiKey — any user of any tenant can complete a login unless you enable require_assignment on the app, which limits login to explicitly assigned users/groups (assignments themselves may span tenants). So today the enforcement point for "which tenants" is your RP's iss allow-list (step 2); a first-class per-tenant restriction on the ScaiKey side is a tracked enhancement, not yet available. If you need it, ask the ScaiKey team.

5. UserInfo, refresh, logout#

  • UserInfo: GET …/oauth/userinfo with Authorization: Bearer <access_token>.
  • Refresh: POST …/oauth/token with grant_type=refresh_token, refresh_token=…, client_id (+ secret for confidential). Refresh tokens rotate on use and enforce reuse-detection (a replayed rotated token revokes the whole family). Persist the newest token; don't run concurrent refreshes.
  • RP-initiated logout: redirect to the end_session_endpoint with id_token_hint=<id_token> and (optionally) post_logout_redirect_uri. A post-logout URI is accepted if it exactly matches a registered logout URI or shares scheme+host+port with a registered redirect_uri.

Claims by scope#

  • emailemail, email_verified.
  • profilename, given_name, family_name, preferred_username, picture, locale, zoneinfo, and — when the user has a manager — manager (the manager's ScaiKey user id, usr_…) and manager_display_name. The manager is populated from Active Directory on LDAP-synced tenants, or set by an admin on local tenants; both claims are omitted when no manager is set. (This is the supported way to render, e.g., a manager line in an email signature — read manager_display_name.)
  • groupsgroups (security-group ids), and distribution_groups when present.

6. Machine-to-machine (SERVICE / client_credentials)#

For a service with no user (e.g. directory sync), register a SERVICE/WEB GLOBAL app and:

bash
1
2
3
4
curl -X POST https://scaikey.scailabs.ai/api/v1/platform/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=<id> -d client_secret=<secret> \
  -d scope=directory:read

See also the service-to-service tutorial.


Common pitfalls#

  • Wrong base path. Endpoints live under /api/v1/platform/… or /api/v1/auth/tenants/{slug}/…. There is no api.scaikey.io host and no bare /tenants/{slug}/oauth/… path — read the discovery document.
  • Broken discovery URL. The discovery document is at …/api/v1/platform/.well-known/openid-configuration (or the tenant equivalent), not /.well-known/openid-configuration at the root or under /oauth/.
  • iss mismatch. Validate against the discovery issuer (…/platform or …/tenants/{slug}), not the bare host.
  • Public client sending a secret. SPA/NATIVE clients send no secret; PKCE is the proof.
  • Redirect URI not exact-match. Register every callback exactly, including path.
Updated 2026-09-12 00:44:55 View source (.md) rev 4