---
title: "Authentication"
description: "How people and machines prove who they are."
url: "https://saved.sh/docs/security/authentication"
---

Every principal, human or machine, authenticates against **WorkOS**. We validate what it
issues and never mint our own credentials.

That gives one identity system rather than two, and it means a revocation in one place is a
revocation everywhere.

## The three principals [#the-three-principals]

| Principal                      | Credential                        | Presented as                  |
| ------------------------------ | --------------------------------- | ----------------------------- |
| **Human** in the dashboard     | AuthKit session, JWT access token | `Authorization: Bearer <jwt>` |
| **Human** in the CLI           | Same, obtained by device code     | `Authorization: Bearer <jwt>` |
| **Machine**: API key or worker | Org-scoped API key, opaque        | `Authorization: Bearer <key>` |

All three resolve to the **same internal principal shape**, so a shared endpoint such as
presign, confirm or download runs one handler regardless of who called it. There is no
"machine path" with different rules.

## Humans [#humans]

### Dashboard [#dashboard]

Standard AuthKit: OAuth authorization code with PKCE, and a sealed session cookie. The
dashboard forwards the current access token to our API as a bearer token.

### CLI [#cli]

`sctl login` uses the **OAuth device code** flow, which is the right choice for a terminal:
no local callback server, no browser embedded in a CLI, and it works over SSH.

```bash
sctl login
```

It prints a code, you approve it in a browser on any device, and the session lands in a local
config file. Tokens are refreshed automatically.

```bash
sctl workspace list
sctl workspace switch <name>
```

<Callout>
  **A session is scoped to exactly one workspace.** Switching workspaces gets a new access
  token carrying that workspace's permissions. There is no token that spans two, which is what
  makes the tenancy boundary a property of the credential rather than of a filter.
</Callout>

Where the CLI stores its session:

| Platform | Path             |
| -------- | ---------------- |
| Linux    | `~/.config/sctl` |
| macOS    | `~/.config/sctl` |
| Windows  | `%APPDATA%\sctl` |

Treat that file as a credential. There is no `sctl logout`: delete the file to end the
session on that machine, which is the first thing to do on a shared or borrowed one.

### Token validation [#token-validation]

| Property               | Value                                      |
| ---------------------- | ------------------------------------------ |
| Algorithm              | RS256, verified against the AuthKit JWKS   |
| Cached                 | JWKS cached for a few minutes              |
| Checked                | Issuer, and the set of accepted client IDs |
| Read for authorization | The `permissions` claim, and nothing else  |

The token also carries our own workspace and user IDs, mirrored into WorkOS as external IDs,
so the backend reads its own identifiers straight from a validated token with **no
per-request database lookup**.

<Callout type="warn">
  `role` is carried in the token and is **display only**. The backend authorizes on
  `permissions` and never reads `role`. See [Permissions](/docs/security/permissions).
</Callout>

## Machines [#machines]

API keys and worker keys are the same kind of credential: an **organization-scoped WorkOS API
key**, opaque rather than a JWT, sent as a bearer token.

| Property   | Behaviour                                                                            |
| ---------- | ------------------------------------------------------------------------------------ |
| Validation | Checked against WorkOS on use, and cached for about 60 seconds                       |
| Revocation | Propagates within the cache TTL, or immediately when the revocation webhook busts it |
| Scope      | Bound to one organization, which is one workspace                                    |
| Storage    | We store the key's identifier. The secret itself is shown once and never persisted   |

<Callout type="warn">
  **Revocation is not instantaneous.** A revoked key can continue to work for up to about a
  minute while a cached validation is still live, and on the orchestration side the cache
  cannot be busted early, so there it is always TTL-bound. Plan a revocation as "effective
  within a minute", not "effective now", and rotate source credentials too if a key leaked.
</Callout>

### Worker keys are also the orchestration credential [#worker-keys-are-also-the-orchestration-credential]

A worker key authenticates twice with the same secret: to our API for presign and confirm,
and to the orchestration hub to poll for work. It carries exactly two permissions, and the
hub requires one of them to admit a worker at all.

See [Permissions](/docs/security/permissions#machine-limits) and
[Registration](/docs/workers/registration#what-the-key-authorizes).

## Workspaces and membership [#workspaces-and-membership]

| Fact                                                     | Consequence                                              |
| -------------------------------------------------------- | -------------------------------------------------------- |
| A workspace is one WorkOS organization, 1:1 and flat     | There is no organization above a workspace               |
| Identity is global, access is per-workspace              | One person, one identity, many possible workspaces       |
| Membership and roles live in WorkOS                      | Not in our database, so there is one place to audit them |
| A worker belongs to one workspace, fixed at provisioning | It cannot be moved                                       |

Creating a workspace is **human-only**, and it is one of the very few gates that is not a
permission check. A brand-new user has no workspace and therefore no workspace-scoped
permission to check, so the rule is expressed on the principal's kind instead: a machine
credential cannot create a workspace, whatever it holds.

## Multi-factor [#multi-factor]

MFA is configured in WorkOS at the organization level, and it applies to human sign-in.
Because WorkOS is the trust anchor, enabling it there covers both the dashboard and the CLI
without any change on our side.

Machine keys are not subject to MFA, which is what makes them appropriate for automation and
also what makes their scoping matter. See [API keys](/docs/security/api-keys).

## Sessions and sign-out [#sessions-and-sign-out]

| Action                       | Effect                                   |
| ---------------------------- | ---------------------------------------- |
| Deleting the config file     | Ends the local session on that machine   |
| Signing out of the dashboard | Clears the session cookie                |
| Removing a member in WorkOS  | Ends their access to that workspace      |
| Revoking an API key          | Stops it within the validation cache TTL |

Removing a member does **not** revoke API keys they created. A key belongs to the workspace,
not to the person who made it, which is deliberate so automation does not break when someone
leaves. It also means offboarding has a second step: review the workspace's keys.

## Next [#next]

<Cards>
  <Card href="/docs/security/permissions" title="Permissions" description="What a validated principal is then allowed to do." />

  <Card href="/docs/security/api-keys" title="API keys" description="Issuing and scoping machine credentials." />

  <Card href="/docs/security/audit-log" title="Audit log" description="What each principal did, recorded." />
</Cards>
