Authentication#
RAGFlow's authentication system is implemented in api/apps/__init__.py and supports four credential methods with a priority-based fallback chain. All protected routes use the @login_required decorator, which calls _load_user() on every request.
Auth Types and Constants#
Three auth-type string constants are defined at module level :
| Constant | Value | Default? |
|---|---|---|
AUTH_JWT | "JWT" | ✅ |
AUTH_API | "API" | ✅ |
AUTH_BETA | "BETA" | ❌ (opt-in only) |
DEFAULT_AUTH_TYPES = (AUTH_JWT, AUTH_API). Routes that need beta-token support must pass auth_types=[AUTH_BETA] explicitly to @login_required.
Resolution Order in _load_user()#
_load_user() tries credentials in this fixed sequence :
- Cache hit — if
g.useris already populated and the auth type matches, return immediately. - No
Authorizationheader → fall through to session cookie (only ifAUTH_JWTis in the allowed set). - Beta token (if
AUTH_BETAin allowed set) — looks upAPIToken.query(beta=auth_token). Setsg.auth_type = "BETA". - JWT (if
AUTH_JWTin allowed set) — decodes the bearer token usingitsdangerous.URLSafeTimedSerializerwithsettings.get_secret_key(), then queriesUserServicebyaccess_token. Setsg.auth_type = "JWT". - API key (if
AUTH_APIin allowed set) — looks upAPIToken.query(token=auth_token)as a plain API token. Setsg.auth_type = "API".
The resolved auth type is written to g.auth_type; route handlers can inspect this if needed.
Session Cookie Fallback#
_load_user_from_session() is called only when the Authorization header is absent and AUTH_JWT is permitted. It reads _user_id from the Quart session (Redis-backed) and validates the associated access_token — rejecting empty values, values shorter than 32 chars, or values prefixed INVALID_. This path exists to support OAuth/OIDC redirect flows where the frontend may clear its stored bearer token after a 401.
Token Lifecycle#
| Event | Effect on access_token |
|---|---|
Login (user_api.py) | Set to a fresh UUID via get_uuid() |
Logout / password change (user_api.py) | Set to "INVALID_<hex>" |
The INVALID_ prefix is checked in both _load_user_from_session() and UserService.query() to prevent revoked tokens from authenticating.
The JWT signing secret is a 64-char hex string lazily created and stored in Redis, shared across all server instances .
The APIToken model stores both plain API keys (token field) and beta tokens (beta field), scoped by tenant_id. The composite primary key is (tenant_id, token).
Error Handling#
Failed auth raises QuartAuthUnauthorized for most routes, returning HTTP 401 . The one exception: routes restricted to AUTH_BETA only return a JSON DATA_ERROR response instead of a 401 , matching the external API convention.
Error codes live in common/constants.py:
UNAUTHORIZED = 401AUTHENTICATION_ERROR = 109PERMISSION_ERROR = 108
Ownership-Based Authorization on Chat Endpoints#
Authentication (who you are) is separate from authorization (what you own). Chat and session endpoints enforce a second ownership check via _ensure_owned_chat(chat_id), which queries DialogService filtering by both tenant_id=current_user.id and id=chat_id. Any mutation route — PUT, PATCH, DELETE on /chats/<chat_id> and all /chats/<chat_id>/sessions/* routes — calls this guard before proceeding . A failed ownership check returns RetCode.AUTHENTICATION_ERROR (109), not 401.
The GET /chats/<chat_id> endpoint uses a broader check: it allows any tenant the user belongs to, not just their own — querying UserTenantService first .
Key Files#
| File | Role |
|---|---|
api/apps/__init__.py | _load_user(), login_required, login_user(), logout_user() |
api/apps/restful_apis/chat_api.py | Chat/session ownership enforcement, _ensure_owned_chat() |
api/apps/restful_apis/user_api.py | Login/logout routes, access_token lifecycle |
api/db/db_models.py | APIToken model |
common/settings.py | get_secret_key() — JWT signing secret |
common/constants.py | RetCode enum |