Skip to content

Add Keycloak browser login and token refresh for CLI clients - #75913

Open
baptistegh wants to merge 4 commits into
apache:mainfrom
baptistegh:keycloak-cli-login
Open

baptistegh wants to merge 4 commits into
apache:mainfrom
baptistegh:keycloak-cli-login

Conversation

@baptistegh

Copy link
Copy Markdown
Contributor

Command-line clients can only log in to the Keycloak auth manager with a username and password, which doesn't work for users who sign in through SSO or with a second factor. This lets them log in through Keycloak in their browser, and refresh the CLI token's short-lived Keycloak tokens. Keycloak advertises both through the CLI login discovery route from #75672.

This is the provider side only. The airflowctl side (auth login --sso) follows in #75804, which also shows the whole flow end to end.

related: #75560

Changes

  • get_cli_login_methods() always advertises password and refresh_token. It adds authorization_code with S256 only when [keycloak_auth_manager] cli_authorization_code_enabled is on (default False).
  • /auth/token/cli gets its own body: password (the default, so existing clients are unaffected), refresh_token or authorization_code. client_credentials and jwt-bearer stay on /auth/token only.
  • GET /auth/login/cli requires a loopback redirect_uri (http://127.0.0.1 or [::1], any port, path /callback) and an S256 challenge, then forwards both to Keycloak. Airflow enforces PKCE here rather than on the Keycloak client, because the web login doesn't use it.
  • Refresh: the new token keeps the expiry of the old one, so a refresh never extends a CLI session. Expired and revoked tokens are refused through the auth manager's own get_user_from_token.
  • Keycloak clients with "Use Refresh Tokens" turned off are handled the same way as in Make refresh_token optional in Keycloak login and token services #75871.
  • Bumps python-keycloak>=6.0.0 for PKCE support.
  • On Airflow versions without CLI login discovery, the new hook is never called, so no version gate is needed.

Trying it without airflowctl

With cli_authorization_code_enabled = True and http://127.0.0.1/callback in the Keycloak client's valid redirect URIs:

# 1. Discover the login methods
curl "${AIRFLOW_URL}/api/v2/auth/cli/metadata"

# 2. Open in a browser. After login, Keycloak redirects to
#    http://127.0.0.1:8765/callback?code=...&state=xyz (nothing needs to listen; copy the code from the address bar)
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
CHALLENGE=$(printf '%s' "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=\n')
echo "${AIRFLOW_URL}/auth/login/cli?redirect_uri=http://127.0.0.1:8765/callback&state=xyz&code_challenge=${CHALLENGE}&code_challenge_method=S256"

# 3. Exchange the code for a CLI token
curl -X POST "${AIRFLOW_URL}/auth/token/cli" -H 'Content-Type: application/json' \
  -d "{\"grant_type\": \"authorization_code\", \"code\": \"<code>\", \"code_verifier\": \"${VERIFIER}\", \"redirect_uri\": \"http://127.0.0.1:8765/callback\"}"

# 4. Refresh it
curl -X POST "${AIRFLOW_URL}/auth/token/cli" -H 'Content-Type: application/json' \
  -d '{"grant_type": "refresh_token", "token": "<CLI token>"}'

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code (Opus 5.5) following the guidelines

@baptistegh

Copy link
Copy Markdown
Contributor Author

The CLI never talks to Keycloak's token endpoint and never gets Keycloak tokens. Keycloak sends the authorization code to the CLI's loopback callback. The CLI passes the code and its PKCE code_verifier to Airflow. Airflow then exchanges the code with Keycloak using the client secret, the same way the web login does, and returns an Airflow CLI token.

So the existing confidential client is enough. Airflow enforces PKCE (S256) and loopback-only redirect URIs itself. An intercepted code is useless without the client secret and the code_verifier.

A separate public client would need its own config option, an audience mapper so its tokens still pass authorization against the airflow client, and separate refresh handling. That's possible as a follow-up if there's a need, but it isn't required for this flow.

Comment thread providers/keycloak/pyproject.toml Outdated
"apache-airflow>=3.0.0",
"apache-airflow-providers-common-compat>=1.18.0",
"python-keycloak>=5.0.0",
"python-keycloak>=6.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that needed? It might break existing users no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, PKCE flow was added by marcospereirampj/python-keycloak#670 in v5.12.0

We can try with >=5.12.0 this would not be a major update.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do that, @stegololz played a bit with Keycloak 6 I think, so he might have some insights here?

Comment thread providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py Outdated
Comment thread providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py Outdated
Comment thread providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py Outdated
Comment thread providers/keycloak/src/airflow/providers/keycloak/auth_manager/services/token.py Outdated
@baptistegh
baptistegh requested a review from vincbeck October 29, 2026 21:48
baptistegh and others added 3 commits October 29, 2026 22:50
Command-line clients could only log in to the Keycloak auth manager with
a username and password, which does not work for users who sign in
through SSO or with a second factor. The CLI login discovery route lets
an auth manager advertise other login methods, and Keycloak is the first
to offer one.

The CLI token embeds a Keycloak access token that usually expires within
minutes, so a CLI session also needs a way to get fresh Keycloak tokens
without ever outliving the original CLI token.

The airflowctl side follows in a separate change.

Generated-by: Claude Code (Opus 5.5)
Signed-off-by: Baptiste Gouhoury <gouhoury.baptiste@gmail.com>
Refreshing a CLI token assumed Keycloak always returns a new refresh
token. Clients with "Use Refresh Tokens" turned off return none at all,
and RFC 6749 section 6 lets any refresh response omit one and keep the
previous token valid, so those deployments could not refresh. Where a
token genuinely cannot be refreshed, a dedicated response tells the user
to log in again instead of leaving a valid token looking invalid.

Token validation allows [api_auth] jwt_leeway, so a token that had just
expired was still accepted and refreshed into one with a lifetime in the
past.

A request body that is valid JSON but not an object reached a validator
that assumed a mapping, so clients got 500 where the request was simply
malformed. The same validator guards the pre-existing /auth/token route.

The floor on python-keycloak was raised further than the PKCE support
this feature needs, which landed in 5.12.0, and would have forced a
major upgrade on existing users for no gain.

Generated-by: Claude Code (Opus 5)
Signed-off-by: BAPTISTE GOUHOURY <baptiste-externe.gouhoury@edf.fr>
Logging out revokes only the token the CLI currently holds. Tokens it
had refreshed away from stayed valid and refreshable until they expired,
so a logged-out CLI session could be resumed with any earlier token.

Generated-by: Claude Code (Opus 5.5)
Signed-off-by: Baptiste Gouhoury <gouhoury.baptiste@gmail.com>
@baptistegh

baptistegh commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Hello @vincbeck, thanks for your review. I tried to fix as much as I can.

I still have 3 things that came up :

  • My refactor made all grants use preferred_username / clientId / "service-account". Before, the password grant required preferred_username. So a real user could now show up as service-account if the claim is missing. I'd keep the fallback only for client credentials and JWT-bearer. OK?

  • Any Airflow API token can be refreshed at /auth/token/cli, not only CLI tokens. The expiry is kept, so a session is never extended.

  • revoke_token only exists since Airflow 3.2.0, so refresh fails with a 500 on 3.0 and 3.1. Can I bump the provider to apache-airflow>=3.2.0?

The /login/cli docstring is published as the endpoint description in
the OpenAPI spec, so implementation details about PKCE did not belong
there. Several comments restated the code or explained a refactor that
is no longer visible, and the browser login flag was read in two places.

Generated-by: Claude Code (Opus 5.5)
Signed-off-by: Baptiste Gouhoury <gouhoury.baptiste@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants