AETHERLABS
API Reference

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:4000

Request groups:

GroupPrefixAuth
Health/healthnone
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:

  1. Cookies — the SSO session cookies accessToken and refreshToken, sent with credentials: "include". Used by first-party apps.
  2. Bearer token — send the access token as Authorization: Bearer <accessToken>.

Token contract

Access tokens are HS256 JWTs signed with the shared JWT_ACCESS_SECRET:

PropertyValue
issaetherlabs-sso
audaetherlabs
AlgorithmHS256
Access token lifetime24 hours
Refresh token lifetime30 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.

MethodPathPurpose
PUT/api/auth/meUpdate your own name and profile
POST/api/auth/change-passwordChange your password; revokes every other session
GET/api/auth/sessionsYour active sessions
POST/api/auth/sessions/revoke-othersSign out everywhere else
DELETE/api/auth/sessions/:sessionIdRevoke one session
GET/api/auth/appsApps approved to sign in with this account
DELETE/api/auth/apps/:clientIdDisconnect 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.

ParameterRequiredDescription
response_typeyesUse code.
client_idyesYour OAuth client identifier.
redirect_uriyesMust exactly match a registered callback URL.
scopeyesSpace-separated, e.g. openid profile email.
stateyesRandom CSRF value; validate it on callback.
code_challengeyesPKCE challenge (S256). Required.
code_challenge_methodyesS256.

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.

MethodPathPermissionPurpose
GET/api/usersusers:readList users.
GET/api/users/:userIdusers:readUser detail with roles and permissions.
PUT/api/users/:userIdusers:writeUpdate a user.
DELETE/api/users/:userIdusers:deleteDeactivate a user.
GET/api/users/:userId/rolesusers:readList a user's roles.
POST/api/users/:userId/rolesusers:manage_rolesAssign a role.
DELETE/api/users/:userId/roles/:roleIdusers:manage_rolesRemove a role.
GET/api/users/admin/listsystem:manage_usersUsers holding admin-scoped roles.
GET/api/users/roles/listsystem:manage_rolesList active roles.
POST/api/users/rolessuper_adminCreate a role.
PUT/api/users/roles/:roleIdsuper_adminUpdate a role.
DELETE/api/users/roles/:roleIdsuper_adminDeactivate a role.

RBAC endpoints

MethodPathPermissionPurpose
GET/api/rbac/permissionsadminPermission constants and catalog.
GET/api/rbac/rolesadminList roles, including scoped application.
POST/api/rbac/rolessuper_adminCreate a role.
PUT/api/rbac/roles/:roleIdsuper_adminUpdate a role.
DELETE/api/rbac/roles/:roleIdsuper_adminDeactivate a role.
GET/api/rbac/users/:userId/rolesusers:readList a user's roles.
POST/api/rbac/users/:userId/rolesusers:manage_rolesAssign a role.
DELETE/api/rbac/users/:userId/roles/:roleIdusers:manage_rolesRemove a role.
GET/api/rbac/users/:userId/permissionsusers:readResolved roles and permissions.
POST/api/rbac/users/:userId/check-permissionusers:read{ permission } → { hasPermission }.
GET/api/rbac/admin-userssystem:manage_usersList admin users.

Admin endpoints (OAuth clients)

MethodPathPermissionPurpose
GET/api/admin/summaryadminCounts of users, clients, roles, admins.
GET/api/admin/oauth-clientsoauth_clients:readList OAuth clients.
GET/api/admin/oauth-clients/:clientIdoauth_clients:readClient detail (secret redacted).
POST/api/admin/oauth-clientsoauth_clients:writeCreate a client; returns the secret once.
PUT/api/admin/oauth-clients/:clientIdoauth_clients:writeUpdate a client.
POST/api/admin/oauth-clients/:clientId/rotate-secretoauth_clients:rotate_secretRotate and return a new secret.
DELETE/api/admin/oauth-clients/:clientIdoauth_clients:deleteDeactivate a client.

Errors

StatusMeaning
400Bad request — missing or invalid parameters.
401No valid access token. Refresh, then retry once.
403Authenticated but lacking the required role/permission.
404Unknown resource.
429Rate limited on /api/*.

See also

On this page