Skip to content

Resolve the caller from the explicit credential, not the session cookie - #72225

Open
potiuk wants to merge 2 commits into
apache:mainfrom
potiuk:security/cookie-bearer-precedence
Open

Resolve the caller from the explicit credential, not the session cookie#72225
potiuk wants to merge 2 commits into
apache:mainfrom
potiuk:security/cookie-bearer-precedence

Conversation

@potiuk

@potiuk potiuk commented Aug 28, 2026

Copy link
Copy Markdown
Member

get_user() codes an explicit precedence — bearer, then OAuth2, then the
session cookie:

if bearer_credentials and bearer_credentials.scheme.lower() == "bearer":
    token_str = bearer_credentials.credentials
elif oauth_token:
    token_str = oauth_token
else:
    token_str = request.cookies.get(COOKIE_NAME_JWT_TOKEN)

That block was never reached when a session cookie was present. JWTRefreshMiddleware
runs first, resolves a user from the _token cookie alone, and stamps it on
request.state together with the trust sentinel; get_user() returned that cached
user up front, before looking at either explicit credential. The effective order on
every core-API route was therefore cookie over bearer — the inverse of what the
function reads as doing.

So a request carrying both a cookie and an explicit Authorization: Bearer token ran
as the cookie's principal. The token the client deliberately presented was ignored,
and the request was recorded in the audit log under the wrong identity.

The change

The cached user is honoured only when the request carries no explicit credential —
which is the case it exists for: a browser session whose token the middleware has just
refreshed. When a bearer or OAuth2 token is present, that token is resolved instead.

The trust-sentinel check is unchanged and still guards the cached-user path; it has
simply moved inside the no-explicit-credential branch.

Behaviour

Request carries Resolved as
cookie only cookie principal (refreshed by the middleware, as before)
bearer only bearer principal (as before)
OAuth2 only OAuth2 principal (as before)
cookie and bearer bearer principal (changed — was the cookie)
cookie and OAuth2 OAuth2 principal (changed — was the cookie)

Only the two mixed-credential rows change. A client that sends one credential is
unaffected, and cookie-only browser sessions keep the refresh behaviour intact.

Tests

test_get_user_explicit_credential_beats_cookie_user, parametrised over bearer and
OAuth2: a trusted cookie-derived user is stamped on request.state and an explicit
credential is supplied; the explicit one must win. Both fail if the source change is
reverted.

The existing test_get_user_with_trusted_request_state still passes unmodified — it
supplies no explicit credential, so it exercises the path that was deliberately kept.

140 passed across core_api/test_security.py and auth/middlewares/; ruff clean.

🤖 Generated with Claude Code

`JWTRefreshMiddleware` resolves a user from the `_token` cookie alone and
stamps it on `request.state` together with the trust sentinel. `get_user()`
returned that cached user before it looked at `bearer_credentials` or
`oauth_token`, so on every core-API route the effective precedence was
cookie over bearer -- the inverse of the order the function itself codes.

A request carrying both a session cookie and an explicit
`Authorization: Bearer` token therefore executed, and was audit-logged, as
the cookie's principal rather than the identity the client asked to act as.

The cached user is now honoured only when the request carries no explicit
credential, which is the case it exists for: a browser session whose token
the middleware has just refreshed. When a bearer or OAuth2 token is
present it is resolved instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:API Airflow's REST/HTTP API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant