API Reference
Every auth, OAuth 2.0, user, RBAC, and admin endpoint for Aether Labs SSO.
API Reference
All Aether Labs SSO endpoints are served from a single base URL:
Production: https://sso.aetherlabs.agency
Local dev: http://localhost:4000Request groups:
| Group | Prefix | Auth |
|---|---|---|
| Health | /health | none |
| Auth | /api/auth/* | cookies or Authorization: Bearer |
| OAuth 2.0 | /api/oauth/* | mixed |
| Users | /api/users/* | access token + permission |
| RBAC | /api/rbac/* | access token + permission |
| Admin | /api/admin/* | access token + permission |
Requests with no Origin header (curl, mobile, server-to-server) are always
allowed. Browser requests are CORS-checked against ALLOWED_ORIGINS. /api/* is
rate-limited to a few hundred requests per 15 minutes per IP.
Authentication model
Two ways to authenticate a request:
- Cookies — the SSO session cookies
accessTokenandrefreshToken, sent withcredentials: "include". Used by first-party apps. - Bearer token — send the access token as
Authorization: Bearer <accessToken>.
Token contract
Access tokens are HS256 JWTs signed with the shared JWT_ACCESS_SECRET:
| Property | Value |
|---|---|
iss | aetherlabs-sso |
aud | aetherlabs |
| Algorithm | HS256 |
| Access token lifetime | 24 hours |
| Refresh token lifetime | 30 days |
Claims: userId, email, name, roles[], plus iat and exp. Refresh tokens
also carry a tokenId. Permissions are not in the token — they are resolved
live from the database (fetch them from /api/auth/me).
{
"userId": "uuid",
"email": "user@example.com",
"name": "Jane Doe",
"roles": ["admin"],
"iat": 123,
"exp": 456,
"iss": "aetherlabs-sso",
"aud": "aetherlabs"
}Cookie vs token lifetime
The accessToken cookie expires after 1 hour and refreshToken after 7 days,
but the JWTs they carry are valid for 24 hours and 30 days. Always drive your
own expiry from the token's exp claim, not the cookie lifetime.
There is no JWKS endpoint. Consumers either verify the JWT with the shared secret
or call /api/auth/me.
Health
GET /health
Returns { "status": "OK" } when the database is reachable, 503 otherwise.
Auth endpoints
POST /api/auth/register
Create a user from first_name, last_name, email, password (min 6 chars).
Returns tokens and sets the SSO cookies. Public. New users start with no roles
until an admin assigns one.
POST /api/auth/login
Authenticate with email and password. On success, sets the accessToken and
refreshToken cookies and returns the user. Public.
GET /api/auth/login-target
Resolves an allowlisted post-login destination from ?app=<key>, ?redirect_to=<url>
or ?path=<path>. Used by the login UI so a caller can never redirect off-site.
POST /api/auth/login-request
Mints a single-use, short-lived login token bound to an allowlisted destination.
Body: { app | redirect_to, path }. Returns a token to hand to
/api/auth/login-resolve.
POST /api/auth/login-resolve
Redeems a login token from /api/auth/login-request and returns { url }.
GET /api/auth/me
Returns the authenticated user with roles and permissions. Send the access token as
a cookie (credentials: "include") or as Authorization: Bearer <accessToken>.
{
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "Jane Doe",
"roles": ["admin"],
"permissions": ["users:read", "oauth_clients:write"],
"adminRole": "admin",
"is_active": true,
"email_verified": true
}
}POST /api/auth/refresh-token
Issues a new access token from the refresh cookie. Refresh tokens rotate — replace the old value with the returned one.
await fetch('https://sso.aetherlabs.agency/api/auth/refresh-token', {
method: 'POST',
credentials: 'include',
});POST /api/auth/logout
Revokes the refresh token and clears the SSO cookies. Requires authentication.
await fetch('https://sso.aetherlabs.agency/api/auth/logout', {
method: 'POST',
credentials: 'include',
});GET /api/auth/verify-admin
Returns 200 with the identity if the user holds an admin-tier role
(admin, super_admin, or manager), otherwise 403. Used by admin-only apps.
GET /api/auth/authorize
Legacy minimal OAuth2-style redirect. Prefer the /api/oauth/* endpoints.
Self-service account endpoints
These act only on the signed-in user's own record.
| Method | Path | Purpose |
|---|---|---|
PUT | /api/auth/me | Update your own name and profile |
POST | /api/auth/change-password | Change your password; revokes every other session |
GET | /api/auth/sessions | Your active sessions |
POST | /api/auth/sessions/revoke-others | Sign out everywhere else |
DELETE | /api/auth/sessions/:sessionId | Revoke one session |
GET | /api/auth/apps | Apps approved to sign in with this account |
DELETE | /api/auth/apps/:clientId | Disconnect an app: drops the grant and revokes its sessions |
Email verification is handled by POST /api/auth/verify-email (validate a link
without consuming it), POST /api/auth/set-password (set the password, mark the
address verified, burn the link) and POST /api/auth/verify-email/resend.
See Connected apps for the grant lifecycle.
OAuth 2.0 endpoints
GET /api/oauth/authorize
Starts the authorization flow. Redirect the user's browser here.
| Parameter | Required | Description |
|---|---|---|
response_type | yes | Use code. |
client_id | yes | Your OAuth client identifier. |
redirect_uri | yes | Must exactly match a registered callback URL. |
scope | yes | Space-separated, e.g. openid profile email. |
state | yes | Random CSRF value; validate it on callback. |
code_challenge | yes | PKCE challenge (S256). Required. |
code_challenge_method | yes | S256. |
If the user is not signed in, they are sent to the login UI, then the consent
screen, then back to redirect_uri with code and state.
GET /api/oauth/consent-info
Requires authentication. Returns the client name and the requested scopes for the consent screen.
POST /api/oauth/authorize/approve
Requires authentication. Approves the pending authorization and returns
{ redirect_url } containing the one-time code. Codes are single-use and expire
after 10 minutes.
POST /api/oauth/token
Exchanges an authorization code for tokens, or refreshes. The client authenticates with its secret, sent as HTTP Basic or as client_id + client_secret in the body. Content-Type: application/json.
Authorization code:
{
"grant_type": "authorization_code",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"redirect_uri": "https://yourapp.example.com/oauth/callback",
"code": "AUTH_CODE",
"code_verifier": "ORIGINAL_CODE_VERIFIER"
}code_verifier must match the code_challenge sent to
/api/oauth/authorize.
Refresh token:
{
"grant_type": "refresh_token",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"refresh_token": "REFRESH_TOKEN"
}Response (both grants):
{
"access_token": "jwt",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "jwt",
"scope": "openid profile email"
}GET /api/oauth/userinfo
Returns the OpenID profile. Send Authorization: Bearer <accessToken>.
{
"sub": "user-id",
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true
}POST /api/oauth/revoke
Revokes a token (usually the refresh token) at logout. The client must authenticate (HTTP Basic or a body secret) and may only revoke tokens it issued.
{
"token": "REFRESH_TOKEN",
"token_type_hint": "refresh_token"
}GET /api/oauth/.well-known/oauth-authorization-server
Public discovery document.
{
"issuer": "aetherlabs-sso",
"authorization_endpoint": "https://sso.aetherlabs.agency/api/oauth/authorize",
"token_endpoint": "https://sso.aetherlabs.agency/api/oauth/token",
"userinfo_endpoint": "https://sso.aetherlabs.agency/api/oauth/userinfo",
"revocation_endpoint": "https://sso.aetherlabs.agency/api/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid", "profile", "email", "read", "write", "admin"],
"claims_supported": ["sub", "name", "email", "email_verified", "updated_at"]
}Users endpoints
All require authentication and the listed permission.
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | /api/users | users:read | List users. |
GET | /api/users/:userId | users:read | User detail with roles and permissions. |
PUT | /api/users/:userId | users:write | Update a user. |
DELETE | /api/users/:userId | users:delete | Deactivate a user. |
GET | /api/users/:userId/roles | users:read | List a user's roles. |
POST | /api/users/:userId/roles | users:manage_roles | Assign a role. |
DELETE | /api/users/:userId/roles/:roleId | users:manage_roles | Remove a role. |
GET | /api/users/admin/list | system:manage_users | Users holding admin-scoped roles. |
GET | /api/users/roles/list | system:manage_roles | List active roles. |
POST | /api/users/roles | super_admin | Create a role. |
PUT | /api/users/roles/:roleId | super_admin | Update a role. |
DELETE | /api/users/roles/:roleId | super_admin | Deactivate a role. |
RBAC endpoints
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | /api/rbac/permissions | admin | Permission constants and catalog. |
GET | /api/rbac/roles | admin | List roles, including scoped application. |
POST | /api/rbac/roles | super_admin | Create a role. |
PUT | /api/rbac/roles/:roleId | super_admin | Update a role. |
DELETE | /api/rbac/roles/:roleId | super_admin | Deactivate a role. |
GET | /api/rbac/users/:userId/roles | users:read | List a user's roles. |
POST | /api/rbac/users/:userId/roles | users:manage_roles | Assign a role. |
DELETE | /api/rbac/users/:userId/roles/:roleId | users:manage_roles | Remove a role. |
GET | /api/rbac/users/:userId/permissions | users:read | Resolved roles and permissions. |
POST | /api/rbac/users/:userId/check-permission | users:read | { permission } → { hasPermission }. |
GET | /api/rbac/admin-users | system:manage_users | List admin users. |
Admin endpoints (OAuth clients)
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | /api/admin/summary | admin | Counts of users, clients, roles, admins. |
GET | /api/admin/oauth-clients | oauth_clients:read | List OAuth clients. |
GET | /api/admin/oauth-clients/:clientId | oauth_clients:read | Client detail (secret redacted). |
POST | /api/admin/oauth-clients | oauth_clients:write | Create a client; returns the secret once. |
PUT | /api/admin/oauth-clients/:clientId | oauth_clients:write | Update a client. |
POST | /api/admin/oauth-clients/:clientId/rotate-secret | oauth_clients:rotate_secret | Rotate and return a new secret. |
DELETE | /api/admin/oauth-clients/:clientId | oauth_clients:delete | Deactivate a client. |
Errors
| Status | Meaning |
|---|---|
400 | Bad request — missing or invalid parameters. |
401 | No valid access token. Refresh, then retry once. |
403 | Authenticated but lacking the required role/permission. |
404 | Unknown resource. |
429 | Rate limited on /api/*. |
See also
- Same-domain SSO — first-party cookie integration.
- OAuth integration — OAuth client + PKCE integration.
- Roles & permissions — the RBAC model and seeded roles.