potiuk opened a new pull request, #72207:
URL: https://github.com/apache/airflow/pull/72207
For Airflow 3.3+ the Keycloak access and refresh tokens are no longer
carried in the signed Airflow JWT — `serialize_user` deliberately omits them,
because Keycloak tokens can exceed the 4k cookie limit — and they travel in
separate `_access_token` / `_refresh_token` cookies instead.
`get_user_from_token` validated the Airflow JWT and then attached whatever
those cookies contained:
```python
user = cast("KeycloakAuthManagerUser", await
super().get_user_from_token(token))
...
if access_token:
user.access_token = access_token
user.refresh_token = refresh_token
return user
```
Nothing checked that the cookies described the same subject as the JWT. A
caller could pair their own Airflow session with another subject's Keycloak
token: every authorization decision is sent to Keycloak carrying
`user.access_token`, so the effective privileges are that token's, while
`get_id()` and `get_name()` — used for the session identity, audit records and
logging — remain those of the Airflow JWT.
## The fix
The access token's `sub` is compared against the user id the signed JWT
established, before the token is attached.
Both are the Keycloak subject. Every place a `KeycloakAuthManagerUser` is
constructed sets `user_id` from `userinfo["sub"]` — the interactive login
(`routes/login.py`), the password grant and the client-credentials grant
(`services/token.py`) — so the comparison is direct rather than a mapping.
A token whose payload cannot be read yields no subject and therefore matches
nothing.
## On reading the subject unverified
`_token_subject` decodes the payload without verifying the signature,
mirroring the existing `_token_expired` helper alongside it. That is sufficient
for this purpose, and deliberately so:
- the value is only ever compared against an identity the **signed** Airflow
JWT has already established, so it is never trusted on its own;
- a forged token is rejected by Keycloak when it is presented for an
authorization decision;
- a *genuine* token belonging to somebody else is precisely what this
comparison exists to catch, and its `sub` is authentic.
Verifying the Keycloak signature here would add a round trip or key handling
on every request without changing which tokens are accepted.
## Tests
The two existing tests passed the literal string `"access_token"` as a
cookie value, which carries no subject at all; they now build a JWT-shaped
token naming the same subject as the session. Added coverage for a token naming
a different subject and for one that cannot be parsed. Reverting the source
change fails both new tests.
Local: 290 passed, 1 skipped across the Keycloak provider; ruff and mypy
clean.
The provider changelog gains a note at the top, since a session whose
cookies disagree will now be refused rather than served.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]