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]

Reply via email to