Building a Token Revocation Subsystem for Apache Airflow’s Security Layer
How an ten month old authentication gap in the world’s leading workflow orchestration platform was closed, and why it required building new…
Building a Token Revocation Subsystem for Apache Airflow’s Security Layer
How an ten month old authentication gap in the world’s leading workflow orchestration platform was closed, and why it required building new infrastructure from scratch
The Security Gap
If you’re running Apache Airflow in production, you probably don’t think much about what happens when someone clicks “Logout.” You’d assume the session is over. Done. No more access.
That assumption was wrong.
For context: Airflow is a Top-Level Project of the Apache Software Foundation, in the same governance tier as Apache Spark and Apache Kafka. It has **44,000+ GitHub stars, [16,500+ forks](https://github.com/apache/airflow/forks), nearly [16 million monthly downloads](https://pypistats.org/packages/apache-airflow) from PyPI, and is used by [21,000+ organizations (HG Insights)](https://discovery.hgdata.com/product/apache-airflow) **including Airbnb, Spotify, Google, Walmart, Adobe and many more. It’s not just popular. It’s infrastructure.
And what makes this security story especially scary is what Airflow has access to. Airflow doesn’t just run simple scripts. It acts like the central nervous system for a company’s data systems. Through its API, Airflow can start, stop, or change data pipelines that move huge amounts of data between systems. So if someone gets hold of a valid Airflow token, it’s not just a small login issue. It could give them access to a company’s entire data infrastructure.
When Airflow 3.x moved to JWT-based authentication under AIP-84 (the formal initiative to modernize Airflow’s auth system), the logout endpoint would delete the JWT cookie from the browser. Sounds right. But here’s the problem: the token itself was still cryptographically valid until it naturally expired. Anyone who had intercepted, cached, or exfiltrated that token could keep using it, accessing every secret, every pipeline, every admin function, for the entire duration of its TTL.
In this post, I will explain how I have solved the problem, the decisions I have made, and what the final solution looked like in PR #61339.
The fix was twisted, and here’s why.
If you are familiar with JWTs, you would know that JWTs are designed in a way that it removes all states. Every time I introduced a revocation, it added server-side state, which is eliminated by JWTs.
I went for several approaches and kept failing.
Short-lived tokens with refresh rotation. I tried using tokens that expire quickly and keep refreshing them. It was still adding risk, since Airflow is responsible for sensitive data for thousands of organizations.
In-memory blocklists (e.g., Redis). Fast, sure but was too heavy. Having memory in one server will not show up in another server automatically, so it would need a new dependency. Henceforth, we now will end up with more ways for things to fail.
Token versioning on the user record. Another idea was to check a user’s record again and again on every request to see if their token is still valid, which was clearly against JWTs. Also, it doesn’t handle the users that are logged in on multiple devices well.
Using standard JWT libraries (PyJWT, python-jose, authlib): Now this one could handle signing and checking but wouldn’t support revoking tokens because JWTs are meant to work without checking a database.
The final solution: I built a new system that stores revoked tokens in database. Since database is already a part of most operations in Airflow, it didn’t slow anything.
The Architecture

On the left before the fix:
If a user would log out, Airflow only deleted the browser cookie; however, the actual token was still valid. That means if someone had a copy of token, they could still use it to access the system.
On the right after the fix:
If a user logs out, Airflow saves the token’s unique ID in a special revocation table in database. Now, every time a request comes in, Airflow checks that table first. If the token’s ID is in the table then the access is denied but if it’s not in the table then the access is allowed.
The Database Structure
The most important part is this new table, which is called revoked token. This is the foundation of the whole system, yet kept very simple to make it fast, which enables checking and maintaining very easy.
class RevokedToken(Base):
__tablename__ = "revoked_token"
jti: Mapped[str] = mapped_column(String(32), primary_key=True)
exp: Mapped[datetime] = mapped_column(UtcDateTime, nullable=False, index=True)
JTI (JWT ID) as primary key: Each token has a unique ID called jti. By utilizing this ID as a primary key, the revocation check operates at O(1) time complexity. The lookup is extremely fast, even when there are many API requests happening at the same time.
Storing expiration: The table also stores the token’s expiration time enabling automatic cleanup. Once the token gets expired, there is no need to keep the revocation record as the token would not work anyway.
No foreign keys: It helps to keep the design simple preventing unnecessary complications in the database.
The Revocation Flow
# In the logout route
if token_str := request.cookies.get(COOKIE_NAME_JWT_TOKEN):
auth_manager.revoke_token(token_str)
When a user logs out, the system verifies the token’s signature and information before deleting the cookie. This is done deliberately because if the token is fake or broken, it is not stored.
def revoke_token(self, token: str) -> None:
try:
claims = self.validated_claims(token)
if (jti := claims.get("jti")) and (exp := claims.get("exp")):
RevokedToken.revoke(jti, datetime.fromtimestamp(exp, tz=timezone.utc))
except (jwt.InvalidTokenError, Exception):
log.warning("Failed to revoke token", exc_info=True)
If anything goes wrong in the revocation process, the system doesn’t stop the logout process. Instead, it records a warning in the logs and will continue to successfully log out.
The Enforcement Point
The system checks if a token has been revoked.
async def get_user_from_token(self, token: str) -> BaseUser:
payload = await signer.avalidated_claims(token)
if (jti := payload.get("jti")) and RevokedToken.is_revoked(jti):
raise InvalidTokenError("Token has been revoked")
return self.deserialize_user(payload)
Signature validation is checked first: If the token is broken or expired, it is rejected without having to touch the database. Then the Revocation check happens before loading the user’s information, so that the normal valid tokens will need one database check and invalid tokens wont ever use the database.
The self cleaning mechanism
During review, Jarek Potiuk, a member of Airflow’s Project Management Committee(PMC), pointed out a critical design requirement that the system shouldn’t depend on people only to manually cleanup the airflow database. His feedback:
“I think we should also add another thing — auto cleanup not only on clean_db but run (not always — just from time to time — we could store in memory last time when it was run and run it after an hour passes or so — when a token is checked). That will slightly slow down some login attempts — but it will also auto-clean the db when
airflow db cleanupis not run periodically. simply those expired tokens are not useful immediately after they expired."
Hence, I added a time based cleanup, so that the system removes the expired tokens itself without a separate manual intervention.
@classmethod
def _maybe_cleanup_expired(cls, session: Session) -> None:
now = time.monotonic()
cleanup_interval = conf.getint("api_auth", "jwt_expiration_time", fallback=3600) * 2
if now - cls._last_cleanup_time >= cleanup_interval:
cls._last_cleanup_time = now
try:
session.execute(delete(cls).where(cls.exp < datetime.now(tz=timezone.utc)))
except Exception:
log.exception("Failed to clean up expired revoked tokens")
Few important details about the cleanup:
The system uses **time.monotonic()** instead of the normal clock, to avoid the problems resulting from clock skew or adjustment.
Cleanup happens 2x the JWT expiration time just to make sure tokens are actually expired and to prevent the table from growing too big.
**_last_cleanup_time** is used to make sure cleanup only runs at most once per interval per API, even if many requests come in.
Failure tolerance: If the cleanup fails, it will just log the problem and will keep going.
The table is also integrated with Airflow’s database cleanup framework, so that the users who do run scheduled cleanup also gets benefitted.
Scope of Changes
The fix implementation touched many parts, such as **17 files across multiple layers of Airflow’s architecture, about [2,047 lines added and 1,565 lines modified or removed](https://github.com/apache/airflow/pull/61339)**.
New database migration (0102_3_2_0_add_revoked_token_table.py): A new migration was written in Alembic was slotted into Airflow’s versioned migration chain for 3.2.0
New ORM model (airflow/models/revoked_token.py): A new SQLAlchemy model was registered in airflow’s model registry along with the built in revocation and self cleaning methods.
Base auth manager (base_auth_manager.py): This particular change allows all kinds of authentication in Airflow, whether it is Simple, FAB or custom to use revocation without having to add any extra code.
JWT validator (tokens.py): was updated so that any token stored in the revoked table is now rejected during validation.
Logout API route (auth.py): was modified to call the revocation process before deleting any browser cookie.
DB cleanup config (db_cleanup.py): was updated so that expired tokens are automatically removed.
ERD documentation: was updated to include the new table.
Comprehensive test suite: was added such as, unit tests for the model, also the integration tests for the logout to enforcement flow and edge cases for invalid tokens, missing JTI claims and database errors.
Tradeoffs and Limitations
I want to be upfront about what this doesn’t solve:
One extra DB query per request. For most Airflow deployments this is negligible; the scheduler and API server are already database-heavy. But for extremely high-throughput API usage, it’s worth knowing about. A future optimization could add a short-lived in-process LRU cache in front of the DB check.
Only covers logout-initiated revocation. If an admin needs to revoke a specific user’s token (say, after a credential compromise), they’d need to wait for expiration or manually insert into the revoked_token table. A dedicated admin revocation endpoint is the natural follow-up, and the infrastructure now supports it.
Distributed clock assumptions. The cleanup logic assumes that datetime.now(tz=timezone.utc) , it is reasonably accurate across API server instances. In practice this is fine, but it's an assumption worth stating.
Technical Review done by the Core Maintainers
The implementation was carefully peer reviewed by the core maintainers, (PMC) members and the committers. These are the experts that act as the technical governing body for Airflow. The design gets approved after multiple rounds of feedback and improvements. The security issue was open for ten months and the fix had to connect several complex parts of Airflow, such as the authentication system, the database, and the API matching the high standards required by the project’s governance process.
**Vincent Beck (vincbeck)**, a PMC member identified a key architectural issue. His feedback: This was pivotal because now all authentication systems will automatically get the token revocation and the code can now be tested separately.
**Jarek Potiuk (potiuk)**, another PMC member recommended on adding an automatic cleanup mechanism making sure the system works reliably.
**Bugra Ozturk (bugraoz93)**, a PMC member who approved the PR and recognized the significance to Airflow’s security and nominated it for Airflow’s Pull Request of the Month (PROTM), which is one of the most competitive award selected from among 700+ PRs per month. His comment:
“I would like to call this PR for the
protmif no one disagrees :) #protm, which is solving a good problem for the token lifecycle. Additionally, it opens the door for great security improvement(s), such as a token invalidation endpoint for administrators in case of a token leak."
**Vincent Beck**, who is a PMC member also supported the “Pull request of the Month” nomination.
**Pierre Jeambrun (pierrejeambrun)**, who also is a PMC member reviewed the final implementation.
**Zhe-You Liu (jason810496)**, who is an Airflow committer identified that the initial logic duplicated functionality and suggested to restructure it. He also recommended to replace a module level global variable. Additionally, he suggested making the clean up interval configurable. Finally, he asked for the integration tests and also seconded the “Pull Request Of The Month” nomination.
These thoughtful reviews and all the positive feedbacks were deeply encouraging.
The pr was merged on February 5, 2026 and now will be rolled out in Apache Airflow 3.2.0.
Impact on the Airflow Ecosystem
This change not only strengthens the security integrity of the authentication layer for Airflow but also for the **21,000+ organizations (HG Insights) globally that rely on Apache Airflow, which is downloaded nearly [16 million times per month](https://pypistats.org/packages/apache-airflow).**
For all the organizations that use Airflow in their industries, it is very important to manage user sessions securely. Server side revocation is a critical part of this security because it allows the system to block a user’s login immediately whenever necessary.
SOC 2 Type II (CC6.3): They expect organizations to remove the access after it is no longer needed. Our change now creates a clear auditable evidence that the access was removed.
HIPAA (§ 164.312(a)(2)(iii)): Their requirement of automatic log off is also supported by the termination of the session.
GDPR Article 5(1)(f): This change also supports the integrity and confidentiality principles outlined in Article 5(1)(f).
As Bugra Ozturk, who is a PMC member noted:
“It opens the door for great security improvement(s), such as a token invalidation endpoint for administrators in case of a token leak.”
The foundation is now in place and the next steps are about building on top of it.
Further Reading
- Pull Request #61339 (
github.com/apache/airflow/pull/61339). Full pull request with code review discussion - Issue #47952 (
github.com/apache/airflow/issues/47952). Original issue tracking the authentication gap - PROTM Nomination Comment (
github.com/apache/airflow/pull/61339#issuecomment-3849798186). Pull Request of the Month nomination and endorsements - Apache Airflow GitHub Repository (
github.com/apache/airflow). Source repository with 44,000+ stars - PyPI Download Statistics (
pypistats.org/packages/apache-airflow). Monthly download metrics - AIP-84: UI REST API (
cwiki.apache.org/confluence/display/AIRFLOW/AIP-84). The formally approved Airflow Improvement Proposal under which this work was done - Apache Airflow Graduates to Top-Level Project (
globenewswire.com). Apache Software Foundation press release - Airflow Project Documentation (
airflow.apache.org/docs/apache-airflow/stable/project.html). Official project page and contributor roster
I would like to express my sincere gratitude towards all the PMC members and the Committers for all the valuable feedbacks and the positive encouragements.
메타데이터
- post_id
- d52dfcebd920
- slug
- building-a-token-revocation-subsystem-for-apache-airflows-security-layer-d52dfcebd920
- url
- https://medium.com/apache-airflow/building-a-token-revocation-subsystem-for-apache-airflows-security-layer-d52dfcebd920
- canonical_url
- https://medium.com/apache-airflow/building-a-token-revocation-subsystem-for-apache-airflows-security-layer-d52dfcebd920
- author_url
- https://medium.com/@giri.girianish
- status
- ok
- fetched_at
- 2026-06-12 18:14:10