greekera1000 opened a new pull request, #72508:
URL: https://github.com/apache/airflow/pull/72508

   Closes: #72351
   
   ## What
   
   On Airflow 3.3+ with `apache-airflow-providers-keycloak>=0.9.0`, a token 
obtained from `POST /auth/token` no longer authenticates when presented as 
`Authorization: Bearer <token>`. The request fails with HTTP 500:
   
   File ".../keycloak/auth_manager/keycloak_auth_manager.py", line 471, in 
_is_authorized
   headers=self._get_headers(user.access_token),
   ^^^^^^^^^^^^^^^^^
   AttributeError: 'NoneType' object has no attribute 'access_token'
   
   
   Downgrading only the provider to `0.8.2`, with the same core version and the 
same Keycloak client configuration, resolves it.
   
   ## Why it happens
   
   Two changes interact:
   
   1. `serialize_user()` omits the Keycloak tokens from the Airflow JWT claims 
on 3.3+, because they are stored in dedicated cookies. `deserialize_user()` 
correspondingly falls back to `access_token=token.get("access_token", "")`.
   2. `get_user_from_token()` returns `None` when it is called without an 
`access_token` argument, on the assumption that the caller is a browser session 
whose Keycloak cookies are missing.
   
   The cookie split is a browser constraint: cookies are capped at roughly 4 KB 
and Keycloak access tokens are large enough that a cookie carrying both can 
exceed it (#61771). But it was applied to token minting in general rather than 
to the cookie path specifically, and `POST /auth/token` also mints its JWT 
through `generate_jwt()` -> `serialize_user()`.
   
   The result is that a non-browser caller has nowhere to carry the Keycloak 
access token: not in the claims, because they are stripped, and not in cookies, 
because it has none. `get_user()` resolves the user and then discards it, 
`None` propagates into `_is_authorized()`, and the unconditional 
`user.access_token` raises.
   
   Note that simply returning the user instead of `None` is not sufficient — 
the user's `access_token` would be `""`, `_is_authorized()` would send 
`Authorization: Bearer ` to the UMA token endpoint, and Keycloak would answer 
401. The claims side has to be fixed too.
   
   ## What this changes
   
   - `serialize_user()` keeps the Keycloak tokens in the claims on all 
versions, as it did before 3.3. The claims are covered by the Airflow JWT 
signature, so this is the one place a non-browser caller can carry them safely.
   - `login_callback()` mints its JWT from a user without the Keycloak tokens 
on 3.3+, so the cookie stays small. The size limit is enforced where the 
cookies are actually set, rather than for every token the auth manager issues.
   - `get_user_from_token()` no longer discards a validly resolved user when no 
cookie tokens were supplied. When they are supplied, the existing 
subject-binding check and the assignment are unchanged.
   
   The `return None` branch was already unreachable from the browser path: 
`KeycloakJWTMiddleware._refresh_user()` returns early without a `_token` cookie 
and raises 401 without an `_access_token` cookie, so it never calls 
`get_user_from_token()` without an access token. The branch only ever fired for 
header-authenticated callers.
   
   `refresh_user()` already returns `None` when the user has no refresh token, 
so the client-credentials service-account path (RFC 6749 §4.4.3, no refresh 
token issued) is unaffected — it is still not refreshable, it is just no longer 
unauthenticated.
   
   ## Tests
   
   - `test_get_user_from_token_round_trip_without_cookies` — round-trips a user 
through the auth manager's own serializer, the way `generate_jwt()` does when 
`/auth/token` mints a token, then resolves it the way the bearer path does, 
with no cookies. This is the regression test for the issue.
   - `test_get_user_from_token_keeps_user_when_cookies_missing` — replaces the 
previous test asserting `None`.
   - `test_serialize_user` and `test_login_callback` updated for the moved 
responsibility.
   
   I wasn't able to run the suite locally (Windows — the `_shared` symlinks 
don't survive checkout), so I'm relying on CI for verification.
   
   ## Backwards compatibility
   
   Browser sessions are unchanged: the JWT cookie stays token-free and the 
middleware continues to attach the tokens from the `_access_token` / 
`_refresh_token` cookies, including the subject-binding check. Airflow < 3.3 
behaviour is unchanged. Tokens already issued to bearer clients before this 
change do not gain claims retroactively and must be re-minted, which the 
existing expiry handling covers.
   
   ##### Was generative AI tooling used to co-author this PR?
   
   Yes. I used Claude to help investigate the root cause and write this 
description. I reviewed the change and am responsible for its correctness.
   


-- 
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