API Authorization#
RAGFlow enforces a two-layer authorization model:
- Authentication — every protected route verifies the caller's identity via the
@login_requireddecorator. - Tenant-scoped resource access — once authenticated, resource ownership is always checked against the caller's
tenant_id, never a user-supplied value.
Both layers are implemented in api/apps/__init__.py.
Layer 1: Authentication (@login_required)#
The @login_required decorator accepts an optional auth_types parameter that controls which credential formats are accepted :
| Constant | Value | Credential source |
|---|---|---|
AUTH_JWT | "JWT" | Authorization: Bearer <signed-token> header or session cookie |
AUTH_API | "API" | APIToken.token looked up in the api_token table |
AUTH_BETA | "BETA" | APIToken.beta field — used by chatbot / bot-facing endpoints |
The default is (AUTH_JWT, AUTH_API) . Endpoint-specific requirements are declared inline, e.g. @login_required(auth_types=AUTH_BETA) for the chatbot completions route .
Resolution order inside _load_user():
g.usercache (already resolved this request).- Session cookie (
_user_id) — OAuth/OIDC fallback when theAuthorizationheader is absent . Authorizationheader — tried as BETA → JWT → API in that order.
For JWT, the token is decoded with itsdangerous.URLSafeTimedSerializer and then matched against User.access_token; tokens prefixed INVALID_ (set on logout) are always rejected . For API tokens, the APIToken row maps token → tenant_id, which resolves the user .
If no valid credential is found, the decorator raises QuartAuthUnauthorized, which is caught by the app-level 401 error handler and returned as a JSON RetCode.UNAUTHORIZED response.
The resolved user is exposed through current_user, a werkzeug.local.LocalProxy that calls _load_user() on each access.
Layer 2: Tenant-Scoped Resource Access#
Identity ≠ ownership. A successful login proves who you are, but every resource operation independently verifies what you own. All resources carry an indexed tenant_id column — dialogs, knowledge bases, API tokens, and conversations.
Dialog model#
The Dialog table has tenant_id as an indexed foreign key to User.id. When a dialog is created, the server always sets req["tenant_id"] = current_user.id — request payloads that include tenant_id are explicitly rejected with an error .
_ensure_owned_chat()#
Before any mutating operation on a dialog (update, delete, session management), _ensure_owned_chat(chat_id) queries:
DialogService.query(tenant_id=current_user.id, id=chat_id, status=StatusEnum.VALID.value)
An empty result returns RetCode.AUTHENTICATION_ERROR . This pattern is applied to all write and session endpoints.
Multi-tenant shared access#
DialogService.get_by_tenant_ids() broadens the ownership check for teams. The WHERE clause is:
(tenant_id IN joined_tenant_ids OR tenant_id = user_id)
AND status = 'valid'
joined_tenant_ids comes from the UserTenant relationship table, allowing dialogs to be visible across tenant groups without bypassing the ownership model .
APIToken model#
The APIToken table uses a composite primary key (tenant_id, token) and optionally stores a dialog_id to bind a token to a specific dialog. The source field (none|agent|dialog) records the token's intended scope.
Error Responses#
| Scenario | HTTP status | Response code |
|---|---|---|
| No valid credential | 401 | RetCode.UNAUTHORIZED |
| Valid credential but wrong tenant | 200 | RetCode.AUTHENTICATION_ERROR |
tenant_id provided in request body | 200 | RetCode.DATA_ERROR |