Chapter 29 · Volume IV — Runtime Subsystems

Authentication & Identity

29.1 Overview

Kalos provides a built-in authentication and identity system through the authd plugin. It handles user registration, login, token management, password lifecycle, email verification, and role assignment. The system uses JWT bearer tokens for stateless authentication across all API endpoints.

The authd plugin is loaded before the ADL plugin in the boot sequence. By the time domain resources are available, the auth system is fully operational — user management actions are registered on the bus, and the seeder can create users and assign roles.


29.2 JWT Token Lifecycle

Token Types

TokenLifetimePurpose
Access token15 minutes (default)Authenticates API requests. Included in the Authorization: Bearer header.
Refresh token7 days (default)Obtains new access tokens without re-entering credentials. Single-use with rotation.

Token Flow

1. Client calls POST /auth/login with credentials
2. Server returns access_token + refresh_token
3. Client includes access_token in Authorization header on every request
4. When access_token expires (15 min), client calls POST /auth/refresh
5. Server returns new access_token + new refresh_token (old refresh invalidated)
6. Repeat from step 3

Token Structure

Access tokens are signed JWTs containing:

{
  "sub": "user-uuid",
  "username": "alice",
  "email": "alice@example.com",
  "roles": ["authenticated", "editor"],
  "iat": 1716000000,
  "exp": 1716000900
}

The roles array is used by the permission system (§11) to evaluate RBAC checks. The sub field is used for owner resolution.

Token Configuration

{
  "auth": {
    "jwt_secret": "${JWT_SECRET}",
    "access_token_ttl": 900,
    "refresh_token_ttl": 604800,
    "issuer": "kalos"
  }
}
KeyTypeDefaultDescription
jwt_secretstringrequiredHMAC-SHA256 signing key. MUST be at least 32 characters.
access_token_ttlinteger900Access token lifetime in seconds (15 minutes)
refresh_token_ttlinteger604800Refresh token lifetime in seconds (7 days)
issuerstring"kalos"JWT iss claim

29.3 Auth Endpoints

All auth endpoints live under /api/v1/auth/.

Login

POST /api/v1/auth/login
Content-Type: application/json

{ "username": "admin", "password": "Admin123!" }

Response (200):

{
  "status": "ok",
  "code": 200,
  "data": {
    "access_token": "eyJ...",
    "refresh_token": "a1b2c3...",
    "token_type": "Bearer",
    "access_expires_in": 900,
    "refresh_expires_in": 604800,
    "user": {
      "id": "uuid",
      "username": "admin",
      "email": "admin@example.com",
      "status": "active",
      "verified": true,
      "created_at": "2026-01-01T00:00:00Z"
    }
  }
}

The username field accepts either a username or email address. The login is case-insensitive for the username but case-sensitive for the password.

Register

POST /api/v1/auth/register
Content-Type: application/json

{
  "username": "newuser",
  "email": "newuser@example.com",
  "password": "SecurePass123!"
}

Response (201): Same shape as login response. The user is created with status: "active" and verified: false. If email verification is enabled, a verification email is sent.

Refresh

POST /api/v1/auth/refresh
Content-Type: application/json

{ "refresh_token": "a1b2c3..." }

Response (200): New access_token and refresh_token. The old refresh token is invalidated (single-use rotation). If the refresh token is expired or already used, the server returns 401.

Current User

GET /api/v1/auth/me
Authorization: Bearer <access_token>

Response (200): The authenticated user’s profile. Use on app initialization to verify the token and load user data.

Logout

POST /api/v1/auth/logout
Authorization: Bearer <access_token>

Revokes the current session. The access token is invalidated. Optionally include { "refresh_token": "..." } to target a specific refresh token.

Logout All Sessions

POST /api/v1/auth/logout-all
Authorization: Bearer <access_token>

Revokes all sessions for the current user. All access and refresh tokens are invalidated. The user must re-authenticate.


29.4 Password Management

Password Requirements

The default password policy requires:

  • Minimum 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one digit
  • At least one special character

Password policy is configurable in the server configuration.

Change Password

POST /api/v1/auth/change-password
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "current_password": "OldPass123!",
  "new_password": "NewPass456!"
}

Response (200): Success. All other sessions for the user are invalidated — the user must re-authenticate on other devices.

Forgot Password

POST /api/v1/auth/forgot-password
Content-Type: application/json

{ "email": "user@example.com" }

Response (200): Always returns success regardless of whether the email exists. This prevents email enumeration attacks. If the email exists, a password reset email is sent with a single-use token.

Reset Password

POST /api/v1/auth/reset-password
Content-Type: application/json

{
  "token": "reset-token-from-email",
  "new_password": "NewPass789!"
}

Response (200): Password updated. The reset token is single-use — it cannot be reused. All existing sessions are invalidated.


29.5 Email Verification

When email verification is enabled, newly registered users and users who change their email address receive a verification email.

Verification Flow

1. User registers or changes email
2. Server sets verified = false and sends verification email
3. Email contains a link with a single-use token
4. User clicks link → POST /auth/verify-email with token
5. Server sets verified = true

Verify Email

POST /api/v1/auth/verify-email
Content-Type: application/json

{ "token": "verification-token-from-email" }

Response (200): Email verified. The user’s verified field is set to true.

Resend Verification

POST /api/v1/auth/resend-verification
Authorization: Bearer <access_token>

Response (200): A new verification email is sent. Returns 409 if the email is already verified.

Email Change

POST /api/v1/auth/change-email
Authorization: Bearer <access_token>
Content-Type: application/json

{ "new_email": "newemail@example.com" }

Response (200): Email updated, verified set to false, verification email sent to the new address.

Email Configuration

{
  "email": {
    "enabled": true,
    "smtp_host": "smtp.example.com",
    "smtp_port": 587,
    "smtp_user": "${SMTP_USER}",
    "smtp_password": "${SMTP_PASS}",
    "from_address": "noreply@example.com",
    "use_tls": true
  }
}

When email is disabled, verification tokens are returned directly in the API response (development mode only).


29.6 User Management

Administrative endpoints for user management are available to users with the admin role.

List Users

GET /api/v1/admin/users
Authorization: Bearer <admin_token>

Get User

GET /api/v1/admin/users/:id
Authorization: Bearer <admin_token>

Set User Roles

PUT /api/v1/admin/users/:id/roles
Authorization: Bearer <admin_token>
Content-Type: application/json

{ "roles": ["editor", "reviewer"] }

Verify User (Admin)

POST /api/v1/admin/users/:id/verify
Authorization: Bearer <admin_token>

Manually marks a user as verified without the email flow.

Suspend / Unsuspend User

POST /api/v1/admin/users/:id/suspend
POST /api/v1/admin/users/:id/unsuspend

Suspended users receive 403 USER_SUSPENDED on all authenticated requests.

Delete User Account

DELETE /api/v1/auth/account
Authorization: Bearer <access_token>

Self-service account deletion. The user’s record is removed and all sessions are invalidated. Subsequent login attempts return 401.


29.7 Role Management

Roles are managed via admin endpoints:

Create Role

POST /api/v1/admin/roles
Authorization: Bearer <admin_token>
Content-Type: application/json

{ "name": "reviewer", "description": "Can review and approve content" }

List Roles

GET /api/v1/admin/roles
Authorization: Bearer <admin_token>

Built-In Roles

The authd plugin creates one built-in role on startup: admin. All other roles are created through the API or the seeder. The permission system’s special identifiers (*, authenticated, owner) are not roles — they are evaluated by the middleware directly (§11.2).


29.8 Seeder Integration

The seeder plugin creates users and assigns roles during boot (§30.4). The seeder uses the __system__ identity to bypass authentication:

roles:
  - name: editor
    description: "Content editor"
  - name: reviewer
    description: "Content reviewer"

users:
  - username: alice
    email: alice@example.com
    password: Demo123!
    roles: [editor]
    verified: true

The seeder executes in four phases: roles → users → verify/role-assign → domain data. Execution is idempotent — existing roles and users are skipped.


29.9 Auth Error Responses

HTTPError CodeMeaningClient Action
401INVALID_CREDENTIALSWrong username or passwordShow login error
401TOKEN_EXPIREDAccess token has expiredCall refresh endpoint
401TOKEN_INVALIDMalformed or revoked tokenRedirect to login
403FORBIDDENValid token but insufficient permissionsShow access denied
403USER_SUSPENDEDAccount suspended by adminShow suspension notice
409USER_EXISTSUsername or email already registeredShow duplicate error
409ALREADY_VERIFIEDEmail already verifiedNo action needed
422WEAK_PASSWORDPassword does not meet policyShow requirements
429RATE_LIMITEDToo many requestsRespect Retry-After header

End of Chapter 29.

Next: Chapter 30 — Migration System