diff --git a/pkg/mock/handler.go b/pkg/mock/handler.go index feb8a26..4d98570 100644 --- a/pkg/mock/handler.go +++ b/pkg/mock/handler.go @@ -188,7 +188,11 @@ func (ip *identityProviderHandler) Token(w http.ResponseWriter, r *http.Request) idToken.Set("acr", auth.AcrLevel) idToken.Set("iat", time.Now().Unix()) idToken.Set("exp", time.Now().Unix()+expires) - idToken.Set("sid", sid) + if !ip.Provider.OpenIDConfiguration.GetCheckSessionIframe() { + idToken.Set("sid", sid) + } else { + idToken.Set("session_state", sid) + } signedIdToken, err := ip.signToken(idToken) if err != nil { diff --git a/pkg/mock/openid.go b/pkg/mock/openid.go index c8e6409..cd1fa4c 100644 --- a/pkg/mock/openid.go +++ b/pkg/mock/openid.go @@ -6,8 +6,7 @@ import ( "github.com/go-chi/chi/v5" ) -func IdentityProviderServer() (*httptest.Server, TestProvider) { - provider := NewTestProvider() +func IdentityProviderServer(provider TestProvider) (*httptest.Server, TestProvider) { handler := newIdentityProviderHandler(provider) router := identityProviderRouter(handler) server := httptest.NewServer(router) diff --git a/pkg/mock/provider.go b/pkg/mock/provider.go index 0971a19..716817c 100644 --- a/pkg/mock/provider.go +++ b/pkg/mock/provider.go @@ -31,6 +31,11 @@ func (p TestProvider) PrivateJwkSet() *jwk.Set { return &p.JwksPair.Private } +func (p TestProvider) WithCheckSessionIframe() TestProvider { + p.OpenIDConfiguration.CheckSessionIframe = "https://some-url/checksession" + return p +} + func NewTestProvider() TestProvider { jwksPair, err := crypto.NewJwkSet() if err != nil { diff --git a/pkg/openid/configuration.go b/pkg/openid/configuration.go index a8e11cc..34bf7ff 100644 --- a/pkg/openid/configuration.go +++ b/pkg/openid/configuration.go @@ -73,12 +73,8 @@ func (c *Configuration) FetchJwkSet(ctx context.Context) (*jwk.Set, error) { return &jwkSet, nil } -func (c *Configuration) FetchCheckSessionIframe() bool { - if c.CheckSessionIframe == "" { - return false - } - - return true +func (c *Configuration) GetCheckSessionIframe() bool { + return c.CheckSessionIframe != "" } func (c *Configuration) SidClaimRequired() bool { diff --git a/pkg/router/handler_callback.go b/pkg/router/handler_callback.go index 35e2346..6eb43e2 100644 --- a/pkg/router/handler_callback.go +++ b/pkg/router/handler_callback.go @@ -123,7 +123,7 @@ func (h *Handler) ExternalSessionId(idToken *openid.IDToken) (string, error) { switch { case openIDconfig.SidClaimRequired(): externalSessionID, err = idToken.GetStringClaim("sid") - case openIDconfig.FetchCheckSessionIframe(): + case openIDconfig.GetCheckSessionIframe(): externalSessionID, err = idToken.GetStringClaim("session_state") default: externalSessionID = h.GenerateExternalSessionID() diff --git a/pkg/router/router_test.go b/pkg/router/router_test.go index e923d6a..24db283 100644 --- a/pkg/router/router_test.go +++ b/pkg/router/router_test.go @@ -44,7 +44,7 @@ func newHandler(provider openid.Provider) *router.Handler { } func TestHandler_Login(t *testing.T) { - idpserver, idp := mock.IdentityProviderServer() + idpserver, idp := mock.IdentityProviderServer(mock.NewTestProvider()) h := newHandler(idp) r := router.New(h) @@ -100,7 +100,7 @@ func TestHandler_Login(t *testing.T) { } func TestHandler_Callback_and_Logout(t *testing.T) { - idpserver, idp := mock.IdentityProviderServer() + idpserver, idp := mock.IdentityProviderServer(mock.NewTestProvider()) h := newHandler(idp) r := router.New(h) @@ -195,7 +195,80 @@ func TestHandler_Callback_and_Logout(t *testing.T) { } func TestHandler_FrontChannelLogout(t *testing.T) { - _, idp := mock.IdentityProviderServer() + _, idp := mock.IdentityProviderServer(mock.NewTestProvider()) + h := newHandler(idp) + r := router.New(h) + server := httptest.NewServer(r) + + idp.ClientConfiguration.RedirectURI = server.URL + "/oauth2/callback" + idp.ClientConfiguration.PostLogoutRedirectURI = server.URL + + jar, err := cookiejar.New(nil) + assert.NoError(t, err) + + client := server.Client() + client.Jar = jar + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // First, run /oauth2/login to set cookies + resp, err := client.Get(server.URL + "/oauth2/login") + assert.NoError(t, err) + assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + defer resp.Body.Close() + + // Get authorization URL + location := resp.Header.Get("location") + u, err := url.Parse(location) + assert.NoError(t, err) + + // Follow redirect to authorize with idporten + resp, err = client.Get(u.String()) + assert.NoError(t, err) + assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + defer resp.Body.Close() + + // Get callback URL after successful auth + location = resp.Header.Get("location") + callbackURL, err := url.Parse(location) + assert.NoError(t, err) + + // Follow redirect to callback + resp, err = client.Get(callbackURL.String()) + assert.NoError(t, err) + assert.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) + + cookies := client.Jar.Cookies(callbackURL) + sessionCookie := getCookieFromJar(router.SessionCookieName, cookies) + + assert.NotNil(t, sessionCookie) + + // Trigger front-channel logout + ciphertext, err := base64.StdEncoding.DecodeString(sessionCookie.Value) + assert.NoError(t, err) + + sid, err := h.Crypter.Decrypt(ciphertext) + assert.NoError(t, err) + + frontchannelLogoutURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + frontchannelLogoutURL.Path = "/oauth2/logout/frontchannel" + + values := url.Values{} + values.Add("sid", string(sid)) + values.Add("iss", idp.GetOpenIDConfiguration().Issuer) + frontchannelLogoutURL.RawQuery = values.Encode() + + resp, err = client.Get(frontchannelLogoutURL.String()) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + defer resp.Body.Close() +} + +func TestHandler_FrontChannelLogoutWithCheckSessionIframe(t *testing.T) { + _, idp := mock.IdentityProviderServer(mock.NewTestProvider().WithCheckSessionIframe()) h := newHandler(idp) r := router.New(h) server := httptest.NewServer(r)