airflow-core/docs/security/jwt_token_authentication.rst
.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
This document describes how JWT (JSON Web Token) authentication works in Apache Airflow for both the public REST API (Core API) and the internal Execution API used by workers.
.. contents:: :local: :depth: 2
Airflow uses JWT tokens as the primary authentication mechanism for its APIs. There are two distinct JWT authentication flows:
Both flows share the same underlying JWT infrastructure (JWTGenerator and JWTValidator
classes in airflow.api_fastapi.auth.tokens) but differ in audience, token lifetime, subject
claims, and scope semantics.
.. mermaid::
flowchart LR
subgraph Clients
UI[UI / browser]
CLI[CLI]
EXT[External REST clients]
end
subgraph Internal["Internal Airflow components"]
WORKER[Worker / Task]
DFP[Dag File Processor]
TRG[Triggerer]
end
APISVR[API Server]
EXECAPI[Execution API]
UI -->|JWT cookie / Bearer| APISVR
CLI -->|Bearer| APISVR
EXT -->|Bearer| APISVR
WORKER -->|Bearer
workload → execution| EXECAPI DFP -. in-process JWT bypassed .-> EXECAPI TRG -. in-process JWT bypassed .-> EXECAPI
classDef internal fill:#bbdefb,stroke:#1565c0,stroke-width:2px,color:#000
class WORKER,DFP,TRG internal
Airflow supports two mutually exclusive signing modes:
Symmetric (shared secret)
Uses a pre-shared secret key ([api_auth] jwt_secret) with the HS512 algorithm.
All components that generate or validate tokens must share the same secret. If no secret
is configured, Airflow auto-generates a random 16-byte key at startup — but this key is
ephemeral and different across processes, which will cause authentication failures in
multi-component deployments. Deployment Managers must explicitly configure this value.
Asymmetric (public/private key pair)
Uses a PEM-encoded private key ([api_auth] jwt_private_key_path) for signing and
the corresponding public key for validation. Supported algorithms: RS256 (RSA) and
EdDSA (Ed25519). The algorithm is auto-detected from the key type when
[api_auth] jwt_algorithm is set to GUESS (the default).
Validation can use either:
[api_auth] trusted_jwks_url
(local file or remote HTTP/HTTPS URL, polled periodically for updates).trusted_jwks_url is not set)... mermaid::
flowchart TB
subgraph Sym["Symmetric (HS512)"]
direction LR
S1[Scheduler / API Server]
S2[Shared secret
jwt_secret] S3[Token validator] S1 -->|sign| S2 -->|same secret also validates| S3 end subgraph Asym["Asymmetric (RS256 / EdDSA)"] direction LR A1[Scheduler / API Server] A2[Private key jwt_private_key_path] A3[Public key / JWKS endpoint] A4[Token validator] A1 -->|sign| A2 A2 -. derives or publishes .-> A3 A3 -->|verify only| A4 end
classDef secret fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000
classDef pub fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000
class S2 secret
class A2 secret
class A3 pub
In asymmetric mode, validators (workers, downstream services) need only the public key — the
private signing key can be tightly scoped to the issuing components (API Server, Scheduler).
In symmetric mode, any component that can validate tokens can also forge them, because there
is only one key. See :ref:jwt-authentication-and-workload-isolation for the deployment
implications.
Token acquisition ^^^^^^^^^^^^^^^^^
POST request to /auth/token with credentials (e.g., username
and password in JSON body).JWTGenerator.generate().access_token.For UI-based authentication, the token is stored in a secure, HTTP-only cookie (_token)
with SameSite=Lax.
The CLI uses a separate endpoint (/auth/token/cli) with a different (shorter) expiration
time.
Token structure (REST API) ^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table:: :header-rows: 1 :widths: 15 85
jtiiss[api_auth] jwt_issuer).aud[api_auth] jwt_audience).subiatnbfiat).expiat + jwt_expiration_time).Token validation (REST API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^
On each API request, the token is extracted in this order of precedence:
Authorization: Bearer <token> header._token cookie.The JWTValidator verifies the signature, expiry (exp), not-before (nbf),
issued-at (iat), audience, and issuer claims. A configurable leeway
([api_auth] jwt_leeway, default 10 seconds) accounts for clock skew.
Token revocation (REST API only) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Token revocation applies only to REST API and UI tokens — it is not used for Execution API tokens issued to workers.
Revoked tokens are tracked in the revoked_token database table by their jti claim.
On logout or explicit revocation, the token's jti and exp are inserted into this
table. Expired entries are automatically cleaned up at a cadence of 2× jwt_expiration_time.
The /auth/logout endpoint always invokes auth_manager.revoke_token() before any
redirect or cookie deletion. This includes deployments where the configured auth manager
(for example FabAuthManager or KeycloakAuthManager) redirects the user to an external
logout URL — the JWT is invalidated in Airflow's revoked_token table regardless of what
the external Identity Provider does with its own session. The revoke_token call is
unconditional; auth managers that do not implement server-side revocation can keep the
default no-op implementation.
Token refresh (REST API) ^^^^^^^^^^^^^^^^^^^^^^^^
The JWTRefreshMiddleware runs on UI requests. When the middleware detects that the
current token's _token cookie is approaching expiry, it calls
auth_manager.refresh_user() to generate a new token and sets it as the updated cookie.
Default timings (REST API) ^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table:: :header-rows: 1 :widths: 50 50
[api_auth] jwt_expiration_time[api_auth] jwt_cli_expiration_time[api_auth] jwt_leewayThe Execution API is an API used for use by Airflow itself (not third party callers) to report and set task state transitions, send heartbeats, and to retrieve connections, variables, and XComs at task runtime, trigger execution and Dag parsing.
Token generation (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
jwt_generator property creates a JWTGenerator configured with the [execution_api] settings.sub (subject) claim is set to the task instance UUID.BaseWorkloadSchema.token field)
that is sent to the worker process.Token structure (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table:: :header-rows: 1 :widths: 15 85
jtiiss[api_auth] jwt_issuer).aud[execution_api] jwt_audience, default: urn:airflow.apache.org:task).subscope"execution" or "workload".iatnbfexpiat + [execution_api] jwt_expiration_time).Token scopes (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Execution API defines two token scopes with different lifetimes:
workload
A token embedded in the workload JSON payload when the Scheduler
dispatches a task. The longer lifetime
allows tasks to remain valid while waiting in executor queues before execution
begins. When a worker calls the /run endpoint with a workload token, the
server issues a fresh execution-scoped token in the Refreshed-API-Token
response header. Lifetime equals [scheduler] task_queued_timeout (default
600 seconds) — the same timeout the scheduler uses to reap queue-starved tasks —
so tuning task_queued_timeout also widens the window a task can wait in a
backed-up queue before its workload token expires.
execution
A short-lived token (default 10 minutes) accepted by all Execution API endpoints.
This is the standard scope for worker communication during task execution. Issued
by the server when the worker transitions to running via the /run endpoint.
The JWTReissueMiddleware refreshes execution tokens transparently,
so the worker maintains access for the duration of the task.
Tokens without a scope claim default to "execution" for backwards compatibility.
Token delivery to workers ^^^^^^^^^^^^^^^^^^^^^^^^^
The token flows through the execution stack as follows:
workload-scoped token (lifetime equals
[scheduler] task_queued_timeout, default 600 seconds) and embeds it in the workload
JSON payload that it passes to Executor.execute_workload() function reads the workload JSON and extracts the token.supervise_task() function receives the token and creates an httpx.Client instance
with BearerAuth(token) for all Execution API HTTP requests./run endpoint with the workload-scoped token to mark the task
as running. The server responds with a fresh execution-scoped token in the
Refreshed-API-Token header._update_auth() hook detects the header and transparently updates
the BearerAuth instance to use the new execution token for all subsequent requests... mermaid::
sequenceDiagram
autonumber
participant SCH as Scheduler
participant EXE as Executor
(Celery / K8s / Local) participant WRK as Worker participant API as Execution API
Note over SCH: Task ready to dispatch
SCH->>SCH: generate workload token
scope=workload exp = task_queued_timeout SCH->>EXE: workload JSON (includes token) Note over EXE: Task waits in queue (can be minutes) EXE->>WRK: dispatch (workload JSON) WRK->>API: PATCH /run Bearer: workload token Note over API: validates workload scope checks TI in QUEUED/RESTARTING 409 if not API-->>WRK: 200 OK Refreshed-API-Token: execution token (scope=execution, ~10 min) WRK->>WRK: BearerAuth swaps to execution token loop For all subsequent calls (heartbeats, XComs, ...) WRK->>API: Bearer: execution token alt token expiring (less than 20% left) API-->>WRK: 200 OK Refreshed-API-Token: new execution token WRK->>WRK: BearerAuth swaps again end end
Even if a workload token is intercepted in transit, it can only call /run. That endpoint
rejects re-runs (409 Conflict unless the task instance is in QUEUED or RESTARTING),
so the attack surface for the longer-lived token is bounded to "start a task that is already
queued". All other endpoints require scope=execution and reject workload tokens.
Token validation (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The JWTBearer security dependency validates the token once per request:
Authorization: Bearer header.JWTValidator.exp, iat, aud — nbf and iss if configured).scope claim to "execution" if absent.TIClaims
(in airflow.api_fastapi.execution_api.datamodels.token) enforces that scope is one
of the declared TokenScope literals ("execution" or "workload"); TIToken
then parses the sub claim through a UUID field, which rejects non-UUID values.
A token whose scope is unknown, or whose sub is not a valid UUID, is rejected with
403 Forbidden even when the cryptographic signature checks pass. TIClaims keeps
extra="allow" so auth managers can attach additional, deployment-specific claims
without modifying the core schema; only the security-critical fields are typed.TIToken object with the task instance ID and validated claims.Route-level enforcement is handled by require_auth:
scope against the route's allowed_token_types (precomputed
by ExecutionAPIRoute from token:* Security scopes at route registration time).ti:self scope — verifies that the token's sub claim matches the
{task_instance_id} path parameter, preventing a worker from accessing another task's
endpoints... mermaid::
flowchart TD
REQ([Incoming request
Authorization: Bearer ...]) REQ --> CACHE{Cached on request.scope?} CACHE -->|yes| RET([Return cached TIToken]) CACHE -->|no| SIG[JWTValidator: verify signature] SIG -->|fail| F1([403 Forbidden]) SIG -->|ok| STD[Verify exp / iat / nbf aud / iss] STD -->|fail| F1 STD -->|ok| SCOPE[Default scope to 'execution' if absent] SCOPE --> SCHEMA[TIClaims: typed Pydantic schema] SCHEMA -->|ValidationError| F1 SCHEMA -->|ok| TYP{require_auth: scope in route.allowed_token_types?} TYP -->|no| F1 TYP -->|yes| SELF{ti:self scope declared?} SELF -->|no| OK([Return TIToken]) SELF -->|yes| MATCH{token.sub == task_instance_id?} MATCH -->|no| F1 MATCH -->|yes| OK
classDef fail fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000
classDef pass fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000
class F1 fail
class OK,RET pass
Token refresh (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The JWTReissueMiddleware automatically refreshes valid tokens that are approaching
expiry. The token must be valid at the start of the request for refresh to occur:
scope and sub).Refreshed-API-Token response header._update_auth() hook detects this header and transparently updates
the BearerAuth instance for subsequent requests.The middleware only refreshes execution-scoped tokens. workload-scoped tokens are
sized to span the queued-timeout window and are explicitly skipped by the middleware —
they are designed to survive executor queue wait times without needing refresh. This
ensures long-running tasks do not lose API access without requiring the worker to
re-authenticate.
No token revocation (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Execution API tokens are not subject to revocation. execution-scoped tokens are short-lived
(default 10 minutes) and automatically refreshed by the JWTReissueMiddleware.
workload-scoped tokens (tracking [scheduler] task_queued_timeout) are not refreshed —
they expire naturally after their validity period. Revocation is not part of the Execution API
security model.
Default timings (Execution API) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table:: :header-rows: 1 :widths: 50 50
[execution_api] jwt_expiration_time[scheduler] task_queued_timeout (default 600 seconds)[execution_api] jwt_audienceurn:airflow.apache.org:taskThe Dag File Processor and Triggerer are internal Airflow components that also
interact with the Execution API, but they do so via an in-process transport
(InProcessExecutionAPI) rather than over the network. This in-process API:
TIToken with the "execution" scope, effectively bypassing
token validation.Airflow implements software guards that prevent accidental direct database access from Dag
author code in these components. However, because the child processes that parse Dag files and
execute trigger code run as the same Unix user as their parent processes, these guards do
not protect against intentional access. A deliberately malicious Dag author can potentially
retrieve the parent process's database credentials (via /proc/<PID>/environ, configuration
files, or secrets manager access) and gain full read/write access to the metadata database and
all Execution API operations — without needing a valid JWT token.
This is in contrast to workers/task execution, where the isolation is implemented ad deployment level - where sensitive configuration of database credentials is not available to Airflow processes because they are not set in their deployment configuration at all, and communicate exclusively through the Execution API.
In the default deployment, a single Dag File Processor instance parses Dag files for all teams and a single Triggerer instance handles all triggers across all teams. This means that Dag author code from different teams executes within the same process, with potentially shared access to the in-process Execution API and the metadata database.
For multi-team deployments that require isolation, Deployment Managers must run separate Dag File Processor and Triggerer instances per team as a deployment-level measure — Airflow does not provide built-in support for per-team DFP or Triggerer instances. Even with separate instances, each retains the same Unix user as the parent process. To prevent credential retrieval, Deployment Managers must implement Unix user-level isolation (running child processes as a different, low-privilege user) or network-level restrictions.
See :doc:/security/security_model for the full security implications, deployment hardening
guidance, and the planned strategic and tactical improvements.
For a detailed discussion of workload isolation protections, current limitations, and planned
improvements, see :ref:workload-isolation.
All JWT-related configuration parameters:
.. list-table:: :header-rows: 1 :widths: 40 15 45
[api_auth] jwt_secretjwt_private_key_path.[api_auth] jwt_private_key_pathRSA or Ed25519). Mutually exclusive with jwt_secret.[api_auth] jwt_algorithmGUESSHS512 for symmetric, RS256 for RSA, EdDSA for Ed25519.[api_auth] jwt_kidRFC 7638 thumbprint)[api_auth] jwt_issueriss). Recommended to be unique per deployment.[api_auth] jwt_audienceaud) for REST API tokens.[api_auth] jwt_expiration_time[api_auth] jwt_cli_expiration_time[api_auth] jwt_leeway[api_auth] trusted_jwks_urljwt_secret.[execution_api] jwt_expiration_timeexecution-scoped token lifetime in seconds.[scheduler] task_queued_timeoutworkload-scoped token lifetime to the same value.[execution_api] jwt_audienceurn:airflow.apache.org:task.. important::
Time synchronization across all Airflow components is critical. Use NTP (e.g., ntpd or
chrony) to keep clocks in sync. Clock skew beyond the configured jwt_leeway will cause
authentication failures.