mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Sign-out of an OIDC session redirects the browser to the provider's discovered end_session_endpoint (id_token_hint + absolute post_logout_redirect_uri) when PHOTOPRISM_OIDC_LOGOUT is enabled, so the provider SSO session ends and the next login re-prompts. Opt-in (default off); local/LDAP logout and providers without an end-session endpoint fall back to local logout. Adds the shared CE /api/v1/oauth/logout route and OAuthLogoutHandler hook plus the Portal OP end_session_endpoint advertised in the Portal discovery document, for Portal builds to serve via the override hook.
98 lines
3.6 KiB
Go
98 lines
3.6 KiB
Go
package api
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/oidc/v3/pkg/client/rp"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/photoprism/photoprism/internal/auth/oidc"
|
|
"github.com/photoprism/photoprism/internal/config"
|
|
"github.com/photoprism/photoprism/internal/entity"
|
|
"github.com/photoprism/photoprism/pkg/authn"
|
|
)
|
|
|
|
// fakeRelyingParty implements only the rp.RelyingParty methods EndSessionURL reads.
|
|
type fakeRelyingParty struct {
|
|
rp.RelyingParty
|
|
endSession string
|
|
clientID string
|
|
}
|
|
|
|
func (f fakeRelyingParty) GetEndSessionEndpoint() string { return f.endSession }
|
|
func (f fakeRelyingParty) OAuthConfig() *oauth2.Config { return &oauth2.Config{ClientID: f.clientID} }
|
|
|
|
func oidcProvider(endSession string) *oidc.Client {
|
|
return &oidc.Client{RelyingParty: fakeRelyingParty{endSession: endSession, clientID: "client-abc"}}
|
|
}
|
|
|
|
func oidcSession(provider authn.ProviderType, idToken string) *entity.Session {
|
|
s := &entity.Session{}
|
|
s.SetProvider(provider)
|
|
s.IdToken = idToken
|
|
return s
|
|
}
|
|
|
|
func TestOidcLogoutURL(t *testing.T) {
|
|
conf := config.NewConfig(config.CliTestContext())
|
|
provider := oidcProvider("https://provider.example/logout")
|
|
|
|
t.Run("Success", func(t *testing.T) {
|
|
conf.Options().OIDCLogout = true
|
|
defer func() { conf.Options().OIDCLogout = false }()
|
|
|
|
result := oidcLogoutURL(conf, provider, oidcSession(authn.ProviderOIDC, "id-token-123"))
|
|
require.NotEmpty(t, result)
|
|
|
|
u, err := url.Parse(result)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "id-token-123", u.Query().Get("id_token_hint"))
|
|
assert.Equal(t, "client-abc", u.Query().Get("client_id"))
|
|
// post_logout_redirect_uri must be absolute (providers reject relative paths).
|
|
plr := u.Query().Get("post_logout_redirect_uri")
|
|
assert.Equal(t, AbsoluteLoginURL(conf), plr)
|
|
assert.Contains(t, plr, "://")
|
|
})
|
|
t.Run("FlagDisabled", func(t *testing.T) {
|
|
conf.Options().OIDCLogout = false
|
|
assert.Equal(t, "", oidcLogoutURL(conf, provider, oidcSession(authn.ProviderOIDC, "id-token-123")))
|
|
})
|
|
t.Run("NotOIDCSession", func(t *testing.T) {
|
|
conf.Options().OIDCLogout = true
|
|
defer func() { conf.Options().OIDCLogout = false }()
|
|
assert.Equal(t, "", oidcLogoutURL(conf, provider, oidcSession(authn.ProviderLocal, "id-token-123")))
|
|
})
|
|
t.Run("NoIdToken", func(t *testing.T) {
|
|
conf.Options().OIDCLogout = true
|
|
defer func() { conf.Options().OIDCLogout = false }()
|
|
assert.Equal(t, "", oidcLogoutURL(conf, provider, oidcSession(authn.ProviderOIDC, "")))
|
|
})
|
|
t.Run("ProviderWithoutEndSession", func(t *testing.T) {
|
|
conf.Options().OIDCLogout = true
|
|
defer func() { conf.Options().OIDCLogout = false }()
|
|
assert.Equal(t, "", oidcLogoutURL(conf, oidcProvider(""), oidcSession(authn.ProviderOIDC, "id-token-123")))
|
|
})
|
|
t.Run("NilProvider", func(t *testing.T) {
|
|
conf.Options().OIDCLogout = true
|
|
defer func() { conf.Options().OIDCLogout = false }()
|
|
assert.Equal(t, "", oidcLogoutURL(conf, nil, oidcSession(authn.ProviderOIDC, "id-token-123")))
|
|
})
|
|
}
|
|
|
|
func TestAbsoluteLoginURL(t *testing.T) {
|
|
t.Run("RelativeLoginPathResolvedToOrigin", func(t *testing.T) {
|
|
conf := config.NewConfig(config.CliTestContext())
|
|
result := AbsoluteLoginURL(conf)
|
|
assert.Contains(t, result, "://", "must be absolute")
|
|
assert.True(t, strings.HasSuffix(result, conf.LoginUri()), "must end with the login path")
|
|
})
|
|
t.Run("AbsoluteLoginUriPassedThrough", func(t *testing.T) {
|
|
conf := config.NewConfig(config.CliTestContext())
|
|
conf.Options().LoginUri = "https://sso.example.com/login"
|
|
assert.Equal(t, "https://sso.example.com/login", AbsoluteLoginURL(conf))
|
|
})
|
|
}
|