# auth.md

Swirls lets AI agents register on behalf of users and act in their workspace.
The resource server is `swirls.ai` (API under `https://swirls.ai/api`). The
authorization server is `auth.swirls.ai`.

Swirls supports the user claimed flow with a required email. You register with
the user's email address, Swirls emails the user an approval link, the user
reads a 6-digit code back to you, and you exchange that code for a credential.

## Discover

Unauthenticated requests to the API return 401 with a pointer to the
protected resource metadata:

```http
WWW-Authenticate: Bearer resource_metadata="https://swirls.ai/.well-known/oauth-protected-resource"
```

Fetch the protected resource metadata, then the authorization server metadata.
The `agent_auth` block describes the registration endpoints:

```http
GET https://swirls.ai/.well-known/oauth-protected-resource
GET https://auth.swirls.ai/.well-known/oauth-authorization-server
```

## Pick a method

Check `identity_assertion_types_supported` in the authorization server
metadata, then pick the first match:

- Your platform can mint an ID-JAG for Swirls: use `identity_assertion`
  with `assertion_type: urn:ietf:params:oauth:token-type:id-jag`. A
  credential is issued immediately. Supported only when the metadata lists
  the ID-JAG assertion type.
- You know the user's email address: use `identity_assertion` with
  `assertion_type: verified_email`. No credential is issued until the user
  approves.

## Register with an ID-JAG

Mint an ID-JAG with `aud` set to `https://auth.swirls.ai` and a verified
email claim, then post it:

```http
POST https://auth.swirls.ai/agent/auth
Content-Type: application/json
```

```json
{
  "type": "identity_assertion",
  "assertion_type": "urn:ietf:params:oauth:token-type:id-jag",
  "assertion": "<ID-JAG JWT>",
  "requested_credential_type": "api_key"
}
```

Successful response (201) with an immediate credential:

```json
{
  "registration_id": "0196f3a2-...",
  "registration_type": "agent-provider",
  "credential_type": "api_key",
  "credential": "eyJ2IjoxLCJpZGVudGlmaWVyIjoi...",
  "credential_expires": "2026-07-05T13:00:00.000Z",
  "scopes": [
    "projects:read",
    "projects:write",
    "deployments:read",
    "deployments:write"
  ]
}
```

If the asserted email is new to Swirls, the account and workspace are created
during registration.

## Register with an email

```http
POST https://auth.swirls.ai/agent/auth
Content-Type: application/json
```

```json
{
  "type": "identity_assertion",
  "assertion_type": "verified_email",
  "assertion": "user@example.com",
  "requested_credential_type": "api_key"
}
```

Successful response (201). No credential yet:

```json
{
  "registration_id": "0196f3a2-...",
  "registration_type": "email-verification",
  "claim_url": "https://swirls.ai/auth.md",
  "claim_token": "clm_AbCdEfGhIjKlMnOpQrStUvWxY",
  "claim_token_expires": "2026-06-05T13:30:00.000Z",
  "post_claim_scopes": [
    "projects:read",
    "projects:write",
    "deployments:read",
    "deployments:write"
  ]
}
```

Keep `claim_token` private. It expires 30 minutes after registration.

## Claim ceremony

1. Swirls emails the user a one-time approval link. Tell the user to open the
   email from Swirls and approve the request.
2. The approval page shows the user a 6-digit code. Ask the user to read it
   to you. The code expires 10 minutes after approval and allows 5 attempts.
3. Submit the code with your claim token:

```http
POST https://auth.swirls.ai/agent/auth/claim/complete
Content-Type: application/json
```

```json
{
  "claim_token": "clm_AbCdEfGhIjKlMnOpQrStUvWxY",
  "otp": "123456"
}
```

Successful response:

```json
{
  "registration_id": "0196f3a2-...",
  "status": "claimed",
  "credential_type": "api_key",
  "credential": "eyJ2IjoxLCJpZGVudGlmaWVyIjoi...",
  "credential_expires": "2026-07-05T13:00:00.000Z",
  "scopes": [
    "projects:read",
    "projects:write",
    "deployments:read",
    "deployments:write"
  ]
}
```

If the user is new to Swirls, approving the request also creates their
account and workspace. No separate signup is needed.

## Use the credential

Send the credential as a bearer token to the Swirls API:

```http
Authorization: Bearer <credential>
```

The credential acts as the user who approved it, limited to the granted
scopes. It expires 30 days after issue; register again to get a new one.

To use the Swirls CLI with the credential, write it to the CLI credentials
file:

```json
{ "accessToken": "<credential>" }
```

The file lives at `$XDG_CONFIG_HOME/swirls/credentials`, which defaults to
`~/.config/swirls/credentials`. Create the directory if it does not exist:

```bash
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/swirls"
printf '{ "accessToken": "%s" }' "$CREDENTIAL" > "${XDG_CONFIG_HOME:-$HOME/.config}/swirls/credentials"
```

Deploy without prompts by passing the deployment name and project. `--create`
makes the project on first use and pins it to `swirls.config.ts` so later
deploys need no flags:

```bash
swirls deploy --project my-project --create --name initial
```

## Errors

| Error | Endpoint | What to do |
| --- | --- | --- |
| `invalid_request` | any | Fix the request body and retry. |
| `unsupported_identity_type` | register | Use `identity_assertion`. |
| `unsupported_assertion_type` | register | Use `verified_email` or the ID-JAG token type. |
| `unsupported_credential_type` | register | Use `api_key`. |
| `rate_limited` | any | Back off and retry later. |
| `invalid_grant` | claim, revoke | The claim token or code is wrong, expired, or not yet approved. Ask the user to approve, or register again. |
| `issuer_not_enabled` | register, revoke | The ID-JAG issuer is not trusted here, or ID-JAG support is not enabled. Use the email flow. |
| `invalid_assertion` | register, revoke | The ID-JAG failed verification (signature, audience, expiry, or claims). Mint a fresh one. |
| `invalid_client_id` | register | The asserting client is not trusted by Swirls. |
| `missing_verified_email` | register | The ID-JAG must carry `email` with `email_verified: true`. |
| `replay_detected` | register, revoke | The assertion's `jti` was already used. Mint a fresh one. |
| `server_error` | any | Retry with backoff. |

## Revocation

Present the credential to revoke it. Users can also deny or revoke requests
from the approval page.

```http
POST https://auth.swirls.ai/agent/auth/revoke
Content-Type: application/json
```

```json
{ "credential": "<credential>" }
```

Agent providers can revoke every credential issued for a user by posting a
logout token:

```http
POST https://auth.swirls.ai/agent/auth/revoke
Content-Type: application/logout+jwt

<logout token JWT>
```

The logout token must be signed by the provider, carry the same `iss` and
`sub` as the original ID-JAG, set `aud` to `https://auth.swirls.ai`, and
include the
`https://schemas.workos.com/events/agent/auth/identity/assertion/revoked`
event.

## Contact

Questions: security@swirls.ai
