Skip to content

feat(oidc): outbound PKCE + configurable prompt parameter#166

Open
rtjdamen wants to merge 2 commits into
sigbit:mainfrom
Virtual-Computing-bv:feat/outbound-pkce
Open

feat(oidc): outbound PKCE + configurable prompt parameter#166
rtjdamen wants to merge 2 commits into
sigbit:mainfrom
Virtual-Computing-bv:feat/outbound-pkce

Conversation

@rtjdamen

Copy link
Copy Markdown

Summary

Two related additions to the OIDC client side of the proxy, both surfaced
by trying to use the proxy against IdPs that follow OAuth 2.1 strictly
(in our case Duo SSO and Microsoft Entra ID).

1. Outbound PKCE

OAuth 2.1-compliant IdPs reject /authorize requests that omit
code_challenge + code_challenge_method, even from confidential
clients with a client_secret. Without this patch, configuring such an
IdP fails immediately with errors like:

Must specify both 'code_challenge' and 'code_challenge_method'.

AuthCodeURL now generates a verifier, stores it keyed by state,
and adds the S256 challenge to the redirect. Exchange reads the
verifier back via the same state key and includes it on the token
exchange. Storage is an in-process sync.Map — acceptable because the
proxy already requires sticky sessions for inbound OAuth state. If
running multi-instance behind a load balancer is on the roadmap, the
verifiers could move into the repository abstraction.

2. --oidc-prompt / OIDC_PROMPT

Sends the value as the OIDC prompt query parameter on the upstream
/authorize call. Empty (default) preserves current behavior.

Concrete reason this is useful: with Microsoft Entra ID, an existing
browser session silently SSOs through the redirect without ever showing
the IdP's login screen — making it hard to verify operationally whether
MFA actually fires (it did, on the original session, but it's invisible).
OIDC_PROMPT=login forces Entra to render the login page on every
pairing; consent / select_account / none are also accepted
per OIDC Core 1.0 § 3.1.2.1.

Test plan

  • Confirmed against Duo SSO MCP OAuth Server: without the PKCE
    patch, /authorize returns the "Must specify code_challenge"
    error; with the patch, the flow completes.
  • Confirmed against Microsoft Entra ID: without OIDC_PROMPT,
    silent SSO bypasses the login screen; with OIDC_PROMPT=login,
    Entra renders the login page on every pairing.
  • No regression on inbound (claude.ai → proxy) flow.
  • No tests touched — let me know if you'd like coverage extending
    oidc_test.go for the new fields and I'll happily add it.

Compatibility

  • Existing deployments with no OIDC_PROMPT set: no behavior change.
  • Existing deployments against IdPs that don't require PKCE (e.g. Okta,
    Auth0 with default config): no behavior change beyond a (harmless)
    extra code_challenge on the wire, which they'll accept.

Happy to split this into two PRs if that's preferable — they're
functionally independent. Kept together because they're both very small
and were debugged in the same session.

rtjdamen added 2 commits May 15, 2026 08:15
OAuth 2.1-compliant IdPs (e.g. Duo SSO) reject /authorize requests that
omit code_challenge + code_challenge_method, even from confidential
clients. Without this, configuring such an IdP results in:

  "Must specify both 'code_challenge' and 'code_challenge_method'."

Generate a verifier in AuthCodeURL, store it keyed by state, and send
the matching code_verifier on Exchange.

Verifiers live in an in-process sync.Map; this is acceptable because
the proxy already requires sticky sessions for inbound OAuth state. A
later change could persist verifiers via the repository abstraction
if the proxy is run multi-instance behind a load balancer.
Add --oidc-prompt / OIDC_PROMPT to control the value sent as the OIDC
`prompt` query parameter on the upstream /authorize call.

Useful for IdPs (e.g. Microsoft Entra ID) where silent SSO would
otherwise hide whether MFA actually fired. Setting OIDC_PROMPT=login
forces the IdP to render its login screen on every authentication,
giving operators visible confirmation that the auth path is exercised.

Other accepted values per OIDC Core 1.0 section 3.1.2.1:
  - login          force re-authentication
  - consent        force user consent
  - select_account let user choose an account
  - none           never prompt (errors if not already authenticated)
@hrntknr

hrntknr commented May 27, 2026

Copy link
Copy Markdown
Member

doc-check has already been removed, so the failure can be ignored.

@hrntknr hrntknr self-requested a review May 27, 2026 13:08

@hrntknr hrntknr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The oidcPrompt approach seems promising. If you specifically want to incorporate oidcPrompt first, we can handle it in a separate PR - we should be able to integrate it quickly. (If you're not in a rush, the current approach is fine as well.)

Comment thread pkg/auth/oidc.go
// upstream IdP, and consumed in Exchange to fulfill the PKCE flow.
// Required for OAuth 2.1-compliant IdPs (e.g. Duo SSO) that reject
// authorization requests without code_challenge.
pkceVerifiers sync.Map

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Information about users who exited the login flow remains stored in memory indefinitely. It should be managed similarly to OAuth state, using session storage or similar mechanisms.

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.

2 participants