Base URL: http://localhost:8001 (Docker Compose)
Authentication: Endpoints marked with "Auth: Yes" require a Bearer token in the Authorization header:
Authorization: Bearer <access_token>
Login with username and password.
- Auth: No
- Content-Type:
application/x-www-form-urlencoded - Body:
username=<string>&password=<string> - Response:
{ access_token, refresh_token, token_type, id_token }
curl -X POST http://localhost:8001/token \
-d "username=jdoe&password=SecurePass123!"Validate an access token and return the user identity. Designed for internal service-to-service calls.
- Auth: Yes
- Response:
{ user_id, username }
curl -X POST http://localhost:8001/token/validate \
-H "Authorization: Bearer $TOKEN"Exchange a refresh token for a new access/refresh token pair. Implements rotation with replay detection.
- Auth: Bearer (refresh token)
- Response:
{ access_token, refresh_token, token_type }
curl -X POST http://localhost:8001/refresh_token \
-H "Authorization: Bearer $REFRESH_TOKEN"Public keys for verifying JWT signatures. Downstream services fetch this to validate tokens locally.
- Auth: No
- Response:
{ keys: [{ kty, use, alg, kid, n, e }] }
curl http://localhost:8001/.well-known/jwks.jsonList all active sessions for the current user.
- Auth: Yes
- Response:
{ sessions: [{ id, ip_address, device_name, login_method, created_at, last_activity_at, is_current }], total }
curl http://localhost:8001/auth/sessions \
-H "Authorization: Bearer $TOKEN"Revoke a specific session. Invalidates its refresh token.
- Auth: Yes
- Response:
{ message }
curl -X DELETE http://localhost:8001/auth/sessions/<session_id> \
-H "Authorization: Bearer $TOKEN"Revoke all sessions except the current one (logout all other devices).
- Auth: Yes
- Response:
{ message }
curl -X DELETE http://localhost:8001/auth/sessions \
-H "Authorization: Bearer $TOKEN"Register a new user. Password must be 12+ characters with uppercase, lowercase, digit, and special character.
- Auth: No
- Body:
{ username, email, password } - Response:
{ id, username, email, disabled, is_active, email_verified, auth_provider, is_native, created_at, last_modified }
curl -X POST http://localhost:8001/users/ \
-H "Content-Type: application/json" \
-d '{"username": "jdoe", "email": "jdoe@example.com", "password": "SecurePass123!"}'List all users.
- Auth: No
- Response: Array of User objects
Get a user by UUID.
- Auth: Yes (the user themself, or admin/root)
- Response: User object
Update a user's username or email. Passwords are not accepted here (400).
- Auth: Yes. Self, or an administrator allowed to manage the target (admins manage regular users; only ROOT manages admins).
- Body:
{ username?, email?, current_password? }.current_passwordis required when changing your own email. - Response: Updated User object with
email_change_pending: truewhenEMAIL_VERIFYholds the change until the new address is confirmed.
Change your own password. Other sessions are signed out; the current one stays.
- Auth: Yes
- Body:
{ current_password, new_password }(current_passwordmay be omitted only by SSO-only accounts that signed in within the last 10 minutes) - Response:
{ message }
Delete a user.
- Auth: Yes (self, or an administrator allowed to manage the target; ROOT cannot be deleted)
- Response:
{ message }
Search for a user by email or username.
- Auth: No
- Body:
{ email? }or{ username? } - Response: User object (404 if not found)
curl -X POST http://localhost:8001/users/search \
-H "Content-Type: application/json" \
-d '{"email": "jdoe@example.com"}'Get the currently authenticated user.
- Auth: Yes
- Response: User object
curl http://localhost:8001/users/me/ \
-H "Authorization: Bearer $TOKEN"Create a profile for the authenticated user. The user_id and email are auto-set from the token.
- Auth: Yes
- Body:
{ given_name, family_name, nick_name?, picture?, sub, locale?, timezone? } - Response: Profile object
curl -X POST http://localhost:8001/profile/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"given_name": "Jane", "family_name": "Doe", "sub": "jdoe"}'Get the authenticated user's profile.
- Auth: Yes
- Response:
{ id, user_id, given_name, family_name, nick_name, picture, picture_key, email, sub, locale, timezone, updated_at }
Update profile fields.
- Auth: Yes
- Body:
{ given_name?, family_name?, nick_name?, picture?, locale?, timezone? } - Response: Updated Profile object
Delete the authenticated user's profile.
- Auth: Yes
- Response:
{ message }
Upload a profile picture. Accepts JPEG, PNG, WebP, or GIF (max 5 MB). Auto-resized to 400x400 and converted to WebP.
- Auth: Yes
- Content-Type:
multipart/form-data - Body: File field named
picture - Response:
{ picture, picture_key }
curl -X POST http://localhost:8001/profile/me/picture \
-H "Authorization: Bearer $TOKEN" \
-F "picture=@photo.jpg"Delete the profile picture.
- Auth: Yes
- Response:
{ message }
List which SSO providers are configured (have credentials set).
- Auth: No
- Response:
{ providers: ["google", "github", "facebook"] }
Start SSO login. Redirects the user to the provider's authorization page.
- Auth: No
- Provider:
google,github, orfacebook - Response: HTTP 302 redirect
Open in a browser:
http://localhost:8001/auth/sso/google/authorize
Handle the OAuth callback from the provider.
- Auth: No (bound to the browser by the
genesis_sso_statecookie) - Query:
code,state(orerror) set by the provider - Response: with
FRONTEND_URLset, a 302 to{FRONTEND_URL}/sso/callback#code=...(or#error=<code>); otherwise{ access_token, refresh_token, token_type }
This endpoint handles three scenarios:
- Returning SSO user - finds existing linked account, issues tokens
- Email matches an account - auto-links only if the account's email is verified in Genesis and by the provider; otherwise 409 (sign in, then link)
- New user - creates account (no password,
is_native=false), profile, and issues tokens
Trade the one-time hand-off code for tokens (single use, two minutes, same browser only).
- Auth: No (send with credentials so the
genesis_sso_handoffcookie is included) - Body:
{ code } - Response:
{ access_token, refresh_token, token_type }
Start linking an SSO provider to the authenticated user's account.
- Auth: Yes (send with credentials so the state cookie is stored)
- Response:
{ authorization_url }for the browser to visit
Unlink an SSO provider. Fails if it's the user's only authentication method (no password set).
- Auth: Yes
- Response:
{ message }
List all SSO providers linked to the authenticated user.
- Auth: Yes
- Response:
{ linked_providers: [{ provider, provider_email, provider_username, linked_at }] }
OpenID Connect discovery document.
- Auth: No
- Response: Standard OIDC discovery JSON (issuer, endpoints, supported scopes/claims/algorithms)
Authorization endpoint. Requires an authenticated user. Returns a redirect with an authorization code.
- Auth: Yes (Bearer token)
- Query params:
response_type=code,client_id,redirect_uri,scope,state,nonce?,code_challenge?,code_challenge_method? - Response: HTTP 302 redirect to
redirect_uri?code=...&state=...
Token endpoint. Supports three grant types.
- Auth: Client credentials (Basic auth or form params)
- Content-Type:
application/x-www-form-urlencoded
grant_type=authorization_code:
curl -X POST http://localhost:8001/oauth/token \
-d "grant_type=authorization_code&code=<code>&redirect_uri=<uri>&client_id=<id>&client_secret=<secret>"grant_type=refresh_token:
curl -X POST http://localhost:8001/oauth/token \
-d "grant_type=refresh_token&refresh_token=<token>"grant_type=client_credentials:
curl -X POST http://localhost:8001/oauth/token \
-d "grant_type=client_credentials&client_id=<id>&client_secret=<secret>&scope=openid"Returns user claims based on the token's scopes. Requires openid scope.
- Auth: Yes (Bearer token with
openidscope) - Response:
{ sub, email?, email_verified?, name?, given_name?, family_name?, nickname?, picture?, locale? }
curl http://localhost:8001/oauth/userinfo \
-H "Authorization: Bearer $TOKEN"Register a new OAuth client. Returns the client secret (shown once).
- Auth: Yes
- Body:
{ client_name, redirect_uris[], allowed_scopes[], allowed_audiences[], grant_types?, is_confidential? } - Response:
{ id, client_name, client_secret, redirect_uris, allowed_scopes, allowed_audiences, grant_types, is_active, is_confidential }
curl -X POST http://localhost:8001/admin/clients \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"client_name": "my-app",
"redirect_uris": ["http://localhost:3000/callback"],
"allowed_scopes": ["openid", "profile", "email"],
"allowed_audiences": ["genesis-api"]
}'List all registered OAuth clients.
- Auth: Yes
- Response: Array of OAuthClient objects
Get a specific client's details.
- Auth: Yes
- Response: OAuthClient object
Update a client's configuration.
- Auth: Yes
- Body:
{ client_name?, redirect_uris?, allowed_scopes?, allowed_audiences?, grant_types?, is_active? }
Generate a new client secret. The old secret is immediately invalidated.
- Auth: Yes
- Response:
{ client_id, client_name, client_secret, message }
| Method & path | Auth | Purpose |
|---|---|---|
GET /auth/config |
No | { email_verification, sso_providers } for the web app |
POST /auth/email/verify |
No | { token } from the email link; single use, expires |
POST /auth/email/resend |
No | { email }; always 202 so accounts are not revealed |
POST /admin/users/{id}/password |
Admin | { new_password } or { generate: true } (see temporary passwords below); admins for regular users, ROOT for admins; revokes the user's sessions |
POST /oauth/authorize |
Yes | Consent decision from the web app; returns { redirect_to } |
GET /oauth/clients/{id}/public |
No | Client name and scopes for the consent screen |
Login endpoints (/token, /admin/login) can now return 429 (account temporarily locked after repeated
failures) and, with EMAIL_VERIFY=true, 403 Email address not verified.
There is no email delivery yet, so administrators hand out temporary passwords themselves.
| Method & path | Auth | Purpose |
|---|---|---|
POST /admin/users |
Admin | { username, email, role?, password?, require_password_change?, email_verified? }. Without password a temporary one is generated and returned once as temporary_password. Admins create user accounts; ROOT may also create admin; nobody creates root. Returns 201 |
POST /admin/users/{id}/password |
Admin | { generate: true } returns a generated temporary_password; { new_password, require_password_change } sets one (temporary unless require_password_change is false). Resolves the user's open reset request |
POST /auth/password-reset-requests |
No | { identifier } (username or email). Always 202 with the same body; queues a request for an administrator when an account matches (never for ROOT) |
GET /admin/requests |
Admin | ?status=open|resolved|dismissed&category=password_reset&page=&per_page=; only requests for accounts the caller may manage |
GET /admin/requests/summary |
Admin | { open }, for the console badge |
POST /admin/requests/{id}/temporary-password |
Admin | Generates a temporary password for the request's user and resolves it |
POST /admin/requests/{id}/dismiss |
Admin | Closes the request without changing the account |
A temporary password works for TEMPORARY_PASSWORD_TTL_HOURS (default 72); after that /token and
/admin/login return 401. While an account holds one (must_change_password: true on GET /users/me/),
every other endpoint returns 403 Password change required except GET /users/me/,
POST /users/me/password (send the temporary password as current_password) and
DELETE /auth/sessions/{id}. Changing the password clears the flag.