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]