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
| Token | Lifetime | Purpose |
|---|---|---|
| Access token | 15 minutes (default) | Authenticates API requests. Included in the Authorization: Bearer header. |
| Refresh token | 7 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"
}
}
| Key | Type | Default | Description |
|---|---|---|---|
jwt_secret | string | required | HMAC-SHA256 signing key. MUST be at least 32 characters. |
access_token_ttl | integer | 900 | Access token lifetime in seconds (15 minutes) |
refresh_token_ttl | integer | 604800 | Refresh token lifetime in seconds (7 days) |
issuer | string | "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
| HTTP | Error Code | Meaning | Client Action |
|---|---|---|---|
| 401 | INVALID_CREDENTIALS | Wrong username or password | Show login error |
| 401 | TOKEN_EXPIRED | Access token has expired | Call refresh endpoint |
| 401 | TOKEN_INVALID | Malformed or revoked token | Redirect to login |
| 403 | FORBIDDEN | Valid token but insufficient permissions | Show access denied |
| 403 | USER_SUSPENDED | Account suspended by admin | Show suspension notice |
| 409 | USER_EXISTS | Username or email already registered | Show duplicate error |
| 409 | ALREADY_VERIFIED | Email already verified | No action needed |
| 422 | WEAK_PASSWORD | Password does not meet policy | Show requirements |
| 429 | RATE_LIMITED | Too many requests | Respect Retry-After header |
End of Chapter 29.
Next: Chapter 30 — Migration System