mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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...))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user