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)
curlandjqinstalled- 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_REGISTRATIONenvironment variable:
open— anyone can registerinvite_only— requires a valid invite codedisabled— 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).
| Scope | Grants |
|---|---|
read | GET requests only |
write | GET, POST, PATCH, PUT, DELETE |
admin | Everything 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 type | Storage | Lifetime |
|---|---|---|
| JWT access token | Client-side only | 1 hour |
| Refresh token | SHA-256 hash in DB | 30 days |
| API key | SHA-256 hash in DB | Until 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
- Client calls
/api/v1/auth/sso/discoverwith the org ID - Alloy generates a PKCE code verifier and challenge
- Alloy returns the IDP authorization URL with embedded state
- User authenticates at the IDP (Okta, Google, etc.)
- IDP redirects back to
/api/v1/auth/sso/callbackwith an auth code - Alloy exchanges the code for IDP tokens using the PKCE verifier
- Alloy validates the ID token signature via the IDP’s JWKS endpoint
- 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
- End-to-End Walkthrough — use your tokens to manage projects, tickets, and sprints
- API Automation — scripting patterns for CI/CD
- API Reference — complete endpoint documentation