Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication Deep Dive

This tutorial covers every authentication mechanism in Alloy: password-based registration, JWT token lifecycle (login, refresh, logout), API key management, scope enforcement, and SSO via OIDC. Every example uses curl and jq so you can follow along in your terminal.

1. Prerequisites

You need:

  • A running Alloy server (see Getting Started)
  • curl and jq installed
  • A terminal with shell variable support

Start the server with open registration:

ALLOY_REGISTRATION=open ./target/release/alloy serve

Set the base URL:

BASE_URL="http://localhost:3000"

2. Registration

Create a new user account. The register endpoint returns an access token, a refresh token, and the user’s ID — everything you need to start making authenticated requests immediately.

curl -s -X POST "$BASE_URL/api/v1/auth/register" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "authdemo@alloy.dev",
    "password": "authdemo1",
    "display_name": "Auth Demo User"
  }' | jq .
{
  "access_token": "...",
  "refresh_token": "...",
  "user_id": "...",
  "email": "authdemo@alloy.dev",
  "display_name": "Auth Demo User"
}

Save the tokens for subsequent requests:

TOKEN="<access_token from above>"
REFRESH_TOKEN="<refresh_token from above>"
USER_ID="<user_id from above>"

Registration automatically creates a personal organization for the new user. Verify by listing your organizations:

curl -s "$BASE_URL/api/v1/orgs" \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "items": [
    {
      "id": "...",
      "name": "...",
      "slug": "...",
      "created_at": "...",
      "updated_at": "..."
    }
  ]
}

Registration modes: Alloy supports three modes controlled by the ALLOY_REGISTRATION environment variable:

  • open — anyone can register
  • invite_only — requires a valid invite code
  • disabled — registration is closed entirely

3. JWT Authentication

Alloy uses short-lived JWT access tokens (1 hour) paired with long-lived refresh tokens (30 days). This section walks through the complete login, refresh, and logout cycle.

Login

Exchange email and password for tokens:

curl -s -X POST "$BASE_URL/api/v1/auth/login" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "authdemo@alloy.dev",
    "password": "authdemo1"
  }' | jq .
{
  "access_token": "...",
  "refresh_token": "...",
  "user_id": "...",
  "email": "authdemo@alloy.dev",
  "display_name": "Auth Demo User"
}

The access token is a signed JWT containing your user ID, org ID, email, and role. Use it in the Authorization header for all authenticated requests:

Authorization: Bearer <access_token>

Refresh

When the access token expires (after 1 hour), use the refresh token to get a new token pair. Alloy implements refresh token rotation — each refresh call returns a brand-new refresh token and revokes the old one:

curl -s -X POST "$BASE_URL/api/v1/auth/refresh" \
  -H "Content-Type: application/json" \
  -d "{
    \"refresh_token\": \"$REFRESH_TOKEN\"
  }" | jq .
{
  "access_token": "...",
  "refresh_token": "...",
  "user_id": "...",
  "email": "authdemo@alloy.dev",
  "display_name": "Auth Demo User"
}

After refreshing, update both tokens in your client. The old refresh token is now invalid and cannot be used again.

Logout

Revoke the refresh token to end the session. This prevents any further token refreshes:

curl -s -X POST "$BASE_URL/api/v1/auth/logout" \
  -H "Content-Type: application/json" \
  -d "{
    \"refresh_token\": \"$REFRESH_TOKEN\"
  }" -w "\n%{http_code}" 2>/dev/null | tail -1

A successful logout returns 204 No Content with an empty body. After logout, the refresh token is revoked and cannot be reused.

Security note: Logging out does not immediately invalidate the JWT access token — it remains valid until it expires (up to 1 hour). To force immediate revocation, rotate your JWT signing keys on the server.


4. API Key Management

API keys provide long-lived, scoped authentication for scripts, CI/CD pipelines, and integrations. Unlike JWTs, API keys do not expire automatically (unless you set an expiration) and can be restricted to specific scopes and projects.

Create an API Key

curl -s -X POST "$BASE_URL/api/v1/api-keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI Pipeline Key"
  }' | jq .
{
  "id": "...",
  "name": "CI Pipeline Key",
  "key": "...",
  "key_prefix": "...",
  "scopes": ["read", "write"],
  "project_ids": [],
  "created_at": "...",
  "expires_at": null
}

The key field contains the full API key (prefixed with alloy_live_). This is the only time the full key is shown. Store it securely — Alloy only keeps a SHA-256 hash.

Use the key exactly like a JWT in the Authorization header:

Authorization: Bearer alloy_live_<key>

List API Keys

View all your API keys. The full key is never returned — only the prefix:

curl -s "$BASE_URL/api/v1/api-keys" \
  -H "Authorization: Bearer $TOKEN" | jq .
[
  {
    "id": "...",
    "name": "CI Pipeline Key",
    "key_prefix": "...",
    "scopes": ["read", "write"],
    "project_ids": [],
    "created_at": "...",
    "last_used_at": null,
    "expires_at": null
  }
]

Revoke an API Key

Delete a key to immediately revoke it. Use the id from the create or list response:

curl -s -X DELETE "$BASE_URL/api/v1/api-keys/$API_KEY_ID" \
  -H "Authorization: Bearer $TOKEN" -w "\n%{http_code}" 2>/dev/null | tail -1

A successful revocation returns 204 No Content. The key is immediately invalid — any request using it will receive a 401 Unauthorized response.


5. Token Security and Scopes

Scope Model

Alloy enforces scopes on API key requests. JWT-authenticated requests always receive full access (* scope).

ScopeGrants
readGET requests only
writeGET, POST, PATCH, PUT, DELETE
adminEverything write grants, plus admin operations

When creating an API key, specify scopes to limit its capabilities:

curl -s -X POST "$BASE_URL/api/v1/api-keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Read-Only Dashboard Key",
    "scopes": ["read"]
  }' | jq .
{
  "id": "...",
  "name": "Read-Only Dashboard Key",
  "key": "...",
  "key_prefix": "...",
  "scopes": ["read"],
  "project_ids": [],
  "created_at": "...",
  "expires_at": null
}

Project-Scoped Keys

Restrict an API key to specific projects. The key can only access resources within the listed projects:

curl -s -X POST "$BASE_URL/api/v1/api-keys" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"Frontend Project Key\",
    \"scopes\": [\"read\", \"write\"],
    \"project_ids\": [\"$PROJECT_ID\"]
  }" | jq .
{
  "id": "...",
  "name": "Frontend Project Key",
  "key": "...",
  "key_prefix": "...",
  "scopes": ["read", "write"],
  "project_ids": ["..."],
  "created_at": "...",
  "expires_at": null
}

Password Security

Alloy hashes all passwords with Argon2, a memory-hard algorithm resistant to GPU and ASIC attacks. Passwords are never stored in plaintext. The minimum password length is 8 characters.

Token Storage

Token typeStorageLifetime
JWT access tokenClient-side only1 hour
Refresh tokenSHA-256 hash in DB30 days
API keySHA-256 hash in DBUntil revoked

6. SSO Authentication (OIDC)

Alloy supports Single Sign-On via OpenID Connect with PKCE. This is used by organizations that require centralized identity management through providers like Okta, Google Workspace, or Azure AD.

SSO Flow

The SSO flow is a two-step redirect process:

Step 1 — Discover the authorization URL:

GET /api/v1/auth/sso/discover?org_id=<org-uuid>

This returns the identity provider’s authorization URL with a PKCE code challenge and a state parameter. Redirect the user’s browser to this URL.

Step 2 — Handle the callback:

GET /api/v1/auth/sso/callback?code=<authorization_code>&state=<state>

After the user authenticates with the identity provider, they are redirected back to Alloy’s callback endpoint. Alloy exchanges the authorization code for tokens, validates the ID token, and returns an Alloy JWT.

How It Works

  1. Client calls /api/v1/auth/sso/discover with the org ID
  2. Alloy generates a PKCE code verifier and challenge
  3. Alloy returns the IDP authorization URL with embedded state
  4. User authenticates at the IDP (Okta, Google, etc.)
  5. IDP redirects back to /api/v1/auth/sso/callback with an auth code
  6. Alloy exchanges the code for IDP tokens using the PKCE verifier
  7. Alloy validates the ID token signature via the IDP’s JWKS endpoint
  8. Alloy finds or creates the user and issues an Alloy JWT

SSO users are automatically added as Member to the organization if they don’t already have a membership.

Note: SSO configuration (client ID, client secret, issuer URL) is stored per-organization in the database. Contact your Alloy administrator to configure SSO for your organization.


Next Steps