Skip to content

Commit 078f8af

Browse files
Marek DanoMarek Dano
authored andcommitted
fix: add to handle htmx header, add comments and update docs
Signed-off-by: Marek Dano <Marek.Dano@ibm.com>
1 parent 9ba9362 commit 078f8af

File tree

3 files changed

+48
-10
lines changed

3 files changed

+48
-10
lines changed

docs/docs/manage/sso-microsoft-entra-id-tutorial.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,12 @@ Front-channel logout enables automatic session clearing when users log out from
178178
- Production: `https://gateway.yourcompany.com/admin/logout`
179179
- Development: `http://localhost:8000/admin/logout`
180180

181-
2. When users log out from Microsoft, Entra ID sends a GET request to this URL
182-
3. ContextForge clears the session cookie and returns HTTP 200
181+
2. **How it works**: The `/admin/logout` endpoint supports three scenarios:
182+
- **OIDC front-channel logout**: When users log out from Microsoft Entra ID, it sends a GET request without browser headers. ContextForge clears the session and returns HTTP 200 (per OpenID Connect Front-Channel Logout 1.0 spec).
183+
- **Browser navigation**: If a user navigates directly to `/admin/logout` in their browser (GET with `Accept: text/html` header), they are redirected to the login page.
184+
- **User-initiated logout**: POST requests from the Admin UI logout button redirect to the login page after clearing the session.
185+
186+
3. All three scenarios properly clear authentication cookies and SSO session state.
183187

184188
## Step 5: Configure ContextForge Environment
185189

mcpgateway/admin.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4114,13 +4114,15 @@ async def _admin_logout(request: Request) -> Response:
41144114
"""
41154115
Handle admin logout by clearing authentication cookies.
41164116

4117-
Supports both GET and POST methods:
4118-
- POST: User-initiated logout from the UI (redirects to login page)
4119-
- GET: OIDC front-channel logout from identity provider (returns 200 OK)
4117+
Supports three logout scenarios:
4118+
- POST: User-initiated logout from the UI (redirects to login page or Keycloak logout)
4119+
- GET with browser headers: Browser navigation to /admin/logout (redirects to login page)
4120+
- GET without browser headers: OIDC front-channel logout callback from IdP (returns 200 OK)
41204121

4121-
For OIDC front-channel logout, Microsoft Entra ID sends GET requests to notify
4122-
the application that the user has logged out from the IdP. The application
4123-
should clear the session and return HTTP 200.
4122+
For OIDC front-channel logout (per OpenID Connect Front-Channel Logout 1.0 spec),
4123+
identity providers like Microsoft Entra ID send GET requests to notify the application
4124+
that the user has logged out from the IdP. The application should clear the session
4125+
and return HTTP 200.
41244126

41254127
Args:
41264128
request (Request): FastAPI request object.
@@ -4264,16 +4266,22 @@ def _build_keycloak_logout_url(root_path: str) -> Optional[str]:
42644266

42654267
# For GET requests, distinguish between browser navigation and OIDC front-channel logout
42664268
if request.method == "GET":
4267-
# Check if request is from a browser (has Accept: text/html header)
4269+
# Check if request is from a browser (Accept: text/html, HX-Request header, or admin referer)
4270+
# Detection must match auth_middleware.py and rbac.py patterns to ensure consistent behavior
42684271
# Browser navigation should redirect to login, OIDC callbacks should return 200 OK
42694272
accept_header = request.headers.get("accept", "")
4270-
is_browser_request = "text/html" in accept_header
4273+
is_htmx = request.headers.get("hx-request") == "true"
4274+
referer = request.headers.get("referer", "")
4275+
is_browser_request = "text/html" in accept_header or is_htmx or "/admin" in referer
42714276

42724277
if is_browser_request:
42734278
# Browser navigation - redirect to login (cookies cleared below)
42744279
response = RedirectResponse(url=f"{root_path}/admin/login", status_code=303)
42754280
else:
42764281
# OIDC front-channel logout from IdP - return 200 OK per OIDC spec
4282+
# Reference: OpenID Connect Front-Channel Logout 1.0
4283+
# https://openid.net/specs/openid-connect-frontchannel-1_0.html
4284+
# The RP must clear the session and return HTTP 200 to acknowledge logout
42774285
response = Response(content="Logged out", status_code=200)
42784286
else:
42794287
# POST requests (user-initiated) - redirect to login (cookies cleared below)

tests/unit/mcpgateway/test_admin_module.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,32 @@ async def test_admin_logout_paths():
496496
assert response.status_code == 200
497497
assert response.body == b"Logged out"
498498

499+
# GET request with HX-Request header (HTMX) should redirect to login
500+
get_htmx_request = _make_request(root_path="/root")
501+
get_htmx_request.method = "GET"
502+
get_htmx_request.headers = {"accept": "application/json", "hx-request": "true"}
503+
response = await admin._admin_logout(get_htmx_request)
504+
assert isinstance(response, RedirectResponse)
505+
assert response.status_code == 303
506+
assert response.headers["location"] == "/root/admin/login"
507+
508+
# GET request with admin referer should redirect to login
509+
get_referer_request = _make_request(root_path="/root")
510+
get_referer_request.method = "GET"
511+
get_referer_request.headers = {"accept": "application/json", "referer": "http://localhost:4444/admin/users"}
512+
response = await admin._admin_logout(get_referer_request)
513+
assert isinstance(response, RedirectResponse)
514+
assert response.status_code == 303
515+
assert response.headers["location"] == "/root/admin/login"
516+
517+
# GET request with */* Accept header (no text/html) should return 200 OK (OIDC)
518+
get_wildcard_request = _make_request(root_path="/root")
519+
get_wildcard_request.method = "GET"
520+
get_wildcard_request.headers = {"accept": "*/*"}
521+
response = await admin._admin_logout(get_wildcard_request)
522+
assert response.status_code == 200
523+
assert response.body == b"Logged out"
524+
499525

500526
@pytest.mark.asyncio
501527
async def test_admin_logout_keycloak_redirect(monkeypatch):

0 commit comments

Comments
 (0)