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

Authentication

Every ScaiDrive request (except /health and public share-link endpoints) needs a Bearer token issued by ScaiKey. This page covers how to get one and how to keep using it.

Token format#

ScaiDrive tokens are standard RS256- or ES256-signed JWTs issued by ScaiKey. The server validates them on every request by fetching ScaiKey's JWKS and verifying the signature.

Claims ScaiDrive reads:

Claim Meaning
sub User ID (usr_...)
tenant_id Tenant the user belongs to (tnt_...)
partner_id Partner (managed service provider) the tenant belongs to
email, name Profile
groups Array of group IDs (grp_...)
scope Space-separated scopes — see below
iss Must match ScaiKey's URL (or the tenant-scoped issuer {scaikey}/tenants/{tenant_id})
exp Expiration — expired tokens are rejected with 401
act RFC 8693 actor claim. Present when the token reached us through a token-exchange flow (e.g., ScaiSpeak acting on behalf of a user). ScaiDrive reads act.client_id (falling back to act.sub) and records it on every audit event as service_account, so the audit trail names the delegating service alongside the acting user. The audience claim (aud) is not checked — narrow audience at exchange time for ScaiKey's records, but ScaiDrive doesn't gate on it

Scopes#

ScaiDrive recognizes these application scopes:

Scope Grants
files:read Read files, list folders, list shares
files:write Create, update, delete files and folders
tenant:admin Manage users, groups, quotas, connectors within the tenant
partner:admin Manage all tenants owned by the caller's partner
admin:* Full platform admin
* Superuser (reserved for platform operators)

A token with no application scopes gets tenant:admin by default on the assumption that ScaiKey already gates who can call ScaiDrive — i.e., ScaiKey is the enforcement point for "is this user allowed to use ScaiDrive at all." You can narrow the scope at token-issue time if you want to restrict a service token.

OIDC scopes like openid, profile, email, offline_access are recognized but not used for authorization — they only matter to ScaiKey.

How end users get a token#

End users don't get tokens directly — their client does, through OAuth 2.0 Authorization Code with PKCE. The flow:

sequenceDiagram participant U as User participant C as Client app participant SD as ScaiDrive participant SK as ScaiKey C->>SD: GET /api/v1/auth/config SD-->>C: authorize_url, token_url, client_id C->>C: generate PKCE verifier + challenge C->>SK: redirect /authorize<br/>(code_challenge) U->>SK: complete SSO SK-->>C: redirect with auth code C->>SD: POST /api/v1/auth/token<br/>(code + code_verifier) SD->>SK: exchange code (server-side) SK-->>SD: access_token, refresh_token SD-->>C: tokens
  1. Client calls GET /api/v1/auth/config to discover ScaiKey's authorize/token URLs and the registered client ID.
  2. Client generates a PKCE code verifier and challenge, redirects the user to ScaiKey's authorize endpoint.
  3. User completes SSO at ScaiKey. ScaiKey redirects to the registered callback URL with an authorization code.
  4. Client exchanges the code at POST /api/v1/auth/token (ScaiDrive proxies this to ScaiKey so clients only need the ScaiDrive URL).
  5. Response contains access_token, refresh_token, expires_in.

The web client does this automatically. The desktop and mobile clients do it via a browser pop-up. For your own integration, the OAuth dance only matters if you're building another end-user app.

Discover OAuth config#

bash
1
curl $SCAIDRIVE_URL/api/v1/auth/config
json
1
2
3
4
5
6
{
  "authorize_url": "https://scaikey.scailabs.ai/oauth2/authorize",
  "token_url": "https://scaidrive.scailabs.ai/api/v1/auth/token",
  "client_id": "scaidrive-desktop",
  "desktop_callback_url": "https://scaidrive.scailabs.ai/api/v1/auth/callback"
}

Exchange authorization code#

bash
1
2
3
4
5
6
curl -X POST $SCAIDRIVE_URL/api/v1/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<authorization_code>" \
  -d "redirect_uri=<your_callback>" \
  -d "code_verifier=<pkce_verifier>"
python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import httpx, os

resp = httpx.post(
    f"{os.environ['SCAIDRIVE_URL']}/api/v1/auth/token",
    data={
        "grant_type": "authorization_code",
        "code": os.environ["CODE"],
        "redirect_uri": "http://localhost:8765/callback",
        "code_verifier": os.environ["VERIFIER"],
    },
)
tokens = resp.json()
print(tokens["access_token"])
typescript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const resp = await fetch(`${process.env.SCAIDRIVE_URL}/api/v1/auth/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: process.env.CODE!,
    redirect_uri: "http://localhost:8765/callback",
    code_verifier: process.env.VERIFIER!,
  }),
});
const tokens = await resp.json();

Response:

json
1
2
3
4
5
6
7
{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_...",
  "id_token": "eyJhbGc..."
}

How services get a token#

For machine-to-machine (a backend service calling ScaiDrive), ask your tenant admin to create a service token in ScaiKey. Service tokens belong to a synthetic user — no human interaction, no refresh flow required — and have a long or configurable lifetime.

Use it the same way as a user token:

bash
1
2
curl -H "Authorization: Bearer $SCAIDRIVE_SERVICE_TOKEN" \
     $SCAIDRIVE_URL/api/v1/users/me

Never put service tokens in client-side code. A JavaScript bundle in a browser leaks the token to anyone who opens DevTools. Keep service tokens on the server.

Refreshing a token#

Access tokens expire (default: 1 hour). Refresh tokens last longer and rotate on use.

bash
1
2
3
4
curl -X POST $SCAIDRIVE_URL/api/v1/auth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=<your_refresh_token>"
python
1
2
3
4
5
resp = httpx.post(
    f"{scaidrive_url}/api/v1/auth/token",
    data={"grant_type": "refresh_token", "refresh_token": refresh_token},
)
new_tokens = resp.json()

Your client should refresh proactively before expiration, or reactively when a request returns 401 AUTH_TOKEN_EXPIRED. Don't busy-refresh — if a refresh fails, the user re-authenticates.

Authorization — what the token is allowed to do#

Authentication answers "who is this request from." Authorization answers "what can they do." ScaiDrive resolves authorization in this order:

  1. Global admin role. Users with the super_admin role (set in the ScaiDrive database, not in the JWT) can do anything. tenant_admin can do anything within their tenant.
  2. Share membership. The user's role in the share (owner, admin, contributor, reader) defines the default permissions for every resource in that share.
  3. ACL entries. Files and folders can override share-level permissions with per-resource allow/deny entries. Inheritance applies unless explicitly broken.
  4. External link scope. Requests authenticated with a link token (not a user JWT) are restricted to the link's allowed actions and resource scope.

See Permissions and ACLs for the full resolution rules.

JIT provisioning#

The first time a user presents a valid ScaiKey JWT that ScaiDrive has never seen, ScaiDrive creates a local user record automatically. No admin action is needed.

Subsequent ScaiKey webhooks on user.updated, group.updated, etc. keep the local records in sync. If you disable a user in ScaiKey, ScaiDrive sees the webhook, marks them suspended, and subsequent requests with their (still-unexpired) JWT fail with AUTHZ_USER_SUSPENDED.

What's next#

Updated 2026-05-18 15:04:08 View source (.md) rev 2