---
audience: developer
summary: OIDC sign-in, RFC 8693 token exchange, refresh tokens, and the mock-mode
  escape hatch.
title: Authenticate with ScaiKey
path: tutorials/developer/authenticate-with-scaikey
status: published
---

# Authenticate with ScaiKey

ScaiWave uses **ScaiKey** (the ScaiLabs OIDC provider) for identity.
Every authenticated request to ScaiWave carries a Bearer token; the
token is a ScaiKey-issued JWT whose claims tell ScaiWave who you
are and what tenant you belong to.

## The auth modes

ScaiWave supports three:

- **`mock`** — for dev. ScaiWave returns a hard-coded test user;
  every Bearer token is accepted as long as it's non-empty. Set
  `SCAIWAVE_AUTH_MODE=mock`.
- **`scaikey`** — production. ScaiWave validates tokens against
  ScaiKey's JWKS and resolves the tenant from the `tenant_id`
  claim.
- **`hybrid`** — accepts both. Used in some staging environments
  where ScaiKey isn't always available.

## Sign in (browser flow)

The OIDC authorization-code flow:

1. Client redirects to `https://scaikey.<your-host>/auth?…&redirect_uri=<scaiwave-host>/v1/auth/login`.
2. User authenticates with ScaiKey.
3. ScaiKey redirects back to ScaiWave with a `code`.
4. ScaiWave POSTs `code` to `/v1/auth/login`, which exchanges it
   for `{access_token, refresh_token, expires_in}`.
5. The client stores the tokens (HttpOnly cookie or memory),
   includes the access token as `Authorization: Bearer <jwt>` on
   every subsequent request.

The `POST /v1/auth/login` endpoint is the only place the code is
exchanged. Don't call ScaiKey directly from the browser.

## Get a token programmatically

For automation:

```bash
curl -X POST "$BASE/v1/auth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password" \
  -d "username=$USER" \
  -d "password=$PASS"
```

Returns `{access_token, refresh_token, expires_in, token_type}`.

> **Not available in all deployments.** Some tenants disable the
> password grant entirely and require browser OAuth. Check with
> your admin.

## Refresh

Access tokens have a short TTL (default 1 hour). When ~80% of TTL
has elapsed, refresh proactively:

```bash
curl -X POST "$BASE/v1/auth/refresh" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "refresh_token=$REFRESH"
```

Returns a new access token (and a rotated refresh token if your
config has rotation enabled).

The ScaiWave web client refreshes automatically when it detects
80% of TTL has passed; you don't need to do anything in the
browser.

## Register

A first-time user needs to be registered against ScaiWave:

```bash
curl -X POST "$BASE/v1/auth/register" \
  -H "Authorization: Bearer $TOKEN"
```

This:

1. Validates the token with ScaiKey.
2. Creates a `Participant` row in ScaiWave's DB if one doesn't exist.
3. Auto-creates a personal workspace for the new user.
4. Returns the participant id.

The web client calls this once after first sign-in. Automation
should call it idempotently before its first real request.

## Token exchange (RFC 8693)

Some plugins (ScaiDrive, ScaiVault, etc.) need a *different* token
scope. ScaiWave uses RFC 8693 token exchange:

1. ScaiWave POSTs to ScaiKey's `/oauth/token` with:
   - `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`
   - `subject_token=<user's ScaiWave token>`
   - `subject_token_type=urn:ietf:params:oauth:token-type:access_token`
   - `requested_token_type=urn:ietf:params:oauth:token-type:jwt`
   - `audience=scaidrive` (or `scaivault`, etc.)
2. ScaiKey returns a new JWT scoped to the requested audience.
3. ScaiWave caches it in Redis (TTL = `expires_in - 30s`).

This happens automatically inside `TokenExchangeService` — you
won't call it directly unless writing a new plugin.

## Service tokens (background tasks)

Background workers (indexers, scheduled summaries) have no
end-user context, so they mint a service token via
`client_credentials` grant. As of 2026-05-16, ScaiKey populates
`sub=client_id` on these tokens so ScaiGrid accepts them — see
[`docs/integrations/scaigrid/service-token-401-response.md`](https://github.com/scailabs/scaiwave/blob/main/docs/integrations/scaigrid/service-token-401-response.md)
in the source repo for the history.

You access these via:

```python
token = await token_exchange.get_service_token(tenant_id)
```

## What's in the JWT

Decode any ScaiWave token (don't verify) to see the claims:

```json
{
  "sub": "<user-id-on-scaikey>",
  "tenant_id": "<scaikey-tenant-id>",
  "aud": "scaiwave",
  "preferred_username": "alice",
  "email": "alice@example.com",
  "roles": ["docs_internal", "tenant_admin"],
  "groups": ["group-eng", "group-sre"],
  "exp": 1778939467,
  "iat": 1778935867
}
```

ScaiWave's `TenantMiddleware` reads `tenant_id` on every request
and resolves it to a `Tenant` row.

## Where to go next

- [Your first REST API call](/docs/scaiwave/tutorials/developer/first-rest-api-call).
- API: [Auth](/docs/scaiwave/reference/api/auth).
- [Concepts: Multi-tenancy](/docs/scaiwave/concepts/multi-tenancy).
