Skip to content

Commit 7dca298

Browse files
authored
Adds a state param into keycloak login (#64114)
1 parent d45ea10 commit 7dca298

File tree

2 files changed

+19
-3
lines changed
  • providers/keycloak
    • src/airflow/providers/keycloak/auth_manager/routes
    • tests/unit/keycloak/auth_manager/routes

2 files changed

+19
-3
lines changed

providers/keycloak/src/airflow/providers/keycloak/auth_manager/routes/login.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import json
2121
import logging
22+
import secrets
2223
from typing import cast
2324
from urllib.parse import quote, urljoin
2425

@@ -53,15 +54,23 @@ class AuthManagerRefreshTokenExpiredException(Exception): # type: ignore[no-red
5354
login_router = AirflowRouter(tags=["KeycloakAuthManagerLogin"])
5455

5556
COOKIE_NAME_ID_TOKEN = "_id_token"
57+
COOKIE_NAME_OAUTH_STATE = "_oauth_state"
5658

5759

5860
@login_router.get("/login")
5961
def login(request: Request) -> RedirectResponse:
6062
"""Initiate the authentication."""
6163
client = KeycloakAuthManager.get_keycloak_client()
6264
redirect_uri = request.url_for("login_callback")
63-
auth_url = client.auth_url(redirect_uri=str(redirect_uri), scope="openid")
64-
return RedirectResponse(auth_url)
65+
state = secrets.token_urlsafe(32)
66+
auth_url = client.auth_url(redirect_uri=str(redirect_uri), scope="openid", state=state)
67+
response = RedirectResponse(auth_url)
68+
secure = bool(conf.get("api", "ssl_cert", fallback=""))
69+
cookie_path = get_cookie_path()
70+
response.set_cookie(
71+
COOKIE_NAME_OAUTH_STATE, state, max_age=300, path=cookie_path, httponly=True, secure=secure
72+
)
73+
return response
6574

6675

6776
@login_router.get("/login_callback")
@@ -70,6 +79,10 @@ def login_callback(request: Request):
7079
code = request.query_params.get("code")
7180
if not code:
7281
return HTMLResponse("Missing code", status_code=400)
82+
state_q = request.query_params.get("state", "")
83+
state_c = request.cookies.get(COOKIE_NAME_OAUTH_STATE, "")
84+
if not state_q or not state_c or not secrets.compare_digest(state_q, state_c):
85+
return HTMLResponse("Invalid OAuth state parameter", status_code=403)
7386

7487
client = KeycloakAuthManager.get_keycloak_client()
7588
redirect_uri = request.url_for("login_callback")

providers/keycloak/tests/unit/keycloak/auth_manager/routes/test_login.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_login(self, mock_get_keycloak_client, client):
4040
def test_login_callback(self, mock_get_keycloak_client, mock_get_auth_manager, client):
4141
code = "code"
4242
token = "token"
43+
state = "state"
4344
mock_keycloak_client = Mock()
4445
mock_keycloak_client.token.return_value = {
4546
"access_token": "access_token",
@@ -55,7 +56,9 @@ def test_login_callback(self, mock_get_keycloak_client, mock_get_auth_manager, c
5556
mock_get_auth_manager.return_value = mock_auth_manager
5657
mock_auth_manager.generate_jwt.return_value = token
5758
response = client.get(
58-
AUTH_MANAGER_FASTAPI_APP_PREFIX + f"/login_callback?code={code}", follow_redirects=False
59+
AUTH_MANAGER_FASTAPI_APP_PREFIX + f"/login_callback?code={code}&state={state}",
60+
follow_redirects=False,
61+
cookies={"_oauth_state": state},
5962
)
6063
mock_keycloak_client.token.assert_called_once_with(
6164
grant_type="authorization_code",

0 commit comments

Comments
 (0)