Skip to content

fix(sso): support IdP-initiated chiclet flow on /auth/sso/login#4679

Open
igor-raits wants to merge 1 commit intoIBM:mainfrom
igor-raits:fix/sso-idp-initiated-chiclet
Open

fix(sso): support IdP-initiated chiclet flow on /auth/sso/login#4679
igor-raits wants to merge 1 commit intoIBM:mainfrom
igor-raits:fix/sso-idp-initiated-chiclet

Conversation

@igor-raits
Copy link
Copy Markdown

@igor-raits igor-raits commented May 8, 2026

✨ Feature / Enhancement PR

🔗 Epic / Issue

Link to the epic or parent issue:
Closes #


🚀 Summary (1-2 sentences)

Make /auth/sso/login/{provider_id} usable as an IdP-initiated chiclet entry point: redirect_uri becomes optional (server-built default) and the response is content-negotiated so browsers get a 302 to the IdP while XHR clients keep getting JSON.


🧪 Checks

  • make lint passes (black, ruff clean on the touched file)
  • make test passes
  • CHANGELOG updated (if user-facing)

📓 Notes

Two changes to mcpgateway/routers/sso.py:

  1. redirect_uri is now optional. When omitted, it is built from request.url_for("handle_sso_callback", ...) so it resolves to this gateway's own /auth/sso/callback/{provider_id} on the request origin (honouring app_root_path / proxy headers). Skipping the user-input _validate_redirect_uri check is safe as long as the request host is itself trustworthy (proxy-normalized in production); that check exists to defend against open redirects on caller-supplied URIs, not server-built ones.

  2. Content-negotiated response. Browser navigation (Accept: text/html without application/json) gets a 302 straight to the IdP's authorization URL; XHR clients sending Accept: application/json keep getting the SSOLoginResponse JSON they parse today. The session-binding cookie is set on the RedirectResponse explicitly because returning a Response object bypasses the injected response parameter.

Combined effect: an Okta app can register

Login Flow = Redirect to app to initiate login (OIDC Compliant)
Login URI  = https://<host>/auth/sso/login/okta

and clicking the chiclet lands the user signed in at /admin in one hop. The existing /admin/login form path is byte-identical to before.

sequenceDiagram
    participant User
    participant Okta
    participant Gateway
    User->>Okta: clicks chiclet
    Okta->>User: 302 to /auth/sso/login/okta
    User->>Gateway: GET /auth/sso/login/okta (Accept: text/html)
    Gateway->>User: 302 to Okta /authorize (cookie set)
    User->>Okta: /authorize
    Okta->>User: 302 to /auth/sso/callback/okta?code=...
    User->>Gateway: GET /auth/sso/callback/okta
    Gateway->>User: signed in, 302 to /admin
Loading

Two changes to mcpgateway/routers/sso.py so an Okta (or any OIDC
provider) IdP-initiated login URL pointed at /auth/sso/login/{provider}
just works as a chiclet entry point, while the existing SP-initiated
form on the gateway's login page keeps behaving as before.

1. Make `redirect_uri` optional with a sensible default. When omitted,
   build it from `request.url_for("handle_sso_callback", ...)` so it
   resolves to this gateway's own /auth/sso/callback/{provider} on the
   request origin (honouring app_root_path / proxy headers). Skipping
   the user-input `_validate_redirect_uri` check is safe as long as the
   request host is itself trustworthy (proxy-normalized in production);
   that check exists to defend against open redirects on caller-supplied
   URIs, not server-built ones.

2. Content-negotiate the response. Browser navigation (Accept: text/html
   without application/json) gets a 302 straight to the IdP's
   authorization URL; XHR clients sending Accept: application/json keep
   getting the SSOLoginResponse JSON they parse today. The
   session-binding cookie is set on the RedirectResponse explicitly
   because returning a Response object bypasses the injected \`response\`
   parameter.

Combined effect: an Okta app can register

    Login Flow = Redirect to app to initiate login (OIDC Compliant)
    Login URI  = https://<host>/auth/sso/login/okta

and clicking the chiclet lands the user signed in at /admin in one hop.
The existing /admin/login form path is byte-identical to before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Igor Raits <igor.raits@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant