Authentication API
Base URL: /api/v1/auth
Handles user authentication, session management, and user administration.
Endpoints Overview
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /login | No | Authenticate user, create session |
| POST | /logout | Yes | End session |
| GET | /me | Yes | Get current user info |
| POST | /change-password | Yes | Change own password |
| POST | /request-access-link | No | Request magic link email |
| GET | /verify-token/{token} | No | Verify magic link token |
| POST | /token-login | No | Login with magic link token |
| POST | /set-password | No | Set password with token |
| POST | /create-user-for-person | Admin | Create user for existing person |
| GET | /users | Admin | List all users |
| POST | /users | Admin | Create user |
| GET | /users/{id} | Admin | Get user details |
| PATCH | /users/{id} | Admin | Update user |
| DELETE | /users/{id} | Admin | Delete user |
| POST | /users/{id}/set-password | Admin | Admin set password |
| POST | /users/{id}/toggle-active | Admin | Toggle user active status |
Login
POST /api/v1/auth/loginAuthenticate a user and create a server-side session. On success, the server sets session cookies.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User's email address |
password | string | Yes | User's password |
Example Request
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "admin@example.com",
"password": "password123"
}'Success Response (200 OK)
Cookies set:
sis_session(HttpOnly) - Opaque session IDsis_csrf- CSRF token for state-changing requestssis_logged_in- Flag for JavaScript auth state check
Body:
{
"success": true,
"data": {
"user": {
"auth_user_id": 1,
"email": "admin@example.com",
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"is_student": false,
"is_faculty": true,
"is_advisor": false,
"is_staff": true,
"is_active": true,
"is_superuser": true,
"date_joined": "2024-01-15T10:30:00Z",
"last_login": "2024-06-20T14:45:00Z",
"roles": ["Faculty", "Staff", "Admin"],
"has_photo": true,
"photo_url": "/api/v1/people/persons/550e8400-e29b-41d4-a716-446655440000/photo/file?size=display",
"thumb_url": "/api/v1/people/persons/550e8400-e29b-41d4-a716-446655440000/photo/file?size=thumb",
"via_sso": false
},
"csrf_token": "<csrf_token>"
}
}roles and the is_* flags are not authorization
roles is a list of labels derived from the legacy auth_users flags (is_student, is_faculty, is_staff, is_superuser, ...), which role assignment does not maintain. Do not decide access from it. The roles a user actually holds, and the one they are acting as, come from GET /authorization/user-roles/my_roles.
Photo URLs are prefixed with the instance's PUBLIC_API_BASE_PATH, and are absent when has_photo is false.
Error Responses
Wrong email, wrong password or disabled account (401). All three get the same message, and take the same time, so the response does not reveal whether an account exists:
{
"error": {
"code": "unauthorized",
"message": "Invalid email or password"
}
}No role usable from this network (403). The account holds roles, but none of them may be used from the client's address (Role Networks):
{
"error": {
"code": "network_role_denied",
"message": "This role is not available from the network you are connecting from",
"message_key": "errors.networkRoleDenied"
}
}Too many attempts (429). The pre-session routes are rate limited per client address.
TypeScript Types
interface LoginCredentials {
email: string
password: string
}
interface LoginResponse {
success: true
data: { user: User; csrf_token: string }
}Logout
POST /api/v1/auth/logoutEnd the current session. Deletes the session from Valkey and clears all auth cookies.
Example Request
curl -X POST http://localhost:8000/api/v1/auth/logout \
-b cookies.txtSuccess Response (200 OK)
{ "success": true, "data": { "message": "Successfully logged out" } }All auth cookies are cleared (sis_session, sis_csrf, sis_logged_in). Logout stays available even when no role is usable from the client's network.
Get Current User
GET /api/v1/auth/meGet the authenticated user's profile information.
Example Request
curl -X GET http://localhost:8000/api/v1/auth/me \
-b cookies.txtSuccess Response (200 OK)
{
"success": true,
"data": {
"auth_user_id": 1,
"email": "admin@example.com",
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"is_student": false,
"is_faculty": true,
"is_advisor": false,
"is_staff": true,
"is_active": true,
"is_superuser": true,
"date_joined": "2024-01-15T10:30:00Z",
"last_login": "2024-06-20T14:45:00Z",
"roles": ["Faculty", "Staff", "Admin"],
"has_photo": true,
"photo_url": "/api/v1/people/persons/550e8400-e29b-41d4-a716-446655440000/photo/file?size=display",
"thumb_url": "/api/v1/people/persons/550e8400-e29b-41d4-a716-446655440000/photo/file?size=thumb",
"via_sso": false,
"session_timeout_minutes": 240,
"client_ip": "10.0.50.17",
"network_restricted_role": "Super Administrator"
}
}The last five fields describe the session, not the account:
| Field | Meaning |
|---|---|
via_sso | The session was established through SAML. The UI hides Change Password |
session_timeout_minutes | The idle timeout the server enforces for this session's acting role: the smallest of SESSION_TTL, the System Settings Session Timeout and the role's own limit. Re-read it after switching role |
client_ip | The source address the server decided on |
network_restricted_role | Present only when the user's chosen role may not be used from client_ip; the request is acting under the highest-priority role that may. Names the role that could not be used |
network_role_denied | Present (true) only when no held role may be used from client_ip. Every other request except logout and stopping an impersonation is answered 403 network_role_denied |
/me is answered even in that last state, so a client can explain what is happening.
TypeScript Types
interface User {
auth_user_id: number
email: string
person_id?: string
is_student: boolean
is_faculty: boolean
is_advisor: boolean
is_staff: boolean
is_active: boolean
is_superuser: boolean
date_joined: string
last_login?: string
roles: string[]
has_photo: boolean
photo_url?: string
thumb_url?: string
via_sso: boolean
session_timeout_minutes?: number
client_ip?: string
network_restricted_role?: string
network_role_denied?: boolean
}Change Password
POST /api/v1/auth/change-passwordChange the current user's password. Requires CSRF token.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
current_password | string | Yes | Current password |
new_password | string | Yes | New password (min 12 characters) |
Example Request
curl -X POST http://localhost:8000/api/v1/auth/change-password \
-b cookies.txt \
-H "X-CSRF-Token: <csrf_token>" \
-H "Content-Type: application/json" \
-d '{
"current_password": "oldpassword123",
"new_password": "newpassword456"
}'Success Response (200 OK)
{
"message": "Password changed successfully"
}Error Responses
Wrong current password (400):
{
"error": "Current password is incorrect"
}Password too short (400):
{
"error": "New password must be at least 8 characters"
}Create User for Person (Admin)
POST /api/v1/auth/create-user-for-personCreate a user account for an existing person record. Admin only.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
person_id | string | Yes | UUID of the person record |
email | string | Yes | Email for the new account |
password | string | No | Password (auto-generated if not provided) |
auto_generate_password | boolean | No | Generate random password (default: false) |
Example Request
curl -X POST http://localhost:8000/api/v1/auth/create-user-for-person \
-b cookies.txt \
-H "X-CSRF-Token: <csrf_token>" \
-H "Content-Type: application/json" \
-d '{
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "john.smith@university.edu",
"auto_generate_password": true
}'Success Response (201 Created)
{
"user": {
"id": 25,
"email": "john.smith@university.edu",
"person_id": "550e8400-e29b-41d4-a716-446655440000"
},
"password_was_generated": true,
"generated_password": "xK9#mP2$vL5@nQ8!",
"message": "User account created successfully"
}Magic Link Authentication
These endpoints support passwordless authentication via email links. Used primarily by the applicant portal.
Request Access Link
POST /api/v1/auth/request-access-linkRequest a magic link email for authentication.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User's email address |
Response (200 OK):
{
"message": "If an account exists with this email, a magic link has been sent."
}Note: Always returns success to prevent email enumeration attacks.
Verify Token
GET /api/v1/auth/verify-token/{token}Check if a magic link token is valid.
Response (200 OK):
{
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"application_id": 123,
"token_type": "MAGIC_LINK",
"expires_at": "2024-06-22T14:45:00Z"
}Error (400):
{
"error": "Token has expired or already been used"
}Token Login
POST /api/v1/auth/token-loginExchange a valid magic link token for a session. Sets session cookies like the standard login.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Valid magic link token |
Response (200 OK):
Session cookies are set, and the response includes the user object and application context:
{
"user": { ... },
"application_id": 123
}Set Password with Token
POST /api/v1/auth/set-passwordSet password for a user using a valid magic link token. Also creates a session.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Valid magic link token |
password | string | Yes | New password (min 8 characters) |
Response (200 OK):
Session cookies are set:
{
"user": { ... },
"message": "Password set successfully"
}User Management (Admin)
These endpoints require admin privileges. All state-changing requests require the X-CSRF-Token header.
List Users
GET /api/v1/auth/usersGet a paginated list of all users.
| Parameter | Type | Description |
|---|---|---|
search | string | Search in email |
is_active | boolean | Filter by active status |
is_staff | boolean | Filter by staff status |
is_superuser | boolean | Filter by superuser status |
ordering | string | Sort by: email, date_joined, last_login, is_active |
curl -X GET "http://localhost:8000/api/v1/auth/users?is_active=true&ordering=-last_login" \
-b cookies.txtResponse (200 OK):
{
"count": 45,
"page": 1,
"page_size": 20,
"total_pages": 3,
"results": [
{
"id": 1,
"email": "admin@example.com",
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"is_staff": true,
"is_superuser": true,
"is_active": true,
"roles": ["admin", "registrar"],
"authorization_role_names": ["System Administrator", "Registrar"],
"date_joined": "2024-01-15T10:30:00Z",
"last_login": "2024-06-20T14:45:00Z"
}
]
}Create User
POST /api/v1/auth/users| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User's email address |
password | string | Yes | Password (min 12 characters) |
password_confirm | string | Yes | Password confirmation |
person_id | string | Yes | UUID of linked person record |
is_staff | boolean | No | Is staff user |
is_superuser | boolean | No | Is superuser |
is_active | boolean | No | Is active (default: true) |
role_ids | integer[] | No | Authorization role IDs to assign |
Update User
PATCH /api/v1/auth/users/{id}Writes only the fields present in the body: email, person_id, is_student, is_faculty, is_advisor, is_staff, is_active, is_superuser, role_ids. A field left out is untouched.
Send the values you loaded for the fields you are changing under _expected and the save is refused with 409 edit_conflict, writing nothing, if someone else changed one of them in the meantime:
{ "is_active": false, "_expected": { "is_active": true } }See Partial updates and edit conflicts.
Admin Set Password
POST /api/v1/auth/users/{id}/set-passwordSet a new password for a user (admin only).
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes | New password (min 8 characters) |
password_confirm | string | Yes | Password confirmation |
Toggle Active Status
POST /api/v1/auth/users/{id}/toggle-activeToggle a user's active status.
Response (200 OK):
{
"message": "User deactivated",
"is_active": false
}Complete endpoint reference
Every route this domain registers, with the permission node that gates it. Generated from the route table, so it is complete by construction — the sections above explain behaviour, this one guarantees nothing is missing.
Paths are relative to /api/v1/auth. A node in italics is an exemption rather than a grant: selfservice acts only on the caller's own record, public needs no session at all, and custom carries its own wrapper.
| Method | Endpoint | Permission node | Description |
|---|---|---|---|
| POST | /change-password | selfservice | Change password |
| POST | /create-user-for-person | ADMIN.USERS | Create user for person |
| POST | /login | custom | Login |
| POST | /logout | selfservice | Logout |
| GET | /me | selfservice | Get the signed-in user |
| POST | /request-access-link | custom | Request access link |
| POST | /saml/{slug}/acs | custom | Add acs to a SAML |
| GET | /saml/{slug}/login | custom | Login for a SAML |
| GET | /saml/{slug}/metadata | public | Get the SAML's metadata |
| POST | /set-password | custom | Set password |
| GET | /sso/idps | ADMIN.SSO_IDP | List IdPs |
| POST | /sso/idps | ADMIN.SSO_IDP | Create a IdP |
| DELETE | /sso/idps/{idpID} | ADMIN.SSO_IDP | Delete a IdP |
| GET | /sso/idps/{idpID} | ADMIN.SSO_IDP | Get one IdP |
| PATCH | /sso/idps/{idpID} | ADMIN.SSO_IDP | Update a IdP |
| POST | /sso/idps/{idpID}/refresh-metadata | ADMIN.SSO_IDP | Refresh metadata for a IdP |
| GET | /sso/providers | public | List providers |
| POST | /token-login | custom | Token login |
| GET | /users | ADMIN.USERS | List users |
| POST | /users | ADMIN.USERS | Create a user |
| DELETE | /users/{userID} | ADMIN.USERS | Delete a user |
| GET | /users/{userID} | ADMIN.USERS | Get one user |
| PATCH | /users/{userID} | ADMIN.USERS | Update a user |
| POST | /users/{userID}/set-password | ADMIN.USERS | Set password for a user |
| POST | /users/{userID}/toggle-active | ADMIN.USERS | Toggle active for a user |
| GET | /verify-token/{token} | custom | Get one verify token |