Skip to content

Latest commit

 

History

History
366 lines (275 loc) · 11.6 KB

File metadata and controls

366 lines (275 loc) · 11.6 KB

API usage examples

These examples exercise the public authentication lifecycle and operational endpoints against a local server. Start the application with python scripts/dev.py serve and use http://127.0.0.1:8000 as the base URL.

The generated OpenAPI document at /docs remains the source of truth for all request and response schemas.

Register a user

curl --request POST http://127.0.0.1:8000/register/ \
  --header "Content-Type: application/json" \
  --data '{"username":"alice","password":"replace-this-password","email":"alice@example.com"}'

Example response:

{
  "id": 1,
  "username": "alice",
  "role": "user",
  "email": "alice@example.com",
  "email_verified_at": null
}

Email is optional, normalized to lowercase, and unique when supplied. Usernames must also be unique. Treat passwords used in examples as disposable local values, never production credentials.

Verify an email address

Email delivery is disabled by default. After configuring SMTP, request a verification message with the same response for known, unknown, and already verified addresses:

curl --request POST http://127.0.0.1:8000/auth/email-verification/request \
  --header "Content-Type: application/json" \
  --data '{"email":"alice@example.com"}'

The email contains a time-limited opaque token. The API never returns that raw token. The frontend submits the token from the link:

curl --request POST http://127.0.0.1:8000/auth/email-verification/confirm \
  --header "Content-Type: application/json" \
  --data '{"token":"paste-token-from-verification-link"}'

Successful confirmation is single use. Expired, consumed, unknown, and wrong-purpose tokens all receive the same generic 400 response.

Reset a forgotten password

Password recovery is available only for active accounts with a verified email, but the request endpoint always returns the same 202 response. This prevents clients from discovering which accounts exist:

curl --request POST http://127.0.0.1:8000/auth/password-reset/request \
  --header "Content-Type: application/json" \
  --data '{"email":"alice@example.com"}'

The reset email contains an opaque, time-limited token. Submit it with a new password of at least 12 characters and no more than 72 UTF-8 bytes:

curl --request POST http://127.0.0.1:8000/auth/password-reset/confirm \
  --header "Content-Type: application/json" \
  --data '{"token":"paste-token-from-reset-link","new_password":"replace-with-a-long-new-password"}'

A successful reset consumes all outstanding password-reset tokens and revokes every refresh token for the account. It does not issue a new session. Existing JWT access tokens are stateless and remain valid until their configured expiry.

Log in

The login endpoint follows the OAuth2 password form convention, so its body is form encoded rather than JSON:

curl --request POST http://127.0.0.1:8000/login/ \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --header "X-Device-Name: Work laptop" \
  --data-urlencode "username=alice" \
  --data-urlencode "password=replace-this-password"

Example response:

{
  "access_token": "eyJ...",
  "refresh_token": "opaque-random-value",
  "token_type": "bearer"
}

Set the returned values in your shell for the next examples:

ACCESS_TOKEN="paste-access-token"
REFRESH_TOKEN="paste-refresh-token"

Access tokens are JWTs and are short lived. Refresh tokens are opaque secrets; the database stores only their hashes. Each login creates a separate device session, and refresh-token rotation remains inside that session family.

Enroll and use TOTP MFA

MFA is disabled by default. After the operator configures MFA_ENABLED=true and a dedicated MFA_ENCRYPTION_KEY, an authenticated user begins enrollment:

curl --request POST http://127.0.0.1:8000/auth/mfa/totp/enroll \
  --header "Authorization: Bearer ${ACCESS_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"password":"replace-this-password"}'

Scan the returned provisioning_uri, then confirm with the current six-digit code. The response displays recovery codes exactly once; store them securely.

curl --request POST http://127.0.0.1:8000/auth/mfa/totp/confirm \
  --header "Authorization: Bearer ${ACCESS_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"code":"123456"}'

Subsequent password login returns mfa_required and a short-lived challenge_token, not access or refresh tokens. Complete it with either a new TOTP code or one unused recovery code:

curl --request POST http://127.0.0.1:8000/auth/mfa/challenge/verify \
  --header "Content-Type: application/json" \
  --data '{"challenge_token":"paste-opaque-challenge","code":"123456"}'

Use GET /auth/mfa/status, POST /auth/mfa/recovery-codes/regenerate, and POST /auth/mfa/disable for lifecycle management. Regeneration and disable require the current password plus a factor and revoke refresh sessions. A recovery code is single use. A refreshed access token does not retain recent MFA status. TOTP is not phishing resistant; prefer WebAuthn/passkeys when that property is required.

Sign in and link accounts with OpenID Connect

After registering the exact callback URI with a provider and enabling OIDC, open the authorization endpoint in a browser:

curl --include http://127.0.0.1:8000/auth/oidc/authorize

The API returns a 303 redirect to the configured provider and sets a short-lived HttpOnly browser-binding cookie. The authorization request uses a transaction-specific state, nonce, and PKCE S256 challenge. The provider redirects the same browser to /auth/oidc/callback; the API validates and consumes the transaction before returning local access and refresh tokens.

To link a provider identity to an existing account, begin from a recently authenticated access token:

curl --request POST --include \
  http://127.0.0.1:8000/auth/oidc/link/authorize \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

A refresh-issued access token cannot begin linking. The callback binds the provider's immutable issuer and subject to the authenticated user. A matching email never silently links an existing account. List or unlink identities with:

curl http://127.0.0.1:8000/auth/oidc/identities \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

curl --request DELETE \
  http://127.0.0.1:8000/auth/oidc/identities/1 \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

Link and unlink operations revoke refresh sessions. The last sign-in method of an OIDC-only account cannot be removed. If local MFA is enabled, a successful OIDC callback still returns an MFA challenge rather than bypassing the second factor.

Read the current user

curl http://127.0.0.1:8000/auth/me \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

Example response:

{
  "id": 1,
  "username": "alice",
  "role": "user"
}

Missing, malformed, expired, or otherwise invalid access tokens return 401.

Rotate a refresh token

curl --request POST http://127.0.0.1:8000/auth/refresh \
  --header "Content-Type: application/json" \
  --data "{\"refresh_token\":\"${REFRESH_TOKEN}\"}"

The response has the same shape as login. Replace both local token variables with the new values. A successful rotation revokes the submitted refresh token, so replaying it returns 401.

Log out

curl --request POST http://127.0.0.1:8000/auth/logout \
  --header "Content-Type: application/json" \
  --data "{\"refresh_token\":\"${REFRESH_TOKEN}\"}"

Logout revokes the refresh token. Existing access tokens remain valid until their configured expiration; clients should discard both tokens locally. The server revokes the submitted token's complete rotation family.

Manage device sessions

List active refresh-token families for the authenticated user:

curl http://127.0.0.1:8000/auth/sessions \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

The response contains a server-generated session ID, bounded device label, and created/last-used/expiry timestamps. It never contains raw tokens, token hashes, or IP addresses. Revoke one family idempotently:

SESSION_ID="paste-session-id"
curl --request DELETE "http://127.0.0.1:8000/auth/sessions/${SESSION_ID}" \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

Revoke every refresh-token session:

curl --request DELETE http://127.0.0.1:8000/auth/sessions \
  --header "Authorization: Bearer ${ACCESS_TOKEN}"

Replaying a refresh token already consumed by rotation revokes the live token in that family. Session revocation does not immediately invalidate stateless JWT access tokens; they remain valid until their configured expiry.

Call an admin endpoint

Admin routes require an access token whose current database user has the admin role. Registration never grants this role and the API does not provide a public self-promotion path.

ADMIN_ACCESS_TOKEN="paste-admin-access-token"

curl http://127.0.0.1:8000/admin/users \
  --header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}"

To change an existing user's role as an admin:

curl --request PATCH http://127.0.0.1:8000/admin/users/1/role \
  --header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"role":"admin"}'

To disable an account and atomically revoke its refresh-token sessions:

curl --request PATCH http://127.0.0.1:8000/admin/users/1/status \
  --header "Authorization: Bearer ${ADMIN_ACCESS_TOKEN}" \
  --header "Content-Type: application/json" \
  --data '{"is_active":false}'

Set is_active to true to allow new authentication again. Re-enabling an account does not restore revoked sessions; the user must sign in again.

Non-admin users receive 403; an unknown user ID returns 404.

Inspect health and metrics

curl --fail http://127.0.0.1:8000/health/live
curl --fail http://127.0.0.1:8000/health/ready
curl --fail http://127.0.0.1:8000/metrics

Liveness confirms that the process can respond. Readiness additionally checks database connectivity. Restrict /metrics to trusted monitoring networks in production.

Trace a request with a correlation ID

curl --include http://127.0.0.1:8000/health/live \
  --header "X-Request-ID: docs-example-001"

The response includes the validated X-Request-ID, and the same value appears in the structured request log. Invalid IDs are replaced rather than trusted.

PowerShell authentication flow

$baseUrl = "http://127.0.0.1:8000"

Invoke-RestMethod -Method Post -Uri "$baseUrl/register/" `
  -ContentType "application/json" `
  -Body '{"username":"alice","password":"replace-this-password"}'

$tokens = Invoke-RestMethod -Method Post -Uri "$baseUrl/login/" `
  -ContentType "application/x-www-form-urlencoded" `
  -Body @{ username = "alice"; password = "replace-this-password" }

$headers = @{ Authorization = "Bearer $($tokens.access_token)" }
Invoke-RestMethod -Uri "$baseUrl/auth/me" -Headers $headers

$rotated = Invoke-RestMethod -Method Post -Uri "$baseUrl/auth/refresh" `
  -ContentType "application/json" `
  -Body (@{ refresh_token = $tokens.refresh_token } | ConvertTo-Json)

Invoke-RestMethod -Method Post -Uri "$baseUrl/auth/password-reset/request" `
  -ContentType "application/json" `
  -Body '{"email":"alice@example.com"}'

Error response conventions

Expected client errors use a JSON detail field and include X-Request-ID:

{
  "detail": "Invalid username or password"
}

Unexpected errors return a generic 500 response without leaking internal exception details. Use the correlation ID to locate the matching structured log entry.