Login
POST /api/v1/auth/login
Rate limit: 5 requests per minute per IP.
Authenticates a user and sets osprey_access and osprey_refresh cookies.
Request body:
{
"username": "admin",
"password": "admin"
}
Response: 200 OK (sets cookies)
{
"user": {
"id": "uuid",
"username": "admin",
"display_name": "",
"role": "admin",
"is_active": true,
"force_password_change": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
},
"default_password_warning": true,
"force_password_change": true
}
Notes:
force_password_change is true when the user must change their password before accessing other endpoints (e.g., default admin on first login, or after an admin password reset). All authenticated endpoints except auth management return 403 until the password is changed.
- Login lockout is persistent (DB-backed). After 5 consecutive failures (configurable via
security.max_login_attempts), the account is locked for the configured security.lockout_duration_minutes (default 15 minutes). Lockout survives API restarts.
- Password change and admin password reset invalidate all existing refresh tokens and sessions for the user, forcing re-login on all devices.
- SSO-provisioned accounts (
auth_source oidc/saml) cannot log in with a password: the attempt gets the same generic 401 as a wrong password (no oracle), counts toward lockout, and is audited. Only the osprey auth reset-password recovery CLI converts such an account back to local.
- LDAP: when enabled LDAP providers exist, an unknown username and any
auth_source=ldap account authenticate against the directory through this same endpoint (bind-auth; first login JIT-provisions the account). The F6 separation is strict: a local account is never tried against LDAP — a typo'd local password cannot leak to AD and AD-lockout amplification is impossible. Wrong directory passwords count toward Osprey's lockout; directory outages do not (they audit as ldap_error). A directory login refused by role mapping returns 403.
- The
auth.local_login system setting gates password login: enabled (default) allows all roles; admins_only and disabled both return 403 for non-admin users after successful password verification (no credential oracle); disabled additionally hides the login form in the UI (break-glass via /?local=1). Admins can always password-log-in — a dead IdP must never lock everyone out.
- The cookie
Max-Age values follow the configured auth.access_token_ttl and refresh_token_ttl.
- All login outcomes are audited (
login, login_failed with a reason, entity type auth).
- MFA step-up: when the account has confirmed TOTP (or its role is in
auth.mfa.required_roles), a correct password does not issue a session. The response is 200 {"mfa_required": true, "mfa_token": "…"} and the client must call POST /auth/login/mfa. The mfa_token is a short-lived (2 min), single-use, typ/aud-bound JWT that is never accepted as a session cookie; the login lockout counter is cleared only after the full flow completes (an attacker with the password but no code cannot reset it). LDAP-sourced accounts are step-up-enforced the same way (a simple bind has no IdP-side second factor); OIDC/SAML do MFA at their IdP.
- MFA enroll-at-login: when
auth.mfa.required_roles mandates MFA for the user's role but no authenticator is enrolled yet, the response is 200 {"mfa_enroll_required": true, "mfa_token": "…"} instead — the role is not locked out. The client drives enroll-at-login with the mfa_token; a session is issued only after the first code is verified (see below).