Sign in

Authentication API — User Service

API reference for login, email-code MFA, token refresh, logout, current user, password reset, user administration, and OAuth2/OIDC endpoints

user
apiuserauthenticationmfajwtoauth2oidctokens

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

MethodPathAuthPurpose
POST/api/v1/users/login/NoneSign in with email and password
POST/api/v1/users/login/mfa/verify/MFA tokenComplete sign-in with an emailed code
POST/api/v1/users/login/mfa/resend/MFA tokenSend a new code
POST/api/v1/users/refresh-token/Refresh tokenGet a new access token
POST/api/v1/users/logout/Refresh tokenInvalidate the refresh token
GET/api/v1/users/me/BearerCurrent user profile
GET/api/v1/users/overview/BearerProfile with roles, permissions, organizations, and MFA status
GET, PATCH/api/v1/users/me/mfa/BearerRead or change personal MFA settings
POST/api/v1/users/send-reset-password-otp/NoneEmail a password-reset code
POST/api/v1/users/validate-reset-password-otp/NoneCheck a password-reset code
PATCH/api/v1/users/reset-password/NoneSet a new password with a validated code
—/api/v1/users/management/...Bearer + X-Org-IdUser administration
GET/.well-known/jwks.jsonNonePublic signing keys
GET/.well-known/openid-configurationNoneOIDC discovery document
GET, POST/oauth/authorize/, /oauth/token/OAuth2OAuth2/OIDC

Login

POST /api/v1/users/login/
json
{"email": "user@example.com", "password": "YOUR_PASSWORD"}

Success (200) when MFA is not required:

json
{
  "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:

json
{
  "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

StatusBodyCause
400{"non_field_errors": [...], "error": "account_disabled"}Account is deactivated
400Field errorsMissing 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/
json
{"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/
json
{"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": "...", ...}:

errorStatusMeaning
invalid_code400Wrong code; includes attempts_remaining
challenge_expired400The code or mfa_token expired; sign in again
challenge_invalid400The mfa_token is not valid
too_many_attempts429Attempts for this challenge are used up
resend_cooldown429Wait resend_after seconds before resending
resend_limit_reached429No resends left for this challenge
mfa_rate_limited429Too many challenges in the last hour
mfa_locked429Too many failures in the last day
mfa_delivery_failed503The 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:

  1. The platform enforces MFA for everyone (MFA_ENFORCE_ALL).
  2. The user is staff or a superuser and privileged enforcement is on (MFA_ENFORCE_PRIVILEGED, on by default).
  3. Any organization the user belongs to has mfa_required: true (see Organizations API).
  4. The user turned on personal email MFA.

Personal MFA settings

GET /api/v1/users/me/mfa/
json
{
  "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/
json
{"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:

json
{"refresh": "YOUR_REFRESH_TOKEN"}

Response 200:

json
{"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:

json
{"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.

  1. POST /api/v1/users/send-reset-password-otp/ with {"email": "user@example.com"}. Returns 200 with a detail message whether or not the account exists.
  2. POST /api/v1/users/validate-reset-password-otp/ with {"otp": "123456", "email": "user@example.com"} (or otp_id instead of email). Returns {"detail": "OTP validated.", "otp_id": "..."}; invalid codes return 400.
  3. 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.

MethodPathPurpose
GETmanagement/List users (page-number pagination, default 10, max 100)
POSTmanagement/Create a user
GETmanagement/{id}/User detail
PATCH, PUTmanagement/{id}/Update first_name, last_name, is_active
DELETEmanagement/{id}/Soft delete; returns {"detail": "User deleted successfully."}
POSTmanagement/{id}/activate/, management/{id}/deactivate/Enable or disable sign-in. Deactivation revokes the user's refresh tokens.
POSTmanagement/{id}/change_password/Set a password: {"password", "password_confirm"} (minimum 8 characters)
POSTmanagement/{id}/make_staff/, remove_staff/, make_superuser/, remove_superuser/, reset_mfa/Superuser only
POSTmanagement/bulk_retrieve/Fetch 1–100 users by user_ids; returns users, total_requested, total_found, not_found_ids, invalid_ids
GETmanagement/stats/User counts
GETmanagement/download_bulk_create_template/Excel template for bulk create
POSTmanagement/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:

json
{
  "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.

PathPurpose
GET /.well-known/openid-configurationDiscovery document
GET /.well-known/jwks.jsonPublic 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:

bash
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

TokenLifetime
Login access token60 minutes
Login refresh token4 hours (rotated on each refresh)
OAuth2 access token30 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):

json
{
  "sub": "3f2b...",
  "user_id": "3f2b...",
  "email": "user@example.com",
  "is_superuser": false,
  "amr": ["pwd", "otp"],
  "exp": 1767225600,
  "...": "..."
}