Files
tibi-docs/src/content/docs/server/05-authentication.md
T

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"}
  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: <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) 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:

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

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

  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: <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 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:

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
}

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:

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.