Authentication API — User Service
API reference for login, email-code MFA, token refresh, logout, current user, password reset, user administration, and OAuth2/OIDC endpoints
Updated 2026-11-03
Verified 2026-11-03
User Service endpoints for signing in, managing tokens, and administering users. For architecture and configuration, see User Service.
Base URLs, the Authorization and X-Org-Id headers, error shapes, and pagination are described in API Conventions. Paths below are relative to the User Service base URL (local: http://localhost:8101).
Endpoint Summary
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /api/v1/users/login/ | None | Sign in with email and password |
| POST | /api/v1/users/login/mfa/verify/ | MFA token | Complete sign-in with an emailed code |
| POST | /api/v1/users/login/mfa/resend/ | MFA token | Send a new code |
| POST | /api/v1/users/refresh-token/ | Refresh token | Get a new access token |
| POST | /api/v1/users/logout/ | Refresh token | Invalidate the refresh token |
| GET | /api/v1/users/me/ | Bearer | Current user profile |
| GET | /api/v1/users/overview/ | Bearer | Profile with roles, permissions, organizations, and MFA status |
| GET, PATCH | /api/v1/users/me/mfa/ | Bearer | Read or change personal MFA settings |
| POST | /api/v1/users/send-reset-password-otp/ | None | Email a password-reset code |
| POST | /api/v1/users/validate-reset-password-otp/ | None | Check a password-reset code |
| PATCH | /api/v1/users/reset-password/ | None | Set a new password with a validated code |
| — | /api/v1/users/management/... | Bearer + X-Org-Id | User administration |
| GET | /.well-known/jwks.json | None | Public signing keys |
| GET | /.well-known/openid-configuration | None | OIDC discovery document |
| GET, POST | /oauth/authorize/, /oauth/token/ | OAuth2 | OAuth2/OIDC |
Login
POST /api/v1/users/login/
{"email": "user@example.com", "password": "YOUR_PASSWORD"}
Success (200) when MFA is not required:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 3600
}
By default (cookie mode) the refresh token is set as the httpOnly cosmos_refresh cookie and is not in the body. Send X-Token-Mode: body to receive refresh_token and refresh_token_expires_in in the JSON body instead, for non-browser clients.
MFA required (401). No tokens are issued. The response carries a short-lived mfa_token for the next step:
{
"error": "mfa_required",
"detail": "Enter the verification code we sent to your email.",
"mfa_token": "YOUR_MFA_TOKEN",
"methods": ["email"],
"masked_email": "u***@example.com",
"expires_in": 600,
"resend_after": 30,
"resends_remaining": 3,
"attempts_remaining": 5
}
Other errors
| Status | Body | Cause |
|---|---|---|
400 | {"non_field_errors": [...], "error": "account_disabled"} | Account is deactivated |
400 | Field errors | Missing fields, or a non-superuser with no organization membership |
401 | {"detail": "..."} | Wrong email or password |
Email-Code MFA
MFA uses one-time codes sent by email. When it is required, sign-in is a two-step flow: login/ returns mfa_required, then the client submits the emailed code.
Verify code
POST /api/v1/users/login/mfa/verify/
{"mfa_token": "YOUR_MFA_TOKEN", "code": "123456"}
Returns the same token response as a successful login (cookie or body mode).
Resend code
POST /api/v1/users/login/mfa/resend/
{"mfa_token": "YOUR_MFA_TOKEN"}
Returns 200 with detail and the refreshed challenge fields (expires_in, resend_after, resends_remaining, attempts_remaining).
MFA errors
MFA errors use {"error": "<code>", "detail": "...", ...}:
error | Status | Meaning |
|---|---|---|
invalid_code | 400 | Wrong code; includes attempts_remaining |
challenge_expired | 400 | The code or mfa_token expired; sign in again |
challenge_invalid | 400 | The mfa_token is not valid |
too_many_attempts | 429 | Attempts for this challenge are used up |
resend_cooldown | 429 | Wait resend_after seconds before resending |
resend_limit_reached | 429 | No resends left for this challenge |
mfa_rate_limited | 429 | Too many challenges in the last hour |
mfa_locked | 429 | Too many failures in the last day |
mfa_delivery_failed | 503 | The code email could not be sent |
Default limits: codes are valid for 10 minutes, 5 attempts per code, 30 seconds between resends, 3 resends, 10 challenges per hour, and 20 failures per day.
When MFA is required
MFA applies only when the platform has it enabled (MFA_ENABLED; enabled in Azure environments). A user must complete MFA when any of these apply:
- The platform enforces MFA for everyone (
MFA_ENFORCE_ALL). - The user is staff or a superuser and privileged enforcement is on (
MFA_ENFORCE_PRIVILEGED, on by default). - Any organization the user belongs to has
mfa_required: true(see Organizations API). - The user turned on personal email MFA.
Personal MFA settings
GET /api/v1/users/me/mfa/
{
"platform_enabled": true,
"methods": ["email"],
"enabled": false,
"required": true,
"required_reason": "org",
"required_by_orgs": [{"id": "YOUR_ORG_ID", "name": "Acme"}],
"can_disable": false
}
PATCH /api/v1/users/me/mfa/
{"enabled": false, "password": "YOUR_PASSWORD"}
required_reason is one of platform, privileged, org, user, bypass, or null. The current password is required to turn MFA off. Errors: 400 invalid_password, 409 mfa_required_by_policy (a policy or organization requires MFA). Changes are throttled to 10 per hour.
Token Refresh
POST /api/v1/users/refresh-token/
In cookie mode, send an empty body with credentials so the browser includes cosmos_refresh. In body mode, send X-Token-Mode: body and:
{"refresh": "YOUR_REFRESH_TOKEN"}
Response 200:
{"access_token": "eyJhbGciOiJSUzI1NiIs...", "expires_in": 3600}
Refresh tokens rotate: each refresh issues a new refresh token (written back to the cookie, or returned in the body in body mode). An invalid or expired refresh token returns 401. The body can include error: "account_disabled" or error: "mfa_reauth_required"; the latter means the user must sign in again and complete MFA.
Logout
POST /api/v1/users/logout/
Send the refresh token as {"refresh_token": "..."} or rely on the cookie. The cookie is cleared. Response 200:
{"detail": "Successfully logged out."}
Current User
GET /api/v1/users/me/
Returns id, email, first_name, last_name, is_active, is_staff, and is_superuser. The profile is read-only here; administrators change names through User administration.
GET /api/v1/users/overview/
Returns the same profile plus roles, permissions, organizations (each with mfa_required), and an mfa object shaped like the personal MFA settings response.
Password Reset
Password reset is a three-step, unauthenticated flow. Requests are throttled to 5 per hour.
POST /api/v1/users/send-reset-password-otp/with{"email": "user@example.com"}. Returns200with adetailmessage whether or not the account exists.POST /api/v1/users/validate-reset-password-otp/with{"otp": "123456", "email": "user@example.com"}(orotp_idinstead ofemail). Returns{"detail": "OTP validated.", "otp_id": "..."}; invalid codes return400.PATCH /api/v1/users/reset-password/with{"password": "NEW_PASSWORD", "otp_id": "..."}.
The OAuth2 login pages offer the same flow at /oauth/forgot-password/, /oauth/verify-otp/, and /oauth/reset-password/.
User Administration
User administration lives under /api/v1/users/management/ and requires Authorization and X-Org-Id. Non-superusers only see and manage users in the organization named by X-Org-Id; operations need the matching user_* permission.
| Method | Path | Purpose |
|---|---|---|
| GET | management/ | List users (page-number pagination, default 10, max 100) |
| POST | management/ | Create a user |
| GET | management/{id}/ | User detail |
| PATCH, PUT | management/{id}/ | Update first_name, last_name, is_active |
| DELETE | management/{id}/ | Soft delete; returns {"detail": "User deleted successfully."} |
| POST | management/{id}/activate/, management/{id}/deactivate/ | Enable or disable sign-in. Deactivation revokes the user's refresh tokens. |
| POST | management/{id}/change_password/ | Set a password: {"password", "password_confirm"} (minimum 8 characters) |
| POST | management/{id}/make_staff/, remove_staff/, make_superuser/, remove_superuser/, reset_mfa/ | Superuser only |
| POST | management/bulk_retrieve/ | Fetch 1–100 users by user_ids; returns users, total_requested, total_found, not_found_ids, invalid_ids |
| GET | management/stats/ | User counts |
| GET | management/download_bulk_create_template/ | Excel template for bulk create |
| POST | management/bulk_create/ | Upload an .xlsx file (up to 5 MB) |
List filters: search, email, first_name, last_name, is_active, is_staff, is_superuser, plus page and page_size.
Create body:
{
"email": "new.user@example.com",
"first_name": "New",
"last_name": "User",
"password": "YOUR_PASSWORD",
"password_confirm": "YOUR_PASSWORD",
"is_active": true
}
List and detail responses include mfa_email_enabled; detail also includes mfa_enabled_at and last_login.
Bulk create columns: email, first_name, last_name, password, org_id, role_name. Rows that fail are reported individually; valid rows are still created.
OAuth2 and OIDC
User Service is an OAuth2 authorization server and OIDC provider built on django-oauth-toolkit. OAuth2 access tokens are JWTs signed with the same key as login tokens, so every service validates them through JWKS.
| Path | Purpose |
|---|---|
GET /.well-known/openid-configuration | Discovery document |
GET /.well-known/jwks.json | Public RSA keys (RS256) |
GET /oauth/authorize/ | Authorization endpoint. Unauthenticated users are sent to /oauth/login/, which applies the same email-code MFA step as API login. |
POST /oauth/token/ | Token endpoint. The authorization-code flow requires PKCE; service clients use client_credentials. |
/oauth/logout/ | End the OAuth login session |
OAuth2 access tokens expire after 30 minutes (OAUTH2_ACCESS_TOKEN_EXPIRE_SECONDS). Supported scopes include openid, read, write, admin, chat:read, chat:write, storage:read, storage:write, agent:execute, agent:manage, tools:call, observer:read, observer:write, projects:read, projects:manage, and projects:create.
Service-to-service calls use client_credentials:
curl -s -X POST https://<user-service-host>/oauth/token/ \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
The Django admin login (/admin/login/) redirects to the MFA-gated OAuth login.
Token Lifetimes and Claims
| Token | Lifetime |
|---|---|
| Login access token | 60 minutes |
| Login refresh token | 4 hours (rotated on each refresh) |
| OAuth2 access token | 30 minutes |
MFA challenge (mfa_token and code) | 10 minutes |
Access tokens are RS256 JWTs. User tokens carry sub, user_id, email, first_name, last_name, is_superuser, amr (["pwd"], or ["pwd", "otp"] after MFA), auth_time, and the standard exp, iat, and jti claims. Organization roles and permissions are not embedded in the token; services check them through the User Service authorization endpoints. Service tokens carry actor_type: "service" and the calling client's ID.
Example payload (truncated):
{
"sub": "3f2b...",
"user_id": "3f2b...",
"email": "user@example.com",
"is_superuser": false,
"amr": ["pwd", "otp"],
"exp": 1767225600,
"...": "..."
}