feat: implement OIDC RP-Initiated logout (#4674)

Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
Signed-off-by: Maksim Nabokikh <max.nabokih@gmail.com>
This commit is contained in:
Maksim Nabokikh
2026-04-01 08:58:44 +02:00
committed by GitHub
parent 486320de07
commit 58f148dd28
25 changed files with 1231 additions and 96 deletions
+20
View File
@@ -116,3 +116,23 @@ type RefreshConnector interface {
type TokenIdentityConnector interface {
TokenIdentity(ctx context.Context, subjectTokenType, subjectToken string) (Identity, error)
}
// LogoutCallbackConnector is a connector that can initiate upstream logout and
// optionally validate the upstream provider's logout response.
// Connectors that implement this interface support RP-Initiated Logout by
// returning a URL that Dex should redirect the user to in order to terminate
// the upstream session.
type LogoutCallbackConnector interface {
// LogoutURL returns the upstream provider's logout URL.
// connectorData is the data stored during the user's authentication session.
// postLogoutRedirectURI is the URL the upstream provider should redirect back to after logout.
// Returns the upstream logout URL or empty string if upstream logout is not available.
LogoutURL(ctx context.Context, connectorData []byte, postLogoutRedirectURI string) (string, error)
// HandleLogoutCallback validates the upstream provider's logout response
// received in the callback request. For example, SAML connectors should
// verify the LogoutResponse signature and status code here.
// Connectors that don't receive a structured response (e.g. OIDC) should
// return nil.
HandleLogoutCallback(ctx context.Context, r *http.Request) error
}
+57 -5
View File
@@ -136,10 +136,13 @@ type ProviderDiscoveryOverrides struct {
// JWKSURL provides a way to user overwrite the JWKS URL
// from the .well-known/openid-configuration jwks_uri
JWKSURL string `json:"jwksURL"`
// EndSessionURL provides a way to override the end_session_endpoint
// from the .well-known/openid-configuration
EndSessionURL string `json:"endSessionURL"`
}
func (o *ProviderDiscoveryOverrides) Empty() bool {
return o.TokenURL == "" && o.AuthURL == "" && o.JWKSURL == ""
return o.TokenURL == "" && o.AuthURL == "" && o.JWKSURL == "" && o.EndSessionURL == ""
}
func getProvider(ctx context.Context, issuer string, overrides ProviderDiscoveryOverrides) (*oidc.Provider, error) {
@@ -319,9 +322,10 @@ func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector,
}
}
// Obtain CodeChallengeMethodsSupported from the provider
// Obtain metadata from the provider
var metadata struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
EndSessionEndpoint string `json:"end_session_endpoint"`
}
if err := provider.Claims(&metadata); err != nil {
logger.Warn("failed to parse provider metadata")
@@ -340,6 +344,22 @@ func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector,
}
}
endSessionURL := metadata.EndSessionEndpoint
if c.ProviderDiscoveryOverrides.EndSessionURL != "" {
endSessionURL = c.ProviderDiscoveryOverrides.EndSessionURL
}
if endSessionURL != "" {
endSessionParsed, err := url.Parse(endSessionURL)
if err != nil {
cancel()
return nil, fmt.Errorf("oidc: invalid end_session_endpoint: %v", err)
}
if endSessionParsed.Scheme != "https" && endSessionParsed.Scheme != "http" {
cancel()
return nil, fmt.Errorf("oidc: end_session_endpoint must use http or https scheme, got %q", endSessionParsed.Scheme)
}
}
clientID := c.ClientID
return &oidcConnector{
provider: provider,
@@ -375,13 +395,15 @@ func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector,
groupsPrefix: c.ClaimMutations.ModifyGroupNames.Prefix,
groupsSuffix: c.ClaimMutations.ModifyGroupNames.Suffix,
pkceChallenge: c.PKCEChallenge,
endSessionURL: endSessionURL,
}, nil
}
var (
_ connector.CallbackConnector = (*oidcConnector)(nil)
_ connector.RefreshConnector = (*oidcConnector)(nil)
_ connector.TokenIdentityConnector = (*oidcConnector)(nil)
_ connector.CallbackConnector = (*oidcConnector)(nil)
_ connector.RefreshConnector = (*oidcConnector)(nil)
_ connector.TokenIdentityConnector = (*oidcConnector)(nil)
_ connector.LogoutCallbackConnector = (*oidcConnector)(nil)
)
type oidcConnector struct {
@@ -409,6 +431,7 @@ type oidcConnector struct {
groupsPrefix string
groupsSuffix string
pkceChallenge string
endSessionURL string
}
func (c *oidcConnector) Close() error {
@@ -739,3 +762,32 @@ func (c *oidcConnector) createIdentity(ctx context.Context, identity connector.I
return identity, nil
}
// LogoutURL returns the upstream OIDC provider's end_session_endpoint URL.
// Per the OIDC RP-Initiated Logout spec, the post_logout_redirect_uri parameter
// tells the upstream where to redirect after logout.
func (c *oidcConnector) LogoutURL(_ context.Context, _ []byte, postLogoutRedirectURI string) (string, error) {
if c.endSessionURL == "" {
return "", nil
}
u, err := url.Parse(c.endSessionURL)
if err != nil {
return "", fmt.Errorf("oidc: failed to parse end_session_endpoint: %v", err)
}
q := u.Query()
if postLogoutRedirectURI != "" {
q.Set("post_logout_redirect_uri", postLogoutRedirectURI)
q.Set("client_id", c.oauth2Config.ClientID)
}
u.RawQuery = q.Encode()
return u.String(), nil
}
// HandleLogoutCallback is a no-op for OIDC. The end_session_endpoint simply
// redirects back without a structured response to validate.
func (c *oidcConnector) HandleLogoutCallback(_ context.Context, _ *http.Request) error {
return nil
}
+106
View File
@@ -20,6 +20,7 @@ import (
"github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/dexidp/dex/connector"
)
@@ -976,3 +977,108 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) {
t.Errorf("Expected %+v to equal %+v", a, b)
}
}
func TestLogoutURL(t *testing.T) {
tests := []struct {
name string
endSessionURL string
postLogoutRedirectURI string
wantURL string
wantEmpty bool
}{
{
name: "no end_session_endpoint",
endSessionURL: "",
wantEmpty: true,
},
{
name: "with end_session_endpoint, no redirect",
endSessionURL: "https://provider.example.com/logout",
wantURL: "https://provider.example.com/logout",
},
{
name: "with end_session_endpoint and redirect",
endSessionURL: "https://provider.example.com/logout",
postLogoutRedirectURI: "https://dex.example.com/logout/callback",
wantURL: "https://provider.example.com/logout?client_id=clientID&post_logout_redirect_uri=https%3A%2F%2Fdex.example.com%2Flogout%2Fcallback",
},
{
name: "with existing query params",
endSessionURL: "https://provider.example.com/logout?existing=param",
postLogoutRedirectURI: "https://dex.example.com/callback",
wantURL: "https://provider.example.com/logout?client_id=clientID&existing=param&post_logout_redirect_uri=https%3A%2F%2Fdex.example.com%2Fcallback",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
conn := &oidcConnector{
endSessionURL: tc.endSessionURL,
oauth2Config: &oauth2.Config{
ClientID: "clientID",
},
}
got, err := conn.LogoutURL(context.Background(), nil, tc.postLogoutRedirectURI)
require.NoError(t, err)
if tc.wantEmpty {
require.Empty(t, got)
return
}
require.Equal(t, tc.wantURL, got)
})
}
}
func TestEndSessionURLDiscovery(t *testing.T) {
// Setup a server that advertises end_session_endpoint in discovery.
key, err := rsa.GenerateKey(rand.Reader, 1024)
require.NoError(t, err)
mux := http.NewServeMux()
mux.HandleFunc("/keys", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(&map[string]interface{}{
"keys": []map[string]interface{}{},
})
})
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
url := fmt.Sprintf("http://%s", r.Host)
json.NewEncoder(w).Encode(&map[string]string{
"issuer": url,
"token_endpoint": fmt.Sprintf("%s/token", url),
"authorization_endpoint": fmt.Sprintf("%s/authorize", url),
"jwks_uri": fmt.Sprintf("%s/keys", url),
"end_session_endpoint": fmt.Sprintf("%s/logout", url),
})
})
ts := httptest.NewServer(mux)
defer ts.Close()
_ = key // We only need the server for discovery.
conn, err := newConnector(Config{
Issuer: ts.URL,
Scopes: []string{"openid"},
})
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("%s/logout", ts.URL), conn.endSessionURL)
}
func TestEndSessionURLOverride(t *testing.T) {
testServer, err := setupServer(nil, true)
require.NoError(t, err)
defer testServer.Close()
conn, err := newConnector(Config{
Issuer: testServer.URL,
Scopes: []string{"openid"},
ProviderDiscoveryOverrides: ProviderDiscoveryOverrides{
EndSessionURL: "https://custom.example.com/logout",
},
})
require.NoError(t, err)
require.Equal(t, "https://custom.example.com/logout", conn.endSessionURL)
}
+6
View File
@@ -82,6 +82,7 @@ type discovery struct {
UserInfo string `json:"userinfo_endpoint"`
DeviceEndpoint string `json:"device_authorization_endpoint"`
Introspect string `json:"introspection_endpoint"`
EndSession string `json:"end_session_endpoint,omitempty"`
GrantTypes []string `json:"grant_types_supported"`
ResponseTypes []string `json:"response_types_supported"`
Subjects []string `json:"subject_types_supported"`
@@ -141,6 +142,11 @@ func (s *Server) constructDiscovery(ctx context.Context) discovery {
sort.Strings(d.ResponseTypes)
d.GrantTypes = s.supportedGrantTypes
if s.sessionConfig != nil {
d.EndSession = s.absURL("/logout")
}
return d
}
+356
View File
@@ -0,0 +1,356 @@
package server
import (
"context"
"errors"
"net/http"
"net/url"
"slices"
"github.com/dexidp/dex/connector"
"github.com/dexidp/dex/server/internal"
"github.com/dexidp/dex/storage"
)
// handleLogout implements OIDC RP-Initiated Logout (https://openid.net/specs/openid-connect-rpinitiated-1_0.html).
//
// GET/POST /logout?id_token_hint=...&post_logout_redirect_uri=...&state=...
//
// Flow:
// 1. Validate id_token_hint (signature + issuer; expiry skipped per spec)
// 2. Extract user identity (subject) and client (audience/azp) from the token
// 3. Validate post_logout_redirect_uri against the client's registered URIs
// 4. Revoke refresh tokens for the user/connector pair
// 5. If the auth session exists and upstream connector implements LogoutCallbackConnector:
// a. Store LogoutState + HMAC key in the session (not deleted yet)
// b. Redirect to upstream logout with signed state
// c. On callback: verify HMAC, read LogoutState from session, delete session, render page
// 6. If no session or no upstream logout support: delete session, clear cookie, render page
//
// Upstream redirect requires a live AuthSession because the session stores the
// HMAC key and logout parameters. Without a session (e.g. already expired, or
// id_token_hint without a cookie) upstream logout is skipped — this is acceptable
// because RP-Initiated Logout treats upstream SLO as best-effort.
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if r.Method != http.MethodGet && r.Method != http.MethodPost {
s.renderError(r, w, http.StatusMethodNotAllowed, "Method not allowed.")
return
}
idTokenHint := r.FormValue("id_token_hint")
postLogoutRedirectURI := r.FormValue("post_logout_redirect_uri")
state := r.FormValue("state")
var userID, connectorID, clientID string
if idTokenHint != "" {
idToken, err := s.validateIDTokenHint(ctx, idTokenHint)
if err != nil {
s.logger.ErrorContext(ctx, "logout: invalid id_token_hint", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Invalid id_token_hint.")
return
}
sub := new(internal.IDTokenSubject)
if err := internal.Unmarshal(idToken.Subject, sub); err != nil {
s.logger.ErrorContext(ctx, "logout: failed to unmarshal subject", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Invalid id_token_hint subject.")
return
}
userID = sub.UserId
connectorID = sub.ConnId
s.logger.DebugContext(ctx, "logout: parsed id_token_hint",
"user_id", userID, "connector_id", connectorID)
// When cross-client (trusted peers) scopes are used, the token may have
// multiple audiences. In that case the requesting client is in the "azp"
// claim, not necessarily Audience[0]. Use the same logic as token introspection.
var claims struct {
AuthorizingParty string `json:"azp"`
}
if err := idToken.Claims(&claims); err != nil {
s.logger.ErrorContext(ctx, "logout: failed to decode id_token_hint claims", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Invalid id_token_hint.")
return
}
switch len(idToken.Audience) {
case 0:
// No audience — cannot determine client.
case 1:
clientID = idToken.Audience[0]
default:
clientID = claims.AuthorizingParty
}
}
// If no id_token_hint, try to identify the user from the session cookie.
// This allows logout without a hint when the user has an active session.
if userID == "" && connectorID == "" {
if cookie, err := r.Cookie(s.sessionConfig.CookieName); err == nil && cookie.Value != "" {
if uid, cid, nonce, err := parseSessionCookie(cookie.Value, s.sessionConfig.CookieEncryptionKey); err == nil {
// Verify the session exists and nonce matches before trusting the cookie.
if session, err := s.storage.GetAuthSession(ctx, uid, cid); err == nil && session.Nonce == nonce {
userID = uid
connectorID = cid
s.logger.DebugContext(ctx, "logout: identified user from session cookie",
"user_id", userID, "connector_id", connectorID)
}
}
}
}
// Validate post_logout_redirect_uri against registered client URIs.
if postLogoutRedirectURI != "" {
if clientID == "" {
s.renderError(r, w, http.StatusBadRequest, "post_logout_redirect_uri requires id_token_hint.")
return
}
client, err := s.storage.GetClient(ctx, clientID)
if err != nil {
s.logger.ErrorContext(ctx, "logout: failed to get client", "client_id", clientID, "err", err)
s.renderError(r, w, http.StatusBadRequest, "Invalid client.")
return
}
if !slices.Contains(client.PostLogoutRedirectURIs, postLogoutRedirectURI) {
s.logger.WarnContext(ctx, "logout: unregistered post_logout_redirect_uri",
"uri", postLogoutRedirectURI, "client_id", clientID)
s.renderError(r, w, http.StatusBadRequest, "Unregistered post_logout_redirect_uri.")
return
}
}
// Revoke refresh tokens (does not touch the auth session or user identity).
var connectorData []byte
if userID != "" && connectorID != "" {
connectorData = s.revokeRefreshTokens(ctx, userID, connectorID)
}
// Try upstream logout. This requires a live auth session to store the HMAC key
// and logout parameters. If the session doesn't exist (expired, no cookie, etc.)
// upstream logout is skipped — RP-Initiated Logout treats upstream SLO as best-effort.
if redirectURL, ok := s.tryUpstreamLogout(ctx, userID, connectorID, connectorData, postLogoutRedirectURI, state, clientID); ok {
http.Redirect(w, r, redirectURL, http.StatusSeeOther)
return
}
// No upstream logout — delete session now, clear cookie, show page.
s.logger.DebugContext(ctx, "logout: completing",
"user_id", userID, "connector_id", connectorID, "client_id", clientID)
loggedOut := s.deleteAuthSession(ctx, userID, connectorID)
s.clearSessionCookie(w)
s.finishLogout(w, r, postLogoutRedirectURI, state, loggedOut)
}
// handleLogoutCallback receives the redirect back from the upstream provider
// after it has completed its logout.
//
// Identity is resolved from the session cookie (HttpOnly, Secure, SameSite=Lax).
// The session must still exist in storage with a non-nil LogoutState (set before
// the upstream redirect). After validation, the session is deleted.
func (s *Server) handleLogoutCallback(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Resolve identity from the session cookie.
cookie, err := r.Cookie(s.sessionConfig.CookieName)
if err != nil || cookie.Value == "" {
s.renderError(r, w, http.StatusBadRequest, "Missing session cookie.")
return
}
userID, connectorID, nonce, err := parseSessionCookie(cookie.Value, s.sessionConfig.CookieEncryptionKey)
if err != nil {
s.renderError(r, w, http.StatusBadRequest, "Invalid session cookie.")
return
}
// Load the session and verify nonce.
session, err := s.storage.GetAuthSession(ctx, userID, connectorID)
if err != nil {
s.logger.ErrorContext(ctx, "logout callback: session not found", "err", err)
s.renderError(r, w, http.StatusBadRequest, "Session not found.")
return
}
if session.Nonce != nonce {
s.renderError(r, w, http.StatusBadRequest, "Invalid session.")
return
}
if session.LogoutState == nil {
s.renderError(r, w, http.StatusBadRequest, "No logout in progress.")
return
}
ls := session.LogoutState
// Let the connector validate the upstream logout response if it supports it.
if ls.ConnectorID != "" {
conn, err := s.getConnector(ctx, ls.ConnectorID)
if err == nil {
if logoutConn, ok := conn.Connector.(connector.LogoutCallbackConnector); ok {
if err := logoutConn.HandleLogoutCallback(ctx, r); err != nil {
s.logger.ErrorContext(ctx, "logout: upstream logout response validation failed",
"connector_id", ls.ConnectorID, "err", err)
}
}
}
}
// Session kept alive until now — delete it and clear the cookie.
s.deleteAuthSession(ctx, userID, connectorID)
s.clearSessionCookie(w)
s.finishLogout(w, r, ls.PostLogoutRedirectURI, ls.State, true)
}
// finishLogout renders the logout page with a "Back to Application" link.
// loggedOut indicates whether an active session was actually terminated.
func (s *Server) finishLogout(w http.ResponseWriter, r *http.Request, postLogoutRedirectURI, state string, loggedOut bool) {
var backURL string
if postLogoutRedirectURI != "" {
u, err := url.Parse(postLogoutRedirectURI)
if err == nil {
if state != "" {
q := u.Query()
q.Set("state", state)
u.RawQuery = q.Encode()
}
backURL = u.String()
}
}
if err := s.templates.logout(r, w, backURL, loggedOut); err != nil {
s.logger.ErrorContext(r.Context(), "server template error", "err", err)
}
}
// tryUpstreamLogout attempts to redirect to the upstream provider's logout endpoint.
// It stores LogoutState in the auth session before redirecting so the callback can
// read it back. Returns the redirect URL and true on success, or ("", false) if
// upstream logout is not possible (no session, connector doesn't support it, etc.).
func (s *Server) tryUpstreamLogout(ctx context.Context, userID, connectorID string, connectorData []byte, postLogoutRedirectURI, state, clientID string) (string, bool) {
if connectorID == "" {
return "", false
}
conn, err := s.getConnector(ctx, connectorID)
if err != nil {
return "", false
}
logoutConn, ok := conn.Connector.(connector.LogoutCallbackConnector)
if !ok {
return "", false
}
// Check that the session exists — we need it to store logout state.
_, err = s.storage.GetAuthSession(ctx, userID, connectorID)
if err != nil {
s.logger.DebugContext(ctx, "logout: no auth session for upstream logout, skipping",
"user_id", userID, "connector_id", connectorID)
return "", false
}
// Store logout parameters in the session.
if err := s.storage.UpdateAuthSession(ctx, userID, connectorID, func(old storage.AuthSession) (storage.AuthSession, error) {
old.LogoutState = &storage.LogoutState{
PostLogoutRedirectURI: postLogoutRedirectURI,
State: state,
ClientID: clientID,
ConnectorID: connectorID,
}
return old, nil
}); err != nil {
s.logger.ErrorContext(ctx, "logout: failed to save logout state", "err", err)
return "", false
}
callbackURI := s.absURL("/logout/callback")
upstreamURL, err := logoutConn.LogoutURL(ctx, connectorData, callbackURI)
if err != nil {
s.logger.ErrorContext(ctx, "logout: upstream connector error", "err", err)
return "", false
}
if upstreamURL == "" {
return "", false
}
u, err := url.Parse(upstreamURL)
if err != nil {
s.logger.ErrorContext(ctx, "logout: failed to parse upstream URL", "err", err)
return "", false
}
return u.String(), true
}
// deleteAuthSession deletes the session and returns true if it existed.
func (s *Server) deleteAuthSession(ctx context.Context, userID, connectorID string) bool {
if userID == "" || connectorID == "" {
return false
}
if err := s.storage.DeleteAuthSession(ctx, userID, connectorID); err != nil {
if !errors.Is(err, storage.ErrNotFound) {
s.logger.ErrorContext(ctx, "logout: failed to delete auth session", "err", err)
}
return false
}
s.logger.InfoContext(ctx, "logout successful", "user_id", userID, "connector_id", connectorID)
return true
}
// revokeRefreshTokens deletes all refresh tokens for the given user/connector
// and clears the references in the offline session (but keeps the session object).
// Returns the connector data from the offline session (for upstream logout).
//
// To avoid a race condition where a new token issued between deletion and the
// OfflineSessions update would have its reference wiped, we:
// 1. Snapshot the token IDs to revoke
// 2. Remove only those specific references from OfflineSessions (the updater
// sees the latest state, so concurrently added refs are preserved)
// 3. Delete the actual refresh tokens
func (s *Server) revokeRefreshTokens(ctx context.Context, userID, connectorID string) []byte {
offlineSessions, err := s.storage.GetOfflineSessions(ctx, userID, connectorID)
if err != nil {
if !errors.Is(err, storage.ErrNotFound) {
s.logger.ErrorContext(ctx, "logout: failed to get offline sessions", "err", err)
}
return nil
}
// Snapshot token IDs to revoke.
tokenIDs := make(map[string]struct{}, len(offlineSessions.Refresh))
for _, ref := range offlineSessions.Refresh {
tokenIDs[ref.ID] = struct{}{}
}
// Remove only the snapshotted references — any token added concurrently
// will not be in tokenIDs and will be left untouched.
if err := s.storage.UpdateOfflineSessions(ctx, userID, connectorID, func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
for clientID, ref := range old.Refresh {
if _, ok := tokenIDs[ref.ID]; ok {
delete(old.Refresh, clientID)
}
}
return old, nil
}); err != nil {
s.logger.ErrorContext(ctx, "logout: failed to update offline sessions", "err", err)
}
// Delete the actual refresh tokens.
for id := range tokenIDs {
if err := s.storage.DeleteRefresh(ctx, id); err != nil {
if !errors.Is(err, storage.ErrNotFound) {
s.logger.ErrorContext(ctx, "logout: failed to delete refresh token", "token_id", id, "err", err)
}
}
}
return offlineSessions.ConnectorData
}
+326
View File
@@ -0,0 +1,326 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/dexidp/dex/storage"
)
func TestHandleLogoutNoSessions(t *testing.T) {
httpServer, server := newTestServer(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout", nil)
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusNotFound, rr.Code)
}
func TestHandleLogoutMethodNotAllowed(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
req := httptest.NewRequest("PUT", "/logout", nil)
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusMethodNotAllowed, rr.Code)
}
func TestHandleLogoutPOST(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/logout", nil)
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
body := rr.Body.String()
require.Contains(t, body, "No active session")
}
func TestHandleLogoutNoHint(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout", nil)
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
body := rr.Body.String()
require.Contains(t, body, "No active session")
require.NotContains(t, body, "successfully logged out")
}
func TestHandleLogoutInvalidHint(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/logout?id_token_hint=invalid-token", nil)
server.ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestHandleLogoutWithValidHint(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
clientID := "test-client"
postLogoutURI := "https://example.com/done"
userID := "test-user"
connectorID := "mock"
require.NoError(t, server.storage.CreateClient(ctx, storage.Client{
ID: clientID,
Secret: "secret",
RedirectURIs: []string{"https://example.com/callback"},
PostLogoutRedirectURIs: []string{postLogoutURI},
}))
require.NoError(t, server.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID,
ConnectorID: connectorID,
Nonce: "testnonce",
CreatedAt: time.Now(),
LastActivity: time.Now(),
}))
idToken, _, err := server.newIDToken(ctx, clientID, storage.Claims{
UserID: userID, Username: "testuser", Email: "test@example.com",
}, []string{"openid"}, "", "", "", connectorID, time.Now())
require.NoError(t, err)
logoutURL := fmt.Sprintf("/logout?id_token_hint=%s&post_logout_redirect_uri=%s&state=mystate",
url.QueryEscape(idToken), url.QueryEscape(postLogoutURI))
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", logoutURL, nil))
require.Equal(t, http.StatusOK, rr.Code)
body := rr.Body.String()
require.Contains(t, body, "successfully logged out")
require.Contains(t, body, "Back to Application")
require.Contains(t, body, postLogoutURI)
require.Contains(t, body, "state=mystate")
// Session deleted.
_, err = server.storage.GetAuthSession(ctx, userID, connectorID)
require.ErrorIs(t, err, storage.ErrNotFound)
// Cookie cleared.
for _, c := range rr.Result().Cookies() {
if c.Name == "dex_session" {
require.Equal(t, -1, c.MaxAge)
}
}
}
func TestHandleLogoutUnregisteredRedirectURI(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
clientID := "test-client"
require.NoError(t, server.storage.CreateClient(ctx, storage.Client{
ID: clientID,
Secret: "secret",
RedirectURIs: []string{"https://example.com/callback"},
PostLogoutRedirectURIs: []string{"https://example.com/done"},
}))
idToken, _, err := server.newIDToken(ctx, clientID, storage.Claims{
UserID: "user1", Username: "testuser", Email: "test@example.com",
}, []string{"openid"}, "", "", "", "mock", time.Now())
require.NoError(t, err)
logoutURL := fmt.Sprintf("/logout?id_token_hint=%s&post_logout_redirect_uri=%s",
url.QueryEscape(idToken), url.QueryEscape("https://evil.com/steal"))
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", logoutURL, nil))
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestHandleLogoutRedirectURIWithoutHint(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", "/logout?post_logout_redirect_uri=https://example.com/done", nil))
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestHandleLogoutRevokesRefreshTokens(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
clientID := "test-client"
postLogoutURI := "https://example.com/done"
userID := "test-user"
connectorID := "mock"
require.NoError(t, server.storage.CreateClient(ctx, storage.Client{
ID: clientID,
Secret: "secret",
RedirectURIs: []string{"https://example.com/callback"},
PostLogoutRedirectURIs: []string{postLogoutURI},
}))
refreshID := storage.NewID()
require.NoError(t, server.storage.CreateRefresh(ctx, storage.RefreshToken{
ID: refreshID, Token: "token-value", ClientID: clientID, ConnectorID: connectorID,
Claims: storage.Claims{UserID: userID, Username: "testuser", Email: "test@example.com"},
Scopes: []string{"openid", "offline_access"}, CreatedAt: time.Now(), LastUsed: time.Now(),
}))
require.NoError(t, server.storage.CreateOfflineSessions(ctx, storage.OfflineSessions{
UserID: userID, ConnID: connectorID,
Refresh: map[string]*storage.RefreshTokenRef{
clientID: {ID: refreshID, ClientID: clientID, CreatedAt: time.Now(), LastUsed: time.Now()},
},
}))
require.NoError(t, server.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: "testnonce",
CreatedAt: time.Now(), LastActivity: time.Now(),
}))
idToken, _, err := server.newIDToken(ctx, clientID, storage.Claims{
UserID: userID, Username: "testuser", Email: "test@example.com",
}, []string{"openid"}, "", "", "", connectorID, time.Now())
require.NoError(t, err)
logoutURL := fmt.Sprintf("/logout?id_token_hint=%s&post_logout_redirect_uri=%s",
url.QueryEscape(idToken), url.QueryEscape(postLogoutURI))
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", logoutURL, nil))
require.Equal(t, http.StatusOK, rr.Code)
_, err = server.storage.GetRefresh(ctx, refreshID)
require.ErrorIs(t, err, storage.ErrNotFound)
os, err := server.storage.GetOfflineSessions(ctx, userID, connectorID)
require.NoError(t, err)
require.Empty(t, os.Refresh)
}
func TestHandleLogoutRepeat(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := t.Context()
clientID := "test-client"
postLogoutURI := "https://example.com/done"
userID := "test-user"
connectorID := "mock"
require.NoError(t, server.storage.CreateClient(ctx, storage.Client{
ID: clientID, Secret: "secret",
RedirectURIs: []string{"https://example.com/callback"},
PostLogoutRedirectURIs: []string{postLogoutURI},
}))
require.NoError(t, server.storage.CreateAuthSession(ctx, storage.AuthSession{
UserID: userID, ConnectorID: connectorID, Nonce: "testnonce",
CreatedAt: time.Now(), LastActivity: time.Now(),
}))
idToken, _, err := server.newIDToken(ctx, clientID, storage.Claims{
UserID: userID, Username: "testuser", Email: "test@example.com",
}, []string{"openid"}, "", "", "", connectorID, time.Now())
require.NoError(t, err)
logoutURL := fmt.Sprintf("/logout?id_token_hint=%s&post_logout_redirect_uri=%s",
url.QueryEscape(idToken), url.QueryEscape(postLogoutURI))
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", logoutURL, nil))
require.Equal(t, http.StatusOK, rr.Code)
require.Contains(t, rr.Body.String(), "successfully logged out")
// Second logout — session already deleted, shows "no active session".
rr = httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", logoutURL, nil))
require.Equal(t, http.StatusOK, rr.Code)
require.Contains(t, rr.Body.String(), "No active session")
}
func TestLogoutCallbackNoState(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", "/logout/callback", nil))
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestDiscoveryWithSessions(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/openid-configuration", nil))
require.Equal(t, http.StatusOK, rr.Code)
var d discovery
require.NoError(t, json.NewDecoder(rr.Result().Body).Decode(&d))
require.Equal(t, fmt.Sprintf("%s/logout", httpServer.URL), d.EndSession)
}
func TestDiscoveryWithoutSessions(t *testing.T) {
httpServer, server := newTestServer(t, nil)
defer httpServer.Close()
rr := httptest.NewRecorder()
server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/openid-configuration", nil))
require.Equal(t, http.StatusOK, rr.Code)
var d discovery
require.NoError(t, json.NewDecoder(rr.Result().Body).Decode(&d))
require.Empty(t, d.EndSession)
}
func TestRevokeRefreshTokensReturnsConnectorData(t *testing.T) {
httpServer, server := newTestServerWithSessions(t, nil)
defer httpServer.Close()
ctx := context.Background()
userID := "user1"
connectorID := "mock"
expectedConnData := []byte(`{"RefreshToken":"abc"}`)
refreshID := storage.NewID()
require.NoError(t, server.storage.CreateRefresh(ctx, storage.RefreshToken{
ID: refreshID, Token: "tok", ClientID: "client1", ConnectorID: connectorID,
Claims: storage.Claims{UserID: userID}, CreatedAt: time.Now(), LastUsed: time.Now(),
}))
require.NoError(t, server.storage.CreateOfflineSessions(ctx, storage.OfflineSessions{
UserID: userID, ConnID: connectorID, ConnectorData: expectedConnData,
Refresh: map[string]*storage.RefreshTokenRef{"client1": {ID: refreshID, ClientID: "client1"}},
}))
connData := server.revokeRefreshTokens(ctx, userID, connectorID)
require.Equal(t, expectedConnData, connData)
_, err := server.storage.GetRefresh(ctx, refreshID)
require.ErrorIs(t, err, storage.ErrNotFound)
os, err := server.storage.GetOfflineSessions(ctx, userID, connectorID)
require.NoError(t, err)
require.Empty(t, os.Refresh)
require.Equal(t, expectedConnData, os.ConnectorData)
}
+5 -9
View File
@@ -444,8 +444,8 @@ func (s *Server) newIDToken(ctx context.Context, clientID string, claims storage
// validateIDTokenHint verifies the signature and issuer of an id_token_hint.
// Expired tokens are accepted per OIDC Core 1.0 §3.1.2.1.
// Returns the raw subject claim from the token.
func (s *Server) validateIDTokenHint(ctx context.Context, hint string) (string, error) {
// Returns the verified token so callers can extract Subject, Audience, etc.
func (s *Server) validateIDTokenHint(ctx context.Context, hint string) (*oidc.IDToken, error) {
verifier := oidc.NewVerifier(s.issuerURL.String(), &signerKeySet{s.signer}, &oidc.Config{
SkipExpiryCheck: true,
// SkipClientIDCheck is set because the hint may originate from any client that
@@ -455,11 +455,7 @@ func (s *Server) validateIDTokenHint(ctx context.Context, hint string) (string,
// Dex does the client id check later in the scope of the session validation.
SkipClientIDCheck: true,
})
idToken, err := verifier.Verify(ctx, hint)
if err != nil {
return "", err
}
return idToken.Subject, nil
return verifier.Verify(ctx, hint)
}
// sessionMatchesHint checks whether the session's user identity matches the
@@ -662,11 +658,11 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
// Validate id_token_hint if provided (OIDC Core 1.0 §3.1.2.1).
var idTokenHintSubject string
if hint := q.Get("id_token_hint"); hint != "" {
sub, err := s.validateIDTokenHint(ctx, hint)
idToken, err := s.validateIDTokenHint(ctx, hint)
if err != nil {
return nil, "", newRedirectedErr(errInvalidRequest, "Invalid id_token_hint.")
}
idTokenHintSubject = sub
idTokenHintSubject = idToken.Subject
}
return &storage.AuthRequest{
+4 -4
View File
@@ -952,9 +952,9 @@ func TestValidateIDTokenHint(t *testing.T) {
Subject: "CgNmb28SA2Jhcg",
Expiry: now.Add(1 * time.Hour).Unix(),
})
sub, err := s.validateIDTokenHint(t.Context(), token)
idToken, err := s.validateIDTokenHint(t.Context(), token)
require.NoError(t, err)
assert.Equal(t, "CgNmb28SA2Jhcg", sub)
assert.Equal(t, "CgNmb28SA2Jhcg", idToken.Subject)
})
t.Run("valid hint (expired)", func(t *testing.T) {
@@ -963,9 +963,9 @@ func TestValidateIDTokenHint(t *testing.T) {
Subject: "CgNmb28SA2Jhcg",
Expiry: now.Add(-1 * time.Hour).Unix(),
})
sub, err := s.validateIDTokenHint(t.Context(), token)
idToken, err := s.validateIDTokenHint(t.Context(), token)
require.NoError(t, err)
assert.Equal(t, "CgNmb28SA2Jhcg", sub)
assert.Equal(t, "CgNmb28SA2Jhcg", idToken.Subject)
})
t.Run("invalid signature", func(t *testing.T) {
+4
View File
@@ -551,6 +551,10 @@ func newServer(ctx context.Context, c Config) (*Server, error) {
// "authproxy" connector.
handleFunc("/callback/{connector}", s.handleConnectorCallback)
handleFunc("/approval", s.handleApproval)
if c.SessionConfig != nil {
handleFunc("/logout", s.handleLogout)
handleFunc("/logout/callback", s.handleLogoutCallback)
}
handleFunc("/mfa/verify", s.handleMFAVerify)
handle("/healthz", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !c.HealthChecker.IsHealthy() {
+12
View File
@@ -24,6 +24,7 @@ const (
tmplDeviceSuccess = "device_success.html"
tmplTOTPVerify = "totp_verify.html"
tmplHome = "home.html"
tmplLogout = "logout.html"
)
var requiredTmpls = []string{
@@ -46,6 +47,7 @@ type templates struct {
deviceSuccessTmpl *template.Template
totpVerifyTmpl *template.Template
homeTmpl *template.Template
logoutTmpl *template.Template
}
type webConfig struct {
@@ -175,6 +177,7 @@ func loadTemplates(c webConfig, templatesDir string) (*templates, error) {
deviceSuccessTmpl: tmpls.Lookup(tmplDeviceSuccess),
totpVerifyTmpl: tmpls.Lookup(tmplTOTPVerify),
homeTmpl: tmpls.Lookup(tmplHome),
logoutTmpl: tmpls.Lookup(tmplLogout),
}, nil
}
@@ -386,6 +389,15 @@ func (t *templates) home(r *http.Request, w http.ResponseWriter, data homeData)
return renderTemplate(w, t.homeTmpl, data)
}
func (t *templates) logout(r *http.Request, w http.ResponseWriter, backURL string, loggedOut bool) error {
data := struct {
BackURL string
LoggedOut bool
ReqPath string
}{backURL, loggedOut, r.URL.Path}
return renderTemplate(w, t.logoutTmpl, data)
}
func (t *templates) oob(r *http.Request, w http.ResponseWriter, code string) error {
data := struct {
Code string
+2
View File
@@ -18,6 +18,7 @@ func (d *Database) CreateClient(ctx context.Context, client storage.Client) erro
SetTrustedPeers(client.TrustedPeers).
SetAllowedConnectors(client.AllowedConnectors).
SetMfaChain(client.MFAChain).
SetPostLogoutRedirectUris(client.PostLogoutRedirectURIs).
Save(ctx)
if err != nil {
return convertDBError("create oauth2 client: %w", err)
@@ -83,6 +84,7 @@ func (d *Database) UpdateClient(ctx context.Context, id string, updater func(old
SetTrustedPeers(newClient.TrustedPeers).
SetAllowedConnectors(newClient.AllowedConnectors).
SetMfaChain(newClient.MFAChain).
SetPostLogoutRedirectUris(newClient.PostLogoutRedirectURIs).
Save(ctx)
if err != nil {
return rollback(tx, "update client uploading: %w", err)
+10 -9
View File
@@ -81,15 +81,16 @@ func toStorageAuthCode(a *db.AuthCode) storage.AuthCode {
func toStorageClient(c *db.OAuth2Client) storage.Client {
return storage.Client{
ID: c.ID,
Secret: c.Secret,
RedirectURIs: c.RedirectUris,
TrustedPeers: c.TrustedPeers,
Public: c.Public,
Name: c.Name,
LogoURL: c.LogoURL,
AllowedConnectors: c.AllowedConnectors,
MFAChain: c.MfaChain,
ID: c.ID,
Secret: c.Secret,
RedirectURIs: c.RedirectUris,
TrustedPeers: c.TrustedPeers,
Public: c.Public,
Name: c.Name,
LogoURL: c.LogoURL,
AllowedConnectors: c.AllowedConnectors,
MFAChain: c.MfaChain,
PostLogoutRedirectURIs: c.PostLogoutRedirectUris,
}
}
+1
View File
@@ -162,6 +162,7 @@ var (
{Name: "logo_url", Type: field.TypeString, Size: 2147483647, SchemaType: map[string]string{"mysql": "varchar(384)", "postgres": "text", "sqlite3": "text"}},
{Name: "allowed_connectors", Type: field.TypeJSON, Nullable: true},
{Name: "mfa_chain", Type: field.TypeJSON, Nullable: true},
{Name: "post_logout_redirect_uris", Type: field.TypeJSON, Nullable: true},
}
// Oauth2clientsTable holds the schema information for the "oauth2clients" table.
Oauth2clientsTable = &schema.Table{
+110 -20
View File
@@ -6380,25 +6380,27 @@ func (m *KeysMutation) ResetEdge(name string) error {
// OAuth2ClientMutation represents an operation that mutates the OAuth2Client nodes in the graph.
type OAuth2ClientMutation struct {
config
op Op
typ string
id *string
secret *string
redirect_uris *[]string
appendredirect_uris []string
trusted_peers *[]string
appendtrusted_peers []string
public *bool
name *string
logo_url *string
allowed_connectors *[]string
appendallowed_connectors []string
mfa_chain *[]string
appendmfa_chain []string
clearedFields map[string]struct{}
done bool
oldValue func(context.Context) (*OAuth2Client, error)
predicates []predicate.OAuth2Client
op Op
typ string
id *string
secret *string
redirect_uris *[]string
appendredirect_uris []string
trusted_peers *[]string
appendtrusted_peers []string
public *bool
name *string
logo_url *string
allowed_connectors *[]string
appendallowed_connectors []string
mfa_chain *[]string
appendmfa_chain []string
post_logout_redirect_uris *[]string
appendpost_logout_redirect_uris []string
clearedFields map[string]struct{}
done bool
oldValue func(context.Context) (*OAuth2Client, error)
predicates []predicate.OAuth2Client
}
var _ ent.Mutation = (*OAuth2ClientMutation)(nil)
@@ -6909,6 +6911,71 @@ func (m *OAuth2ClientMutation) ResetMfaChain() {
delete(m.clearedFields, oauth2client.FieldMfaChain)
}
// SetPostLogoutRedirectUris sets the "post_logout_redirect_uris" field.
func (m *OAuth2ClientMutation) SetPostLogoutRedirectUris(s []string) {
m.post_logout_redirect_uris = &s
m.appendpost_logout_redirect_uris = nil
}
// PostLogoutRedirectUris returns the value of the "post_logout_redirect_uris" field in the mutation.
func (m *OAuth2ClientMutation) PostLogoutRedirectUris() (r []string, exists bool) {
v := m.post_logout_redirect_uris
if v == nil {
return
}
return *v, true
}
// OldPostLogoutRedirectUris returns the old "post_logout_redirect_uris" field's value of the OAuth2Client entity.
// If the OAuth2Client object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *OAuth2ClientMutation) OldPostLogoutRedirectUris(ctx context.Context) (v []string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldPostLogoutRedirectUris is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldPostLogoutRedirectUris requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldPostLogoutRedirectUris: %w", err)
}
return oldValue.PostLogoutRedirectUris, nil
}
// AppendPostLogoutRedirectUris adds s to the "post_logout_redirect_uris" field.
func (m *OAuth2ClientMutation) AppendPostLogoutRedirectUris(s []string) {
m.appendpost_logout_redirect_uris = append(m.appendpost_logout_redirect_uris, s...)
}
// AppendedPostLogoutRedirectUris returns the list of values that were appended to the "post_logout_redirect_uris" field in this mutation.
func (m *OAuth2ClientMutation) AppendedPostLogoutRedirectUris() ([]string, bool) {
if len(m.appendpost_logout_redirect_uris) == 0 {
return nil, false
}
return m.appendpost_logout_redirect_uris, true
}
// ClearPostLogoutRedirectUris clears the value of the "post_logout_redirect_uris" field.
func (m *OAuth2ClientMutation) ClearPostLogoutRedirectUris() {
m.post_logout_redirect_uris = nil
m.appendpost_logout_redirect_uris = nil
m.clearedFields[oauth2client.FieldPostLogoutRedirectUris] = struct{}{}
}
// PostLogoutRedirectUrisCleared returns if the "post_logout_redirect_uris" field was cleared in this mutation.
func (m *OAuth2ClientMutation) PostLogoutRedirectUrisCleared() bool {
_, ok := m.clearedFields[oauth2client.FieldPostLogoutRedirectUris]
return ok
}
// ResetPostLogoutRedirectUris resets all changes to the "post_logout_redirect_uris" field.
func (m *OAuth2ClientMutation) ResetPostLogoutRedirectUris() {
m.post_logout_redirect_uris = nil
m.appendpost_logout_redirect_uris = nil
delete(m.clearedFields, oauth2client.FieldPostLogoutRedirectUris)
}
// Where appends a list predicates to the OAuth2ClientMutation builder.
func (m *OAuth2ClientMutation) Where(ps ...predicate.OAuth2Client) {
m.predicates = append(m.predicates, ps...)
@@ -6943,7 +7010,7 @@ func (m *OAuth2ClientMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *OAuth2ClientMutation) Fields() []string {
fields := make([]string, 0, 8)
fields := make([]string, 0, 9)
if m.secret != nil {
fields = append(fields, oauth2client.FieldSecret)
}
@@ -6968,6 +7035,9 @@ func (m *OAuth2ClientMutation) Fields() []string {
if m.mfa_chain != nil {
fields = append(fields, oauth2client.FieldMfaChain)
}
if m.post_logout_redirect_uris != nil {
fields = append(fields, oauth2client.FieldPostLogoutRedirectUris)
}
return fields
}
@@ -6992,6 +7062,8 @@ func (m *OAuth2ClientMutation) Field(name string) (ent.Value, bool) {
return m.AllowedConnectors()
case oauth2client.FieldMfaChain:
return m.MfaChain()
case oauth2client.FieldPostLogoutRedirectUris:
return m.PostLogoutRedirectUris()
}
return nil, false
}
@@ -7017,6 +7089,8 @@ func (m *OAuth2ClientMutation) OldField(ctx context.Context, name string) (ent.V
return m.OldAllowedConnectors(ctx)
case oauth2client.FieldMfaChain:
return m.OldMfaChain(ctx)
case oauth2client.FieldPostLogoutRedirectUris:
return m.OldPostLogoutRedirectUris(ctx)
}
return nil, fmt.Errorf("unknown OAuth2Client field %s", name)
}
@@ -7082,6 +7156,13 @@ func (m *OAuth2ClientMutation) SetField(name string, value ent.Value) error {
}
m.SetMfaChain(v)
return nil
case oauth2client.FieldPostLogoutRedirectUris:
v, ok := value.([]string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetPostLogoutRedirectUris(v)
return nil
}
return fmt.Errorf("unknown OAuth2Client field %s", name)
}
@@ -7124,6 +7205,9 @@ func (m *OAuth2ClientMutation) ClearedFields() []string {
if m.FieldCleared(oauth2client.FieldMfaChain) {
fields = append(fields, oauth2client.FieldMfaChain)
}
if m.FieldCleared(oauth2client.FieldPostLogoutRedirectUris) {
fields = append(fields, oauth2client.FieldPostLogoutRedirectUris)
}
return fields
}
@@ -7150,6 +7234,9 @@ func (m *OAuth2ClientMutation) ClearField(name string) error {
case oauth2client.FieldMfaChain:
m.ClearMfaChain()
return nil
case oauth2client.FieldPostLogoutRedirectUris:
m.ClearPostLogoutRedirectUris()
return nil
}
return fmt.Errorf("unknown OAuth2Client nullable field %s", name)
}
@@ -7182,6 +7269,9 @@ func (m *OAuth2ClientMutation) ResetField(name string) error {
case oauth2client.FieldMfaChain:
m.ResetMfaChain()
return nil
case oauth2client.FieldPostLogoutRedirectUris:
m.ResetPostLogoutRedirectUris()
return nil
}
return fmt.Errorf("unknown OAuth2Client field %s", name)
}
+16 -3
View File
@@ -32,8 +32,10 @@ type OAuth2Client struct {
// AllowedConnectors holds the value of the "allowed_connectors" field.
AllowedConnectors []string `json:"allowed_connectors,omitempty"`
// MfaChain holds the value of the "mfa_chain" field.
MfaChain []string `json:"mfa_chain,omitempty"`
selectValues sql.SelectValues
MfaChain []string `json:"mfa_chain,omitempty"`
// PostLogoutRedirectUris holds the value of the "post_logout_redirect_uris" field.
PostLogoutRedirectUris []string `json:"post_logout_redirect_uris,omitempty"`
selectValues sql.SelectValues
}
// scanValues returns the types for scanning values from sql.Rows.
@@ -41,7 +43,7 @@ func (*OAuth2Client) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case oauth2client.FieldRedirectUris, oauth2client.FieldTrustedPeers, oauth2client.FieldAllowedConnectors, oauth2client.FieldMfaChain:
case oauth2client.FieldRedirectUris, oauth2client.FieldTrustedPeers, oauth2client.FieldAllowedConnectors, oauth2client.FieldMfaChain, oauth2client.FieldPostLogoutRedirectUris:
values[i] = new([]byte)
case oauth2client.FieldPublic:
values[i] = new(sql.NullBool)
@@ -124,6 +126,14 @@ func (_m *OAuth2Client) assignValues(columns []string, values []any) error {
return fmt.Errorf("unmarshal field mfa_chain: %w", err)
}
}
case oauth2client.FieldPostLogoutRedirectUris:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field post_logout_redirect_uris", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.PostLogoutRedirectUris); err != nil {
return fmt.Errorf("unmarshal field post_logout_redirect_uris: %w", err)
}
}
default:
_m.selectValues.Set(columns[i], values[i])
}
@@ -183,6 +193,9 @@ func (_m *OAuth2Client) String() string {
builder.WriteString(", ")
builder.WriteString("mfa_chain=")
builder.WriteString(fmt.Sprintf("%v", _m.MfaChain))
builder.WriteString(", ")
builder.WriteString("post_logout_redirect_uris=")
builder.WriteString(fmt.Sprintf("%v", _m.PostLogoutRedirectUris))
builder.WriteByte(')')
return builder.String()
}
@@ -27,6 +27,8 @@ const (
FieldAllowedConnectors = "allowed_connectors"
// FieldMfaChain holds the string denoting the mfa_chain field in the database.
FieldMfaChain = "mfa_chain"
// FieldPostLogoutRedirectUris holds the string denoting the post_logout_redirect_uris field in the database.
FieldPostLogoutRedirectUris = "post_logout_redirect_uris"
// Table holds the table name of the oauth2client in the database.
Table = "oauth2clients"
)
@@ -42,6 +44,7 @@ var Columns = []string{
FieldLogoURL,
FieldAllowedConnectors,
FieldMfaChain,
FieldPostLogoutRedirectUris,
}
// ValidColumn reports if the column name is valid (part of the table columns).
+10
View File
@@ -327,6 +327,16 @@ func MfaChainNotNil() predicate.OAuth2Client {
return predicate.OAuth2Client(sql.FieldNotNull(FieldMfaChain))
}
// PostLogoutRedirectUrisIsNil applies the IsNil predicate on the "post_logout_redirect_uris" field.
func PostLogoutRedirectUrisIsNil() predicate.OAuth2Client {
return predicate.OAuth2Client(sql.FieldIsNull(FieldPostLogoutRedirectUris))
}
// PostLogoutRedirectUrisNotNil applies the NotNil predicate on the "post_logout_redirect_uris" field.
func PostLogoutRedirectUrisNotNil() predicate.OAuth2Client {
return predicate.OAuth2Client(sql.FieldNotNull(FieldPostLogoutRedirectUris))
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.OAuth2Client) predicate.OAuth2Client {
return predicate.OAuth2Client(sql.AndPredicates(predicates...))
+10
View File
@@ -67,6 +67,12 @@ func (_c *OAuth2ClientCreate) SetMfaChain(v []string) *OAuth2ClientCreate {
return _c
}
// SetPostLogoutRedirectUris sets the "post_logout_redirect_uris" field.
func (_c *OAuth2ClientCreate) SetPostLogoutRedirectUris(v []string) *OAuth2ClientCreate {
_c.mutation.SetPostLogoutRedirectUris(v)
return _c
}
// SetID sets the "id" field.
func (_c *OAuth2ClientCreate) SetID(v string) *OAuth2ClientCreate {
_c.mutation.SetID(v)
@@ -206,6 +212,10 @@ func (_c *OAuth2ClientCreate) createSpec() (*OAuth2Client, *sqlgraph.CreateSpec)
_spec.SetField(oauth2client.FieldMfaChain, field.TypeJSON, value)
_node.MfaChain = value
}
if value, ok := _c.mutation.PostLogoutRedirectUris(); ok {
_spec.SetField(oauth2client.FieldPostLogoutRedirectUris, field.TypeJSON, value)
_node.PostLogoutRedirectUris = value
}
return _node, _spec
}
+58
View File
@@ -156,6 +156,24 @@ func (_u *OAuth2ClientUpdate) ClearMfaChain() *OAuth2ClientUpdate {
return _u
}
// SetPostLogoutRedirectUris sets the "post_logout_redirect_uris" field.
func (_u *OAuth2ClientUpdate) SetPostLogoutRedirectUris(v []string) *OAuth2ClientUpdate {
_u.mutation.SetPostLogoutRedirectUris(v)
return _u
}
// AppendPostLogoutRedirectUris appends value to the "post_logout_redirect_uris" field.
func (_u *OAuth2ClientUpdate) AppendPostLogoutRedirectUris(v []string) *OAuth2ClientUpdate {
_u.mutation.AppendPostLogoutRedirectUris(v)
return _u
}
// ClearPostLogoutRedirectUris clears the value of the "post_logout_redirect_uris" field.
func (_u *OAuth2ClientUpdate) ClearPostLogoutRedirectUris() *OAuth2ClientUpdate {
_u.mutation.ClearPostLogoutRedirectUris()
return _u
}
// Mutation returns the OAuth2ClientMutation object of the builder.
func (_u *OAuth2ClientUpdate) Mutation() *OAuth2ClientMutation {
return _u.mutation
@@ -276,6 +294,17 @@ func (_u *OAuth2ClientUpdate) sqlSave(ctx context.Context) (_node int, err error
if _u.mutation.MfaChainCleared() {
_spec.ClearField(oauth2client.FieldMfaChain, field.TypeJSON)
}
if value, ok := _u.mutation.PostLogoutRedirectUris(); ok {
_spec.SetField(oauth2client.FieldPostLogoutRedirectUris, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedPostLogoutRedirectUris(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, oauth2client.FieldPostLogoutRedirectUris, value)
})
}
if _u.mutation.PostLogoutRedirectUrisCleared() {
_spec.ClearField(oauth2client.FieldPostLogoutRedirectUris, field.TypeJSON)
}
if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil {
if _, ok := err.(*sqlgraph.NotFoundError); ok {
err = &NotFoundError{oauth2client.Label}
@@ -424,6 +453,24 @@ func (_u *OAuth2ClientUpdateOne) ClearMfaChain() *OAuth2ClientUpdateOne {
return _u
}
// SetPostLogoutRedirectUris sets the "post_logout_redirect_uris" field.
func (_u *OAuth2ClientUpdateOne) SetPostLogoutRedirectUris(v []string) *OAuth2ClientUpdateOne {
_u.mutation.SetPostLogoutRedirectUris(v)
return _u
}
// AppendPostLogoutRedirectUris appends value to the "post_logout_redirect_uris" field.
func (_u *OAuth2ClientUpdateOne) AppendPostLogoutRedirectUris(v []string) *OAuth2ClientUpdateOne {
_u.mutation.AppendPostLogoutRedirectUris(v)
return _u
}
// ClearPostLogoutRedirectUris clears the value of the "post_logout_redirect_uris" field.
func (_u *OAuth2ClientUpdateOne) ClearPostLogoutRedirectUris() *OAuth2ClientUpdateOne {
_u.mutation.ClearPostLogoutRedirectUris()
return _u
}
// Mutation returns the OAuth2ClientMutation object of the builder.
func (_u *OAuth2ClientUpdateOne) Mutation() *OAuth2ClientMutation {
return _u.mutation
@@ -574,6 +621,17 @@ func (_u *OAuth2ClientUpdateOne) sqlSave(ctx context.Context) (_node *OAuth2Clie
if _u.mutation.MfaChainCleared() {
_spec.ClearField(oauth2client.FieldMfaChain, field.TypeJSON)
}
if value, ok := _u.mutation.PostLogoutRedirectUris(); ok {
_spec.SetField(oauth2client.FieldPostLogoutRedirectUris, field.TypeJSON, value)
}
if value, ok := _u.mutation.AppendedPostLogoutRedirectUris(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, oauth2client.FieldPostLogoutRedirectUris, value)
})
}
if _u.mutation.PostLogoutRedirectUrisCleared() {
_spec.ClearField(oauth2client.FieldPostLogoutRedirectUris, field.TypeJSON)
}
_node = &OAuth2Client{config: _u.config}
_spec.Assign = _node.assignValues
_spec.ScanValues = _node.scanValues
+2
View File
@@ -49,6 +49,8 @@ func (OAuth2Client) Fields() []ent.Field {
Optional(),
field.JSON("mfa_chain", []string{}).
Optional(),
field.JSON("post_logout_redirect_uris", []string{}).
Optional(),
}
}

Some files were not shown because too many files have changed in this diff Show More