JWT Authentication in Dify#
Overview#
Dify uses JWT (HS256) for authenticating both console (admin/developer) and web app (end-user) API requests. All tokens are signed and verified by PassportService using dify_config.SECRET_KEY. The console flow additionally issues a refresh token (non-JWT, Redis-backed) and a CSRF token (JWT). Web app tokens (public-site passports) have no expiry by default.
Core Signing Primitive: PassportService#
api/libs/passport.py is the single signing/verification entry point:
issue(payload)β encodes a dict as a signed HS256 JWTverify(token)β decodes and validates; raisesUnauthorizedon expiry, bad signature, or decode errorsSECRET_KEYmust be identical across all API processes (workers, WebSocket servers, Kubernetes pods). A mismatch producesInvalidSignatureErrorand silently rejects all tokens.
Console Login Flow#
Entry point: POST /console/api/login in api/controllers/console/auth/login.py.
- Credentials are validated;
AccountService.login()is called. AccountService.get_account_jwt_token()builds the access token payload:{user_id, exp, iss: EDITION, sub: "Console API Passport"}. Expiry =ACCESS_TOKEN_EXPIRE_MINUTES(default 60 min) .- A 128-char random hex refresh token is stored in Redis with a TTL of
REFRESH_TOKEN_EXPIRE_DAYS(default 30 days) . - A CSRF token (short-lived JWT with
{exp, sub: user_id}) is also issued. - All three are returned as HTTP-only, SameSite=Lax cookies β not in the response body.
Token refresh: POST /console/api/refresh-token looks up the refresh token in Redis, returns a new access + refresh + CSRF pair, and invalidates the old refresh token.
Console Request Verification#
Flask-Login's load_user_from_request hook in api/extensions/ext_login.py intercepts every request to the console blueprint:
extract_access_token()checks theCOOKIE_NAME_ACCESS_TOKENcookie first, then theAuthorization: Bearer <token>header.PassportService().verify(token)is called;user_idis extracted from the payload.- Console tokens must not have a
token_sourcefield (that field is reserved for webapp login tokens and its presence causes a 401).
Web App (End-User) Passport Flow#
Entry point: GET /api/passport in api/controllers/web/passport.py.
Public apps (no enterprise auth)#
The endpoint creates or reuses an anonymous EndUser and issues a no-expiry JWT :
payload = {iss: app_id, sub: "Web API Passport", app_id, app_code, end_user_id}
No exp field β tokens are permanent session identifiers tied to the site.
Enterprise-authenticated apps (INTERNAL / EXTERNAL)#
- A
webapp_login_tokenis obtained viaWebAppAuthService.login()after email/password authentication. - This login token's payload includes
token_source: "webapp_login_token"and expiry =ACCESS_TOKEN_EXPIRE_MINUTES(default 60 min) . - The
/passportendpoint exchanges this login token for a scoped webapp access token , re-using the originalexpif present . - The issued webapp token carries:
{iss: site.id, sub: "Web API Passport", app_id, app_code, user_id, end_user_id, auth_type, token_source: "webapp", exp}.
decode_enterprise_webapp_user_id() at the /passport endpoint validates that the presented token has token_source == "webapp_login_token" before exchange .
Token Expiry Summary#
| Token | Default TTL | Configured by |
|---|---|---|
| Console access token | 60 min | ACCESS_TOKEN_EXPIRE_MINUTES |
| Console refresh token | 30 days | REFRESH_TOKEN_EXPIRE_DAYS |
| Webapp login token (enterprise) | ACCESS_TOKEN_EXPIRE_MINUTES = 60 min (default) | ACCESS_TOKEN_EXPIRE_MINUTES |
| Public webapp passport | No expiry | β |
Key Files#
| File | Role |
|---|---|
api/libs/passport.py | JWT issue & verify |
api/configs/feature/__init__.py | ACCESS_TOKEN_EXPIRE_MINUTES, REFRESH_TOKEN_EXPIRE_DAYS config |
api/controllers/web/passport.py | /api/passport endpoint (web app token exchange) |
api/services/webapp_auth_service.py | Web app auth (login, token generation) |
api/controllers/console/auth/login.py | Console /login and /refresh-token endpoints |
api/extensions/ext_login.py | Flask-Login request loader (console token verification) |
api/libs/token.py | extract_access_token(), refresh token Redis helpers |