--- 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"} ``` 1. Password is hashed with MD5 and compared against the stored hash 2. If matched, a JWT token and refresh token are generated 3. JWT token is returned in the response body 4. 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: 1. The refresh token JWT is validated (HMAC-SHA256, expiration required) 2. User ID is extracted from the token claims 3. User is looked up by ID 4. 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: Content-Type: application/json {"userId": ""} ``` 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):** ```json { "token": "", "user": { "id": "...", "username": "targetuser", "role": 1, ... }, "impersonatedBy": { "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) user - `impersonatedBy` → the original admin who initiated the impersonation - `authMethod` → `"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: ```json { "object": { "id": "60a1b2c3d4...", "username": "alice", "role": 1, "permissions": [], "orgs": [{"orgId": "...", "role": "admin"}], "teams": [{"teamId": "...", "role": "member"}], "primaryOrgId": "..." }, "type": "User", "exp": 1700000000 } ``` See [Orgs & Teams](18-orgs-teams.md) 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: ```go // 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 ```yaml 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: 1. **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). 2. **JWT Auth** (`X-Auth-Token`): Checked as fallback if no admin token is provided. For collection endpoints (`/_/:project/:collection`): 1. **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). 2. **Collection Token Permissions**: Header/query-based tokens defined in collection config. 3. **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): 1. Server-level admin tokens with the corresponding permission (e.g. `project.reload`, `project.regenerate-search`, `project.backup`) 2. Project-level tokens configured in the project's `config.yml` (`admin.tokens[].allowReload`, `admin.tokens[].allowRegenerateSearch`, `admin.tokens[].allowBackup`) 3. JWT login with admin role (`X-Auth-Token` header) ### 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`, and `llmBudgets` on 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`, or `llmBudgets` - Subject to collection permission checks (via team permissions) - See [Orgs & Teams](18-orgs-teams.md) for the full role hierarchy --- ## Permission System Each collection defines a `permissions` map with named permission sets: ```yaml 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 ```go 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](18-orgs-teams.md#permission-system) for details on team-based permission resolution. ### Token-Based Permissions Token permissions are matched against HTTP headers or query parameters: ```yaml 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: ```json { "username": "editor1", "role": 1, "permissions": ["myblog:editor", "myblog:moderator"] } ``` These are matched against collection permission keys using the format `{namespace}:{permissionKey}`: ```yaml # 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: ```yaml 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 1. The permission set's filter is deep-copied (to avoid mutation) 2. `$subst` expressions are resolved with the current request context 3. The filter is combined with any user-provided filter query via `$and` 4. Admin role (0) never has pre-filters applied ### Filter Combination ``` Final filter = $and: [ { _id: }, { }, { } ] ``` --- ## Projection Restrictions Permission sets can restrict which projections are available: ```yaml 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 `validProjections` is empty/not set → no restrictions (any projection or none) - If `validProjections` is set and no `projection` parameter → first entry is used as default - If `validProjections` is 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: ```go type AuthData struct { ID string // User ObjectID (hex) Username string // Username Role int // 0=Admin, 1=User Permissions []string // Custom permission strings } ``` --- ## User Model ```go 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: - `username` - `meta.company` - `meta.prename` - `meta.surname` - `meta.email` - `meta.phone` --- ## Server-Level Admin Authentication Server-level admin endpoints (e.g. shutdown) are protected by admin tokens with the appropriate permission: ```bash 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](02-configuration.md) 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](22-personal-access-tokens.md) for format, scope grammar, endpoints, examples and security properties.