Skip to content

Authentication API ​

Base URL: /api/v1/auth

Handles user authentication, session management, and user administration.

Endpoints Overview ​

MethodEndpointAuthDescription
POST/loginNoAuthenticate user, create session
POST/logoutYesEnd session
GET/meYesGet current user info
POST/change-passwordYesChange own password
POST/request-access-linkNoRequest magic link email
GET/verify-token/{token}NoVerify magic link token
POST/token-loginNoLogin with magic link token
POST/set-passwordNoSet password with token
POST/create-user-for-personAdminCreate user for existing person
GET/usersAdminList all users
POST/usersAdminCreate user
GET/users/{id}AdminGet user details
PATCH/users/{id}AdminUpdate user
DELETE/users/{id}AdminDelete user
POST/users/{id}/set-passwordAdminAdmin set password
POST/users/{id}/toggle-activeAdminToggle user active status

Login ​

POST /api/v1/auth/login

Authenticate a user and create a server-side session. On success, the server sets session cookies.

Request Body ​

FieldTypeRequiredDescription
emailstringYesUser's email address
passwordstringYesUser's password

Example Request ​

bash
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 ID
  • sis_csrf - CSRF token for state-changing requests
  • sis_logged_in - Flag for JavaScript auth state check

Body:

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

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

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

typescript
interface LoginCredentials {
  email: string
  password: string
}

interface LoginResponse {
  success: true
  data: { user: User; csrf_token: string }
}

Logout ​

POST /api/v1/auth/logout

End the current session. Deletes the session from Valkey and clears all auth cookies.

Example Request ​

bash
curl -X POST http://localhost:8000/api/v1/auth/logout \
  -b cookies.txt

Success Response (200 OK) ​

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

Get the authenticated user's profile information.

Example Request ​

bash
curl -X GET http://localhost:8000/api/v1/auth/me \
  -b cookies.txt

Success Response (200 OK) ​

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

FieldMeaning
via_ssoThe session was established through SAML. The UI hides Change Password
session_timeout_minutesThe 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_ipThe source address the server decided on
network_restricted_rolePresent 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_deniedPresent (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 ​

typescript
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-password

Change the current user's password. Requires CSRF token.

Request Body ​

FieldTypeRequiredDescription
current_passwordstringYesCurrent password
new_passwordstringYesNew password (min 12 characters)

Example Request ​

bash
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) ​

json
{
  "message": "Password changed successfully"
}

Error Responses ​

Wrong current password (400):

json
{
  "error": "Current password is incorrect"
}

Password too short (400):

json
{
  "error": "New password must be at least 8 characters"
}

Create User for Person (Admin) ​

POST /api/v1/auth/create-user-for-person

Create a user account for an existing person record. Admin only.

Request Body ​

FieldTypeRequiredDescription
person_idstringYesUUID of the person record
emailstringYesEmail for the new account
passwordstringNoPassword (auto-generated if not provided)
auto_generate_passwordbooleanNoGenerate random password (default: false)

Example Request ​

bash
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) ​

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

These endpoints support passwordless authentication via email links. Used primarily by the applicant portal.

POST /api/v1/auth/request-access-link

Request a magic link email for authentication.

FieldTypeRequiredDescription
emailstringYesUser's email address

Response (200 OK):

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

json
{
  "person_id": "550e8400-e29b-41d4-a716-446655440000",
  "application_id": 123,
  "token_type": "MAGIC_LINK",
  "expires_at": "2024-06-22T14:45:00Z"
}

Error (400):

json
{
  "error": "Token has expired or already been used"
}

Token Login ​

POST /api/v1/auth/token-login

Exchange a valid magic link token for a session. Sets session cookies like the standard login.

FieldTypeRequiredDescription
tokenstringYesValid magic link token

Response (200 OK):

Session cookies are set, and the response includes the user object and application context:

json
{
  "user": { ... },
  "application_id": 123
}

Set Password with Token ​

POST /api/v1/auth/set-password

Set password for a user using a valid magic link token. Also creates a session.

FieldTypeRequiredDescription
tokenstringYesValid magic link token
passwordstringYesNew password (min 8 characters)

Response (200 OK):

Session cookies are set:

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

Get a paginated list of all users.

ParameterTypeDescription
searchstringSearch in email
is_activebooleanFilter by active status
is_staffbooleanFilter by staff status
is_superuserbooleanFilter by superuser status
orderingstringSort by: email, date_joined, last_login, is_active
bash
curl -X GET "http://localhost:8000/api/v1/auth/users?is_active=true&ordering=-last_login" \
  -b cookies.txt

Response (200 OK):

json
{
  "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
FieldTypeRequiredDescription
emailstringYesUser's email address
passwordstringYesPassword (min 12 characters)
password_confirmstringYesPassword confirmation
person_idstringYesUUID of linked person record
is_staffbooleanNoIs staff user
is_superuserbooleanNoIs superuser
is_activebooleanNoIs active (default: true)
role_idsinteger[]NoAuthorization 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:

json
{ "is_active": false, "_expected": { "is_active": true } }

See Partial updates and edit conflicts.


Admin Set Password ​

POST /api/v1/auth/users/{id}/set-password

Set a new password for a user (admin only).

FieldTypeRequiredDescription
passwordstringYesNew password (min 8 characters)
password_confirmstringYesPassword confirmation

Toggle Active Status ​

POST /api/v1/auth/users/{id}/toggle-active

Toggle a user's active status.

Response (200 OK):

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

MethodEndpointPermission nodeDescription
POST/change-passwordselfserviceChange password
POST/create-user-for-personADMIN.USERSCreate user for person
POST/logincustomLogin
POST/logoutselfserviceLogout
GET/meselfserviceGet the signed-in user
POST/request-access-linkcustomRequest access link
POST/saml/{slug}/acscustomAdd acs to a SAML
GET/saml/{slug}/logincustomLogin for a SAML
GET/saml/{slug}/metadatapublicGet the SAML's metadata
POST/set-passwordcustomSet password
GET/sso/idpsADMIN.SSO_IDPList IdPs
POST/sso/idpsADMIN.SSO_IDPCreate a IdP
DELETE/sso/idps/{idpID}ADMIN.SSO_IDPDelete a IdP
GET/sso/idps/{idpID}ADMIN.SSO_IDPGet one IdP
PATCH/sso/idps/{idpID}ADMIN.SSO_IDPUpdate a IdP
POST/sso/idps/{idpID}/refresh-metadataADMIN.SSO_IDPRefresh metadata for a IdP
GET/sso/providerspublicList providers
POST/token-logincustomToken login
GET/usersADMIN.USERSList users
POST/usersADMIN.USERSCreate a user
DELETE/users/{userID}ADMIN.USERSDelete a user
GET/users/{userID}ADMIN.USERSGet one user
PATCH/users/{userID}ADMIN.USERSUpdate a user
POST/users/{userID}/set-passwordADMIN.USERSSet password for a user
POST/users/{userID}/toggle-activeADMIN.USERSToggle active for a user
GET/verify-token/{token}customGet one verify token

Student Information System Documentation