---
title: Authentication
path: concepts/authentication
status: published
---

# Authentication

ScaiDNS supports two authentication methods: **JWT tokens** issued by [ScaiKey](https://scaikey.scailabs.ai) for interactive users, and **API keys** for machine clients. Every API request uses one or the other.

Use API keys for servers, CI, and automation. Use JWTs when a human is in the loop.

## API keys

API keys are the simplest option and recommended for any non-interactive caller.

### Create a key

From the admin UI: **API Keys → Create**. A key needs:

- **Name.** A label shown in listings and audit logs. Pick something specific.
- **Permission source.** A user or a group. The key inherits permissions from whichever you pick. This is the mechanism that gives the key any access at all.
- **Optional rate limit.** Requests per minute. Omit for unlimited (subject to platform limits).
- **Optional IP whitelist.** CIDR blocks. Requests from outside are rejected with `403`.
- **Optional expiry.** Timestamp after which the key is refused. Omit for no expiry.

Creation returns the full secret exactly once:

```json
{
  "id": "key_a1b2c3d4",
  "name": "ci-production",
  "key": "sdk_live_f7a9...",
  "permission_source": "user",
  "permission_source_id": "u_...",
  "created_at": "2026-04-23T10:00:00Z"
}
```

Store `key` in a secret manager immediately. ScaiDNS does not store it in plaintext — you cannot retrieve it later. If lost, regenerate via `POST /api/v1/api-keys/{key_id}/regenerate` (invalidates the old secret).

### Use a key

Send the key in the `X-API-Key` header:

```bash
curl https://scaidns.scailabs.ai/api/v1/domains/ \
  -H "X-API-Key: $SCAIDNS_API_KEY"
```

```python
import os, httpx

resp = httpx.get(
    "https://scaidns.scailabs.ai/api/v1/domains/",
    headers={"X-API-Key": os.environ["SCAIDNS_API_KEY"]},
)
```

```typescript
const resp = await fetch("https://scaidns.scailabs.ai/api/v1/domains/", {
  headers: { "X-API-Key": process.env.SCAIDNS_API_KEY! },
});
```

### Revoke vs delete

- **Revoke** (`POST /api/v1/api-keys/{id}/revoke`) disables the key. You can re-activate it later. Use this when the key might come back.
- **Delete** (`DELETE /api/v1/api-keys/{id}`) removes the key permanently. Use this when it's compromised or permanently retired.

## JWT tokens

Human users authenticate through ScaiKey's OAuth 2.0 authorization code flow with PKCE. ScaiDNS itself is the OAuth client, the frontend handles the browser redirect.

The three relevant endpoints:

| Endpoint | Purpose |
|----------|---------|
| `GET /api/v1/auth/config` | OAuth configuration — authorization and token endpoints, client ID, scopes |
| `POST /api/v1/auth/token` | Exchange an authorization code for access + refresh tokens |
| `POST /api/v1/auth/refresh` | Exchange a refresh token for a new access token |

### Full flow

1. **Fetch config.** Call `GET /api/v1/auth/config` to discover the authorization endpoint, client ID, and scopes.

2. **Generate PKCE pair.** Generate a `code_verifier` (random string) and derive `code_challenge = base64url(sha256(code_verifier))`.

3. **Redirect to ScaiKey.** Send the user to `{authorization_endpoint}?response_type=code&client_id=...&redirect_uri=...&scope=...&state=...&code_challenge=...&code_challenge_method=S256`.

4. **Receive callback.** ScaiKey redirects back to your `redirect_uri` with `?code=...&state=...`. Verify `state` matches what you sent.

5. **Exchange for tokens.** `POST /api/v1/auth/token` with the code and verifier:

    ```bash
    curl -X POST https://scaidns.scailabs.ai/api/v1/auth/token \
      -H "Content-Type: application/json" \
      -d '{
        "code": "authcode_from_callback",
        "code_verifier": "your_pkce_verifier",
        "redirect_uri": "https://your-app.example.com/oauth/callback"
      }'
    ```

    Response:

    ```json
    {
      "access_token": "eyJhbGc...",
      "refresh_token": "eyJhbGc...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "id_token": "eyJhbGc..."
    }
    ```

6. **Use the access token.** Send `Authorization: Bearer <access_token>` on every request.

7. **Refresh when expired.** When the access token expires, `POST /api/v1/auth/refresh` with the refresh token. You get a fresh access token.

If you are building a first-party frontend, the reference implementation in the ScaiDNS admin UI demonstrates the full flow — look at `authStore.ts`.

### Using a JWT in requests

```bash
curl https://scaidns.scailabs.ai/api/v1/me \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

```python
resp = httpx.get(
    "https://scaidns.scailabs.ai/api/v1/me",
    headers={"Authorization": f"Bearer {access_token}"},
)
```

```typescript
const resp = await fetch("https://scaidns.scailabs.ai/api/v1/me", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
```

## Scope and tenant

Both authentication methods resolve to a **tenant context**. Every API request operates within a tenant:

- **API keys** are bound to a tenant at creation. The tenant of the key's owning user (or group's tenant) is used.
- **JWT tokens** carry tenant info in their claims. ScaiDNS resolves this to the internal tenant UUID on each request.

You cannot make cross-tenant calls with a non-admin key or token. Platform admins can see and mutate across tenants; see [Permissions and Access](./permissions-and-access.md).

## Errors

| Status | Meaning | Action |
|--------|---------|--------|
| `401 Unauthorized` | No credentials, invalid JWT, or invalid API key | Check the header is set and the key/token hasn't expired |
| `403 Forbidden` | Authenticated, but the caller lacks permission | Check role assignments, domain access grants, or IP whitelist |
| `429 Too Many Requests` | Rate limit exceeded | Back off; inspect key's rate limit config |

## Which should I use?

| Situation | Use |
|-----------|-----|
| A CI job mutating DNS | API key scoped to a service user or group |
| A Terraform provider | API key |
| A first-party frontend with human users | JWT via OAuth |
| A CLI on an operator's laptop | Either — API key for simplicity, JWT if audit needs to show the individual human |
| A one-off admin script | JWT if interactive (they log in once), API key if scheduled |

## What's next

- [Your First Zone](../tutorials/your-first-zone.md) — full walkthrough from zero to signed zone.
- [API Keys reference](../reference/api-keys.md) — every endpoint for managing keys.
- [Permissions and Access](./permissions-and-access.md) — how authorization resolves.
