From 30853098c71dd4088bff9eb4069e7c6e7cee9ef8 Mon Sep 17 00:00:00 2001 From: Alban Fonrouge Date: Wed, 18 Mar 2026 13:19:10 +0100 Subject: [PATCH] feat: possibility to inject id_token in redirect url during sign out (#3278) * feat: possibility to inject id_token in redirect url during sign out Signed-off-by: Alban Fonrouge * doc: changelog for #3278 Signed-off-by: Jan Larwig * test: fix assertion for TestIdTokenPlaceholderInSignOut Signed-off-by: Jan Larwig --------- Signed-off-by: Alban Fonrouge Signed-off-by: Jan Larwig Co-authored-by: Jan Larwig --- CHANGELOG.md | 1 + docs/docs/features/endpoints.md | 14 ++++++++++ oauthproxy.go | 14 +++++++++- oauthproxy_test.go | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0470479c..d3225c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - [#3352](https://github.com/oauth2-proxy/oauth2-proxy/pull/3352) fix: backend logout URL call on sign out (#3172)(@vsejpal) - [#3332](https://github.com/oauth2-proxy/oauth2-proxy/pull/3332) ci: distribute windows binary with .exe extension (@igitur) - [#2685](https://github.com/oauth2-proxy/oauth2-proxy/pull/2685) feat: allow arbitrary claims from the IDToken and IdentityProvider UserInfo endpoint to be added to the session state (@vegetablest) +- [#3278](https://github.com/oauth2-proxy/oauth2-proxy/pull/3278) feat: possibility to inject id_token in redirect url during sign out (@albanf) # V7.14.3 diff --git a/docs/docs/features/endpoints.md b/docs/docs/features/endpoints.md index 5befce18..f310e48a 100644 --- a/docs/docs/features/endpoints.md +++ b/docs/docs/features/endpoints.md @@ -38,6 +38,20 @@ X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored. Make sure to include the actual domain and port (if needed) and not the URL (e.g "localhost:8081" instead of "http://localhost:8081"). +ID Token can be injected in the redirect url by using `{id_token}` placeholder. For example to redirect to `https://my-oidc-provider.example.com/sign_out_page?id_token_hint={id_token}&post_logout_redirect_uri=https://my-app.example.com`; + +``` +/oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page%3Fid_token_hint%3D%7Bid_token%7D%26post_logout_redirect_uri%3Dhttps%3A%2F%2Fmy-app.example.com +``` + +or alternatively in the header: + +``` +GET /oauth2/sign_out HTTP/1.1 +X-Auth-Request-Redirect: https://my-oidc-provider.example.com/sign_out_page?id_token_hint={id_token}&post_logout_redirect_uri=https://my-app.example.com +... +``` + ### Auth This endpoint returns 202 Accepted response or a 401 Unauthorized response. diff --git a/oauthproxy.go b/oauthproxy.go index 895f61a2..1610507b 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -54,6 +54,8 @@ const ( authOnlyPath = "/auth" userInfoPath = "/userinfo" staticPathPrefix = "/static/" + + idTokenPlaceholder = "{id_token}" ) var ( @@ -748,6 +750,16 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) return } + + if strings.Contains(redirect, idTokenPlaceholder) { + session, err := p.getAuthenticatedSession(rw, req) + if err != nil { + logger.Errorf("error getting authenticated session during SignOut, won't replace id_token placeholder in redirect URL: %v", err) + } else { + redirect = strings.ReplaceAll(redirect, idTokenPlaceholder, session.IDToken) + } + } + // Call backend logout before clearing the session so we still have the session // (and id_token) available to invoke the provider's logout endpoint p.backendLogout(rw, req) @@ -778,7 +790,7 @@ func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) { return } - backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, "{id_token}", session.IDToken) + backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, idTokenPlaceholder, session.IDToken) // security exception because URL is dynamic ({id_token} replacement) but // base is not end-user provided but comes from configuration somewhat secure resp, err := http.Get(backendLogoutURL) // #nosec G107 diff --git a/oauthproxy_test.go b/oauthproxy_test.go index 69951375..38cdccab 100644 --- a/oauthproxy_test.go +++ b/oauthproxy_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" + middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/hmacauth" @@ -3583,3 +3584,49 @@ func TestGetOAuthRedirectURI(t *testing.T) { }) } } + +func TestIdTokenPlaceholderInSignOut(t *testing.T) { + opts := baseTestOptions() + opts.WhitelistDomains = []string{"my-oidc-provider.example.com"} + + err := validation.Validate(opts) + assert.NoError(t, err) + + const emailAddress = "john.doe@example.com" + const userName = "9fcab5c9b889a557" + created := time.Now() + + session := &sessions.SessionState{ + User: userName, + Groups: []string{"a", "b"}, + Email: emailAddress, + IDToken: "eYjjjjjj.vvvv.ddd", + AccessToken: "oauth_token", + CreatedAt: &created, + } + + proxy, err := NewOAuthProxy(opts, func(email string) bool { + return true + }) + assert.NoError(t, err) + + // Save the required session + rw := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/", nil) + err = proxy.sessionStore.Save(rw, req, session) + assert.NoError(t, err) + + rw = httptest.NewRecorder() + + rdUrl := url.QueryEscape("https://my-oidc-provider.example.com/sign_out_page?id_token_hint={id_token}&post_logout_redirect_uri=https://my-app.example.com/") + req, _ = http.NewRequest("GET", "/oauth2/sign_out?rd="+rdUrl, nil) + req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{ + RequestID: "11111111-2222-4333-8444-555555555555", + Session: session, + }) + + proxy.SignOut(rw, req) + newLocation := rw.Header().Values("Location")[0] + + assert.Equal(t, "https://my-oidc-provider.example.com/sign_out_page?id_token_hint=eYjjjjjj.vvvv.ddd&post_logout_redirect_uri=https://my-app.example.com/", newLocation) +}