18 KiB
title
| title |
|---|
| Authentication & Authorization |
Authentication & Authorization
Overview
Tibi-Server uses JWT-based authentication with a role and permission system. Access control is configured per collection via permission sets that support public access, role-based access, token-based access, and custom permissions.
Login Flow
Username/Password Login
POST /api/v1/login
Content-Type: application/json
{"username": "admin", "password": "admin"}
- Password is hashed with MD5 and compared against the stored hash
- If matched, a JWT token and refresh token are generated
- JWT token is returned in the response body
- Refresh token is set as an HttpOnly cookie
Refresh Token Login
POST /api/v1/login
Cookie: tibi-refresh-token=eyJ...
When no username/password is provided but a refresh token cookie exists:
- The refresh token JWT is validated (HMAC-SHA256, expiration required)
- User ID is extracted from the token claims
- User is looked up by ID
- New JWT token is issued
Logout
POST /api/v1/login?logout=1
Clears the refresh token cookie (sets expiry to -1). Any non-empty logout query parameter value triggers logout.
Impersonation (Admin/OrgAdmin → User)
POST /api/v1/login/impersonate
X-Auth-Token: <admin-jwt>
Content-Type: application/json
{"userId": "<target-user-id>"}
Allows a SystemAdmin (role=0) or OrgAdmin to obtain a JWT token acting as another user. This is useful for debugging permission issues ("why can User X not see document Y?").
Security rules:
| Role | Can impersonate | Reason |
|---|---|---|
| SystemAdmin (0) | Any user | Admin already has full access |
| OrgAdmin | Users in same org(s) | OrgAdmin can already grant themselves the same team permissions |
| User (1) | Nobody | Would break the permission model |
Response (200):
{
"token": "<jwt-acting-as-target-user>",
"user": { "id": "...", "username": "targetuser", "role": 1, ... },
"impersonatedBy": {
"id": "<original-admin-id>",
"username": "admin",
"role": 0
}
}
Important: The JWT token contains an impersonatedBy field in the object claim,
which is used by the audit system to record who actually performed each action.
Every audit entry during an impersonated session will include:
userId/username/userRole→ the impersonated (target) userimpersonatedBy→ the original admin who initiated the impersonationauthMethod→"impersonation"
Logout from impersonation: Same as normal logout (POST /api/v1/login?logout=1).
The refresh token cookie is cleared, ending the impersonated session. The admin
must re-authenticate with their own credentials.
Chained impersonation: If an admin impersonates an org-admin, and that
org-admin then impersonates a user, the impersonatedBy field always points to
the original admin who started the chain — not the intermediate impersonator.
Errors:
| Status | Description |
|---|---|
| 400 | userId is required or invalid userId format |
| 403 | impersonation not allowed: insufficient role |
| 403 | impersonation not allowed: not admin of target user's org |
| 404 | target user not found |
JWT Token Structure
Access Token
| Field | Value | Description |
|---|---|---|
| Algorithm | HS256 | HMAC-SHA256 |
object |
User data | {id, username, role, permissions, orgs, teams, primaryOrgId, meta, ...} |
type |
"User" |
Token type identifier |
exp |
now + 60min | Expiration timestamp |
The password field is explicitly excluded from the token (via custom JSON marshaling).
The object includes org and team membership if the user has any:
{
"object": {
"id": "60a1b2c3d4...",
"username": "alice",
"role": 1,
"permissions": [],
"orgs": [{"orgId": "...", "role": "admin"}],
"teams": [{"teamId": "...", "role": "member"}],
"primaryOrgId": "..."
},
"type": "User",
"exp": 1700000000
}
See Orgs & Teams for details on org/team-based access control.
Auth Header:
X-Auth-Token: eyJhbGciOiJIUzI1NiIs...
Refresh Token
| Field | Value | Description |
|---|---|---|
| Algorithm | HS256 | HMAC-SHA256 |
object |
{id: "userId"} |
Minimal user reference |
type |
"RefreshTokenData" |
Token type identifier |
exp |
now + 10h | Expiration timestamp |
Cookie:
| Property | Value |
|---|---|
| Name | tibi-refresh-token |
| MaxAge | 36000 (10 hours) |
| Path | / |
| HttpOnly | true |
| SameSite | Strict |
| Secure | false |
Token Validation
Token validation includes caching — subsequent calls within the same request reuse the validated result:
// Cached in gin.Context:
// "validate-auth-token-object" → user data map
// "validate-auth-token-type" → token type string
// "validate-auth-token-err" → validation error
Admin Tokens (Server-Level)
Admin tokens provide token-based access for agents, CI/CD pipelines, and other automated systems without requiring the JWT login flow.
Configuration
api:
adminTokens:
- token: "agent-token-xyz"
label: "ci-deploy" # Optional: human-readable label for audit logs
permissions:
- project # Shorthand: all project CRUD operations
- project.reload # Reload project config
- user # Shorthand: all user CRUD operations
- server.shutdown
- namespace.myproject # All collections in myproject
- token: "readonly-token"
label: "monitoring" # Appears as tokenLabel in audit entries
permissions:
- project.read
- user.read
- namespace.*.*.read # Read-only across all namespaces and collections
- token: "importer-token"
label: "data-import"
permissions:
- namespace.myproject.articles.create # Only create in myproject/articles
- namespace.myproject.articles.update # Only update in myproject/articles
Available Permissions
| Permission | Description |
|---|---|
project |
Shorthand for all project operations (project.*) |
project.read |
GET projects |
project.create |
POST (create) projects |
project.update |
PUT (update) projects |
project.delete |
DELETE projects |
project.reload |
Reload project configuration |
user |
Shorthand for all user operations (user.*) |
user.read |
GET users |
user.create |
POST (create) users |
user.update |
PUT (update) users |
user.delete |
DELETE users |
server.shutdown |
Shutdown the server |
namespace |
Shorthand: all collections in all projects |
namespace.{project} |
Shorthand: all collections in a project |
namespace.{project}.{collection} |
Shorthand: all actions on a collection |
namespace.{project}.{collection}.read |
GET documents |
namespace.{project}.{collection}.create |
POST (create) documents |
namespace.{project}.{collection}.update |
PUT (update) documents |
namespace.{project}.{collection}.delete |
DELETE documents |
Wildcards
Use * as a wildcard for any single segment in a permission string:
| Permission | Description |
|---|---|
namespace.* |
All collections in any project |
namespace.*.articles.read |
Read articles across all projects |
namespace.myproject.*.read |
Read all collections in myproject |
namespace.*.*.read |
Read-only across everything |
Shorthand works at any level — a permission with fewer segments implies all descendants:
namespace=namespace.*.*.*(everything)namespace.myproject=namespace.myproject.*.*(all collections, all actions)namespace.myproject.articles=namespace.myproject.articles.*(all actions)
Usage
Send the token via X-Admin-Token header or Authorization: Bearer header:
# Via X-Admin-Token header — project management
POST /api/v1/project
X-Admin-Token: agent-token-xyz
Content-Type: application/json
{"name": "my-project", "description": "A new project", "configFile": "/path/to/config.yml"}
# Via Authorization: Bearer header — collection access
GET /api/v1/_/myproject/articles
Authorization: Bearer readonly-token
# Create document in a collection
POST /api/v1/_/myproject/articles
X-Admin-Token: importer-token
Content-Type: application/json
{"title": "New Article", "body": "Content..."}
Token Priority
For project/user endpoints:
- Admin Token (
X-Admin-Token/Authorization: Bearer): Checked first. If present and valid, access is granted immediately. If present but invalid, access is denied (no fallthrough). - JWT Auth (
X-Auth-Token): Checked as fallback if no admin token is provided.
For collection endpoints (/_/:project/:collection):
- Admin Token: If present, checked for
namespace.{project}.{collection}.{action}permission. If present but lacking permission, access is denied (no fallthrough to JWT or public permissions). - Collection Token Permissions: Header/query-based tokens defined in collection config.
- Public / User / JWT: Normal permission chain if no admin token is present.
Project Admin Endpoints
For project admin endpoints (reload, regenerate-search, backup, restore), three auth methods are supported (checked in order):
- Server-level admin tokens with the corresponding permission (e.g.
project.reload,project.regenerate-search,project.backup) - Project-level tokens configured in the project's
config.yml(admin.tokens[].allowReload,admin.tokens[].allowRegenerateSearch,admin.tokens[].allowBackup) - JWT login with admin role (
X-Auth-Tokenheader)
Audit Trail
All admin token actions are logged in the audit log with additional context:
| Field | Description | Example |
|---|---|---|
authMethod |
Authentication method used | admin-token, jwt |
tokenLabel |
Label from token config (if set) | ci-deploy |
tokenPrefix |
First 8 characters of the token | agent-to |
ip |
Client IP address | 192.168.1.100 |
The full token is never logged for security reasons. Use descriptive label values to identify tokens in audit logs.
Roles
| Role | Value | Description |
|---|---|---|
| Admin | 0 |
Full access to all system and collection endpoints |
| User | 1 |
Access limited to assigned projects and permitted methods |
Admin Role Behavior
- Can access all projects regardless of assignment
- Can manage all users (create, update, delete)
- Can manage all projects
- Bypasses all collection permission checks
- No projection restrictions
- No pre-filter application
- Only admins can change
role,permissions, andllmBudgetson users
User Role Behavior
- Can only see projects belonging to their orgs (via
project.orgId∈ user's org IDs) - Cannot create/update/delete projects (OrgAdmins can modify
teams[]) - Can only GET and PUT their own user profile (OrgAdmins have broader scope)
- Cannot change
username,role,permissions, orllmBudgets - Subject to collection permission checks (via team permissions)
- See Orgs & Teams for the full role hierarchy
Permission System
Each collection defines a permissions map with named permission sets:
permissions:
public:
methods:
get: true
filter:
published: true
user:
methods:
get: true
post: true
put:
allow: true
bulk: true # allow bulk PUT
delete:
allow: true
bulk: true # allow bulk DELETE
"token:apikey":
methods:
get: true
post: true
editor:
methods:
get: true
put: true # shorthand for {allow: true}
PermissionSet Structure
type PermissionSet struct {
Methods map[string]MethodPermission // Allowed methods (get, post, put, delete)
Filter dbcrud.M // Auto-applied filter for queries
ValidProjections []string // Allowed projection names
CompiledFilterSubs []*CompiledSubst // Pre-compiled $subst expressions
}
// MethodPermission represents a method permission.
// Can be unmarshalled from a boolean (true/false) or an object {allow: bool, bulk: bool}.
type MethodPermission struct {
Allow bool // Allow the method
Bulk bool // Allow bulk operations (POST array, PUT/DELETE without ID)
}
Permission Check Priority
When a request comes in, permissions are checked in this order:
1. Token-based permissions (header/query parameter match)
│
├── Token name found, value matches → Use token's permission set
├── Token name found, value WRONG → ACCESS DENIED (no fallback!)
└── No token name found → Continue
│
2. Public permissions
│
├── Method allowed → ACCESS GRANTED (no auth required)
└── Method not allowed → Continue
│
3. Auth token validation (X-Auth-Token header)
│
├── No token / invalid → ACCESS DENIED
└── Valid token → Continue
│
4. Admin role check (role = 0)
│
├── Is admin → ACCESS GRANTED
└── Not admin → Continue
│
5. Team "user" permissions (role = 1, team-assigned to project)
│
├── User is member of a team assigned to this project
│ AND collection has "user" permission set → Use "user" set
└── Otherwise → Continue
│
6. Custom user permissions
│
├── Auth permissions contain "namespace:permName" → Use that permission set
└── No match → Continue
│
7. Custom team permissions
│
├── User's teams (assigned to project) have permissions matching a collection key
│ → Use that team permission set (union of all matching teams)
└── No match → ACCESS DENIED
See Orgs & Teams — Permission System for details on team-based permission resolution.
Token-Based Permissions
Token permissions are matched against HTTP headers or query parameters:
permissions:
"token:secretkey123": # Matches ?token=secretkey123 OR header token: secretkey123
methods:
get: true
"x-api-key:my-api-key": # Matches ?x-api-key=my-api-key OR header x-api-key: my-api-key
methods:
get: true
post: true
Important: The permission key format is name:value. The name part is checked against both query parameters and headers. If the name is found but the value doesn't match, access is denied immediately with no fallback to public/user permissions.
Custom Permissions
Users can have custom permission strings in their permissions array:
{
"username": "editor1",
"role": 1,
"permissions": ["myblog:editor", "myblog:moderator"]
}
These are matched against collection permission keys using the format {namespace}:{permissionKey}:
# In project config (namespace: myblog)
permissions:
editor: # Matched by "myblog:editor"
methods:
get: true
put: true
moderator: # Matched by "myblog:moderator"
methods:
get: true
delete: true
Pre-Filters
Permission sets can define filters that are automatically applied to all queries:
permissions:
public:
methods:
get: true
filter:
published: true
deletedAt: null
user:
methods:
get: true
post: true
put: true
filter:
author:
$subst:
expr: "auth.id" # Only see own documents
How Pre-Filters Work
- The permission set's filter is deep-copied (to avoid mutation)
$substexpressions are resolved with the current request context- The filter is combined with any user-provided filter query via
$and - Admin role (0) never has pre-filters applied
Filter Combination
Final filter = $and: [
{ _id: <from URL if single document> },
{ <pre-filter from permissions> },
{ <user-provided filter query> }
]
Projection Restrictions
Permission sets can restrict which projections are available:
permissions:
public:
methods:
get: true
validProjections:
- summary # Public can only use "summary" projection
user:
methods:
get: true
validProjections:
- summary
- detailed # Users can also use "detailed"
Behavior:
- If
validProjectionsis empty/not set → no restrictions (any projection or none) - If
validProjectionsis set and noprojectionparameter → first entry is used as default - If
validProjectionsis set and invalid projection requested →400 Bad Request - Admin role (0) is never restricted
AuthData Model
Extracted from the JWT token on each authenticated request:
type AuthData struct {
ID string // User ObjectID (hex)
Username string // Username
Role int // 0=Admin, 1=User
Permissions []string // Custom permission strings
}
User Model
type User struct {
Model // ID, InsertTime, UpdateTime
Username *string // Required, alphanumeric, unique
Password *string // Required, min 3 chars (stored as MD5 hash)
Role *int // 0=Admin, 1=User (validated min=0, max=2)
Permissions []string // Custom permission strings
Meta map[string]interface{} // Arbitrary user metadata
LLMBudgets []LLMBudget // Per-provider LLM token budgets
}
User Search
The _search parameter on /api/v1/user searches across:
usernamemeta.companymeta.prenamemeta.surnamemeta.emailmeta.phone
Server-Level Admin Authentication
Server-level admin endpoints (e.g. shutdown) are protected by admin tokens with the appropriate permission:
curl -X POST http://localhost:8080/api/v1/admin/shutdown \
-H "X-Admin-Token: your-admin-token"
Admin tokens are configured via api.adminTokens in config.yml. See Configuration for details.
Personal Access Tokens (PAT)
Users can create and manage their own API tokens. A PAT uses the same
X-Auth-Token header as the JWT and is always checked live against the owner's
current rights. Tokens are a core feature and always enabled.
See 22-personal-access-tokens.md for format, scope grammar, endpoints, examples and security properties.