mirror of
https://github.com/netbirdio/dex.git
synced 2026-05-22 18:43:53 -07:00
feat: add SSO sharing policy (#4705)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
This commit is contained in:
+295
-264
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ message Client {
|
||||
string name = 6;
|
||||
string logo_url = 7;
|
||||
repeated string allowed_connectors = 8;
|
||||
repeated string sso_shared_with = 9;
|
||||
}
|
||||
|
||||
// ClientInfo represents an OAuth2 client without sensitive information.
|
||||
@@ -26,6 +27,7 @@ message ClientInfo {
|
||||
string name = 5;
|
||||
string logo_url = 6;
|
||||
repeated string allowed_connectors = 7;
|
||||
repeated string sso_shared_with = 8;
|
||||
}
|
||||
|
||||
// GetClientReq is a request to retrieve client details.
|
||||
@@ -69,6 +71,7 @@ message UpdateClientReq {
|
||||
string name = 4;
|
||||
string logo_url = 5;
|
||||
repeated string allowed_connectors = 6;
|
||||
repeated string sso_shared_with = 7;
|
||||
}
|
||||
|
||||
// UpdateClientResp returns the response from updating a client.
|
||||
|
||||
@@ -687,6 +687,9 @@ type Sessions struct {
|
||||
// Must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256.
|
||||
// If empty, cookies are not encrypted.
|
||||
CookieEncryptionKey string `json:"cookieEncryptionKey"`
|
||||
// SSOSharedWithDefault is the default SSO sharing policy for clients without explicit ssoSharedWith.
|
||||
// "all" = share with all clients, "none" = share with no one (default: "none").
|
||||
SSOSharedWithDefault string `json:"ssoSharedWithDefault"`
|
||||
}
|
||||
|
||||
// MFAAuthenticator defines a multi-factor authentication provider.
|
||||
|
||||
@@ -807,6 +807,9 @@ func parseSessionConfig(s *Sessions) (*server.SessionConfig, error) {
|
||||
if s.CookieEncryptionKey != "" {
|
||||
sc.CookieEncryptionKey = []byte(s.CookieEncryptionKey)
|
||||
}
|
||||
if s.SSOSharedWithDefault != "" {
|
||||
sc.SSOSharedWithDefault = s.SSOSharedWithDefault
|
||||
}
|
||||
}
|
||||
if sc.AbsoluteLifetime <= 0 {
|
||||
return nil, fmt.Errorf("absoluteLifetime must be positive, got %v", sc.AbsoluteLifetime)
|
||||
@@ -820,6 +823,12 @@ func parseSessionConfig(s *Sessions) (*server.SessionConfig, error) {
|
||||
if k := len(sc.CookieEncryptionKey); k > 0 && k != 16 && k != 24 && k != 32 {
|
||||
return nil, fmt.Errorf("cookieEncryptionKey must be 16, 24, or 32 bytes (AES-128/192/256), got %d", k)
|
||||
}
|
||||
switch sc.SSOSharedWithDefault {
|
||||
case "", "none", "all":
|
||||
// valid
|
||||
default:
|
||||
return nil, fmt.Errorf("ssoSharedWithDefault must be \"none\" or \"all\", got %q", sc.SSOSharedWithDefault)
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,20 @@ web:
|
||||
# keysRotationPeriod: "6h"
|
||||
# algorithm: "RS256" # supported values: "RS256" (default) and "ES256"; changes apply on the next key rotation
|
||||
|
||||
# Authentication sessions configuration.
|
||||
# Requires DEX_SESSIONS_ENABLED=true feature flag.
|
||||
# sessions:
|
||||
# cookieName: "dex_session"
|
||||
# absoluteLifetime: "24h"
|
||||
# validIfNotUsedFor: "1h"
|
||||
# rememberMeCheckedByDefault: false
|
||||
# # AES key for encrypting session cookies. Must be 16, 24, or 32 bytes.
|
||||
# # If empty, cookies are not encrypted.
|
||||
# cookieEncryptionKey: ""
|
||||
# # Default SSO sharing policy for clients without explicit ssoSharedWith.
|
||||
# # "all" = share with all clients (Keycloak-like), "none" = no sharing (default).
|
||||
# ssoSharedWithDefault: "none"
|
||||
|
||||
# OAuth2 configuration
|
||||
# oauth2:
|
||||
# # use ["code", "token", "id_token"] to enable implicit flow for web-only clients
|
||||
@@ -159,6 +173,19 @@ web:
|
||||
# allowedConnectors:
|
||||
# - github
|
||||
# - google
|
||||
#
|
||||
# # Example of SSO sharing between clients.
|
||||
# # ssoSharedWith defines which other clients can reuse this client's session.
|
||||
# # ["*"] = share with all, [] = share with no one.
|
||||
# # If omitted, ssoSharedWithDefault from sessions config is used.
|
||||
# - id: portal-app
|
||||
# secret: portal-secret
|
||||
# redirectURIs:
|
||||
# - 'https://portal.example.com/callback'
|
||||
# name: 'Portal'
|
||||
# ssoSharedWith:
|
||||
# - "dashboard-app"
|
||||
# - "admin-app"
|
||||
|
||||
# Connectors are used to authenticate users against upstream identity providers.
|
||||
#
|
||||
|
||||
@@ -102,6 +102,9 @@ telemetry:
|
||||
# absoluteLifetime: "24h"
|
||||
# validIfNotUsedFor: "1h"
|
||||
# rememberMeCheckedByDefault: false
|
||||
# # Default SSO sharing policy for clients without explicit ssoSharedWith.
|
||||
# # "all" = share with all clients (Keycloak-like), "none" = no sharing (default).
|
||||
# ssoSharedWithDefault: "none"
|
||||
|
||||
# Options for controlling the logger.
|
||||
# logger:
|
||||
@@ -187,6 +190,11 @@ staticClients:
|
||||
# If omitted, mfa.defaultMFAChain is used.
|
||||
# mfaChain:
|
||||
# - totp-1
|
||||
# Optional: which other clients can reuse this client's authentication session (SSO).
|
||||
# ["*"] = share with all clients, [] = share with no one.
|
||||
# If omitted, ssoSharedWithDefault from sessions config is used.
|
||||
# ssoSharedWith:
|
||||
# - "*"
|
||||
|
||||
# Example using environment variables
|
||||
# Set DEX_CLIENT_ID and DEX_SECURE_CLIENT_SECRET before starting Dex
|
||||
|
||||
@@ -66,6 +66,7 @@ func (d dexAPI) GetClient(ctx context.Context, req *api.GetClientReq) (*api.GetC
|
||||
Public: c.Public,
|
||||
LogoUrl: c.LogoURL,
|
||||
AllowedConnectors: c.AllowedConnectors,
|
||||
SsoSharedWith: c.SSOSharedWith,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -91,6 +92,7 @@ func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*ap
|
||||
Name: req.Client.Name,
|
||||
LogoURL: req.Client.LogoUrl,
|
||||
AllowedConnectors: req.Client.AllowedConnectors,
|
||||
SSOSharedWith: req.Client.SsoSharedWith,
|
||||
}
|
||||
if err := d.s.CreateClient(ctx, c); err != nil {
|
||||
if err == storage.ErrAlreadyExists {
|
||||
@@ -126,6 +128,9 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap
|
||||
if req.AllowedConnectors != nil {
|
||||
old.AllowedConnectors = req.AllowedConnectors
|
||||
}
|
||||
if req.SsoSharedWith != nil {
|
||||
old.SSOSharedWith = req.SsoSharedWith
|
||||
}
|
||||
return old, nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -167,6 +172,7 @@ func (d dexAPI) ListClients(ctx context.Context, req *api.ListClientReq) (*api.L
|
||||
Public: client.Public,
|
||||
LogoUrl: client.LogoURL,
|
||||
AllowedConnectors: client.AllowedConnectors,
|
||||
SsoSharedWith: client.SSOSharedWith,
|
||||
}
|
||||
clients = append(clients, &c)
|
||||
}
|
||||
|
||||
@@ -246,6 +246,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
panic("unsupported error type")
|
||||
}
|
||||
return
|
||||
}
|
||||
prompt, err := ParsePrompt(authReq.Prompt)
|
||||
if err != nil {
|
||||
|
||||
@@ -1604,6 +1604,30 @@ func TestHandleAuthorizationConnectorGrantTypeFiltering(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAuthorizationInvalidRequestWithSessions(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
httpServer, s := newTestServerMultipleConnectors(t, func(c *Config) {
|
||||
c.SessionConfig = &SessionConfig{
|
||||
CookieName: "dex_session",
|
||||
AbsoluteLifetime: 24 * time.Hour,
|
||||
ValidIfNotUsedFor: 1 * time.Hour,
|
||||
}
|
||||
c.Storage.CreateClient(ctx, storage.Client{
|
||||
ID: "test",
|
||||
RedirectURIs: []string{"http://example.com/callback"},
|
||||
})
|
||||
})
|
||||
defer httpServer.Close()
|
||||
|
||||
// Send a request with an unregistered redirect_uri — should not panic.
|
||||
rr := httptest.NewRecorder()
|
||||
reqURL := fmt.Sprintf("%s/auth?response_type=code&client_id=test&redirect_uri=http://evil.com/callback&scope=openid", httpServer.URL)
|
||||
req := httptest.NewRequest(http.MethodGet, reqURL, nil)
|
||||
s.handleAuthorization(rr, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
}
|
||||
|
||||
func TestHandleConnectorLoginGrantTypeRejection(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
httpServer, s := newTestServer(t, func(c *Config) {
|
||||
|
||||
@@ -156,6 +156,9 @@ type SessionConfig struct {
|
||||
AbsoluteLifetime time.Duration
|
||||
ValidIfNotUsedFor time.Duration
|
||||
RememberMeCheckedByDefault bool
|
||||
// SSOSharedWithDefault is the default SSO sharing policy for clients without explicit SSOSharedWith.
|
||||
// "all" = share with all clients, "none" or "" = share with no one (default).
|
||||
SSOSharedWithDefault string
|
||||
}
|
||||
|
||||
// WebConfig holds the server's frontend templates and asset configuration.
|
||||
|
||||
+108
-11
@@ -306,6 +306,65 @@ func (s *Server) trySessionLogin(ctx context.Context, r *http.Request, w http.Re
|
||||
return s.trySessionLoginWithSession(ctx, r, w, authReq, session)
|
||||
}
|
||||
|
||||
// clientSharesSessionWith checks if sourceClient shares its session with targetClientID.
|
||||
// SSO sharing is unidirectional: source sharing with target does NOT mean target shares with source.
|
||||
func (s *Server) clientSharesSessionWith(sourceClient storage.Client, targetClientID string) bool {
|
||||
ssoSharedWith := sourceClient.SSOSharedWith
|
||||
|
||||
// If client has no explicit ssoSharedWith, use default from session config.
|
||||
if ssoSharedWith == nil {
|
||||
switch s.sessionConfig.SSOSharedWithDefault {
|
||||
case "all":
|
||||
return true
|
||||
default: // "none" or ""
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit empty slice means share with no one.
|
||||
if len(ssoSharedWith) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, peer := range ssoSharedWith {
|
||||
if peer == "*" || peer == targetClientID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findSSOSession checks whether any active client in the session shares its
|
||||
// authentication with targetClientID via the ssoSharedWith policy.
|
||||
//
|
||||
// Note: the caller already has the target client loaded (for AllowedConnectors
|
||||
// validation), but here we need the *source* client configs - those are the
|
||||
// clients the user previously authenticated for, and their ssoSharedWith
|
||||
// policies determine whether SSO is allowed. These are different clients,
|
||||
// so the GetClient calls below are not redundant.
|
||||
func (s *Server) findSSOSession(ctx context.Context, session *storage.AuthSession, targetClientID string) *storage.ClientAuthState {
|
||||
now := s.now()
|
||||
|
||||
for sourceClientID, state := range session.ClientStates {
|
||||
if !state.Active || now.After(state.ExpiresAt) {
|
||||
continue
|
||||
}
|
||||
|
||||
sourceClient, err := s.storage.GetClient(ctx, sourceClientID)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "session: SSO lookup failed to get source client",
|
||||
"source_client_id", sourceClientID, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if s.clientSharesSessionWith(sourceClient, targetClientID) {
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// trySessionLoginWithSession is like trySessionLogin but accepts a pre-retrieved session.
|
||||
// This allows callers to inspect the session (e.g., for id_token_hint comparison) before
|
||||
// attempting session-based login.
|
||||
@@ -314,17 +373,47 @@ func (s *Server) trySessionLoginWithSession(ctx context.Context, r *http.Request
|
||||
return "", false
|
||||
}
|
||||
|
||||
clientState, ok := session.ClientStates[authReq.ClientID]
|
||||
if !ok || !clientState.Active {
|
||||
return "", false
|
||||
}
|
||||
|
||||
now := s.now()
|
||||
if now.After(clientState.ExpiresAt) {
|
||||
return "", false
|
||||
|
||||
clientState, ok := session.ClientStates[authReq.ClientID]
|
||||
fallbackToSSO := !ok || !clientState.Active || now.After(clientState.ExpiresAt)
|
||||
|
||||
if fallbackToSSO {
|
||||
// No direct session for this client — try SSO from a sharing client.
|
||||
sourceState := s.findSSOSession(ctx, session, authReq.ClientID)
|
||||
if sourceState == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// Cap the derived state expiry at min(configured lifetime, source state expiry).
|
||||
expiresAt := now.Add(s.sessionConfig.AbsoluteLifetime)
|
||||
if sourceState.ExpiresAt.Before(expiresAt) {
|
||||
expiresAt = sourceState.ExpiresAt
|
||||
}
|
||||
|
||||
// Create a new client state for the target client via SSO.
|
||||
if err := s.storage.UpdateAuthSession(ctx, session.UserID, session.ConnectorID, func(old storage.AuthSession) (storage.AuthSession, error) {
|
||||
if old.ClientStates == nil {
|
||||
old.ClientStates = make(map[string]*storage.ClientAuthState)
|
||||
}
|
||||
old.ClientStates[authReq.ClientID] = &storage.ClientAuthState{
|
||||
Active: true,
|
||||
ExpiresAt: expiresAt,
|
||||
LastActivity: now,
|
||||
}
|
||||
old.LastActivity = now
|
||||
old.IdleExpiry = now.Add(s.sessionConfig.ValidIfNotUsedFor)
|
||||
return old, nil
|
||||
}); err != nil {
|
||||
s.logger.ErrorContext(ctx, "session: failed to create SSO client state", "err", err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
s.logger.DebugContext(ctx, "session: SSO login from sharing client",
|
||||
"user_id", session.UserID, "connector_id", session.ConnectorID, "client_id", authReq.ClientID)
|
||||
}
|
||||
|
||||
// Load identity from storage.
|
||||
// Load identity from storage (same path for direct and SSO login).
|
||||
ui, err := s.storage.GetUserIdentity(ctx, session.UserID, session.ConnectorID)
|
||||
if err != nil {
|
||||
s.logger.ErrorContext(ctx, "session: failed to get user identity", "err", err)
|
||||
@@ -338,6 +427,17 @@ func (s *Server) trySessionLoginWithSession(ctx context.Context, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
if !fallbackToSSO {
|
||||
s.logger.DebugContext(ctx, "session: re-authenticated from session",
|
||||
"user_id", session.UserID, "connector_id", session.ConnectorID)
|
||||
}
|
||||
|
||||
return s.finishSessionLogin(ctx, r, w, authReq, session, &ui, now)
|
||||
}
|
||||
|
||||
// finishSessionLogin completes a session-based login (direct or SSO) by updating the auth request
|
||||
// with the user's identity, refreshing session activity, and returning the appropriate redirect URL.
|
||||
func (s *Server) finishSessionLogin(ctx context.Context, r *http.Request, w http.ResponseWriter, authReq *storage.AuthRequest, session *storage.AuthSession, ui *storage.UserIdentity, now time.Time) (string, bool) {
|
||||
claims := storage.Claims{
|
||||
UserID: ui.Claims.UserID,
|
||||
Username: ui.Claims.Username,
|
||||
@@ -359,9 +459,6 @@ func (s *Server) trySessionLoginWithSession(ctx context.Context, r *http.Request
|
||||
return "", false
|
||||
}
|
||||
|
||||
s.logger.DebugContext(ctx, "session: re-authenticated from session",
|
||||
"user_id", session.UserID, "connector_id", session.ConnectorID)
|
||||
|
||||
// Update session activity.
|
||||
_ = s.storage.UpdateAuthSession(ctx, session.UserID, session.ConnectorID, func(old storage.AuthSession) (storage.AuthSession, error) {
|
||||
old.LastActivity = now
|
||||
|
||||
@@ -869,3 +869,489 @@ func TestParseAuthRequest_PromptAndMaxAge(t *testing.T) {
|
||||
assert.Equal(t, -1, authReq.MaxAge)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClientSharesSessionWith(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ssoSharedWith []string
|
||||
defaultPolicy string
|
||||
targetClientID string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil uses default none",
|
||||
ssoSharedWith: nil,
|
||||
defaultPolicy: "none",
|
||||
targetClientID: "client-b",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil uses default all",
|
||||
ssoSharedWith: nil,
|
||||
defaultPolicy: "all",
|
||||
targetClientID: "client-b",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nil with empty default",
|
||||
ssoSharedWith: nil,
|
||||
defaultPolicy: "",
|
||||
targetClientID: "client-b",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty slice means no sharing",
|
||||
ssoSharedWith: []string{},
|
||||
defaultPolicy: "all",
|
||||
targetClientID: "client-b",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wildcard shares with everyone",
|
||||
ssoSharedWith: []string{"*"},
|
||||
defaultPolicy: "none",
|
||||
targetClientID: "any-client",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "explicit match",
|
||||
ssoSharedWith: []string{"client-b", "client-c"},
|
||||
defaultPolicy: "none",
|
||||
targetClientID: "client-b",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match in list",
|
||||
ssoSharedWith: []string{"client-b", "client-c"},
|
||||
defaultPolicy: "none",
|
||||
targetClientID: "client-d",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
s.sessionConfig.SSOSharedWithDefault = tt.defaultPolicy
|
||||
|
||||
client := storage.Client{
|
||||
ID: "source-client",
|
||||
SSOSharedWith: tt.ssoSharedWith,
|
||||
}
|
||||
got := s.clientSharesSessionWith(client, tt.targetClientID)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindSSOSession(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("finds SSO session from sharing client", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"client-b"},
|
||||
}))
|
||||
|
||||
session := &storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(24 * time.Hour),
|
||||
LastActivity: now.Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotNil(t, s.findSSOSession(ctx, session, "client-b"))
|
||||
})
|
||||
|
||||
t.Run("no SSO when client does not share", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"client-c"}, // Does not share with client-b
|
||||
}))
|
||||
|
||||
session := &storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(24 * time.Hour),
|
||||
LastActivity: now.Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Nil(t, s.findSSOSession(ctx, session, "client-b"))
|
||||
})
|
||||
|
||||
t.Run("skips inactive client states", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"*"},
|
||||
}))
|
||||
|
||||
session := &storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: false, // Inactive
|
||||
ExpiresAt: now.Add(24 * time.Hour),
|
||||
LastActivity: now.Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Nil(t, s.findSSOSession(ctx, session, "client-b"))
|
||||
})
|
||||
|
||||
t.Run("skips expired client states", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"*"},
|
||||
}))
|
||||
|
||||
session := &storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(-1 * time.Hour), // Expired
|
||||
LastActivity: now.Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Nil(t, s.findSSOSession(ctx, session, "client-b"))
|
||||
})
|
||||
|
||||
t.Run("wildcard SSO with default all", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
s.sessionConfig.SSOSharedWithDefault = "all"
|
||||
now := s.now()
|
||||
|
||||
// Client with nil SSOSharedWith — uses default "all"
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
// SSOSharedWith is nil → uses ssoSharedWithDefault="all"
|
||||
}))
|
||||
|
||||
session := &storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(24 * time.Hour),
|
||||
LastActivity: now.Add(-5 * time.Minute),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NotNil(t, s.findSSOSession(ctx, session, "client-b"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTrySessionLogin_SSO(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
t.Run("SSO login from sharing client", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
s.skipApproval = true
|
||||
now := s.now()
|
||||
|
||||
// Create source client that shares with target
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"client-b"},
|
||||
}))
|
||||
|
||||
// Create session with client-a authenticated
|
||||
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Nonce: "test-nonce",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(24 * time.Hour),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
},
|
||||
},
|
||||
CreatedAt: now.Add(-30 * time.Minute),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
IPAddress: "127.0.0.1",
|
||||
UserAgent: "test",
|
||||
AbsoluteExpiry: now.Add(24 * time.Hour),
|
||||
IdleExpiry: now.Add(59 * time.Minute),
|
||||
}))
|
||||
|
||||
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Claims: storage.Claims{
|
||||
UserID: "user-1",
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
Consents: map[string][]string{"client-b": {"openid", "email"}},
|
||||
CreatedAt: now.Add(-1 * time.Hour),
|
||||
LastLogin: now.Add(-30 * time.Minute),
|
||||
}))
|
||||
|
||||
// Auth request for client-b (not directly in session)
|
||||
authReq := storage.AuthRequest{
|
||||
ID: storage.NewID(),
|
||||
ClientID: "client-b",
|
||||
ConnectorID: "mock",
|
||||
Scopes: []string{"openid", "email"},
|
||||
RedirectURI: "http://localhost/callback",
|
||||
MaxAge: -1,
|
||||
HMACKey: storage.NewHMACKey(crypto.SHA256),
|
||||
Expiry: now.Add(10 * time.Minute),
|
||||
}
|
||||
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq))
|
||||
|
||||
r := sessionCookieRequest("user-1", "mock", "test-nonce")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
session := s.getValidAuthSession(ctx, w, r, &authReq)
|
||||
require.NotNil(t, session)
|
||||
|
||||
_, ok := s.trySessionLoginWithSession(ctx, r, w, &authReq, session)
|
||||
assert.True(t, ok, "SSO login should succeed")
|
||||
|
||||
// Verify client-b state was created in session
|
||||
updated, err := s.storage.GetAuthSession(ctx, "user-1", "mock")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, updated.ClientStates, "client-b")
|
||||
assert.True(t, updated.ClientStates["client-b"].Active)
|
||||
})
|
||||
|
||||
t.Run("SSO derived state capped by source expiry", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
s.skipApproval = true
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"client-b"},
|
||||
}))
|
||||
|
||||
// Source state expires in 1 hour — less than AbsoluteLifetime (24h).
|
||||
sourceExpiry := now.Add(1 * time.Hour)
|
||||
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Nonce: "test-nonce",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: sourceExpiry,
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
},
|
||||
},
|
||||
CreatedAt: now.Add(-30 * time.Minute),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
IPAddress: "127.0.0.1",
|
||||
UserAgent: "test",
|
||||
AbsoluteExpiry: now.Add(24 * time.Hour),
|
||||
IdleExpiry: now.Add(59 * time.Minute),
|
||||
}))
|
||||
|
||||
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Claims: storage.Claims{
|
||||
UserID: "user-1",
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
Consents: map[string][]string{"client-b": {"openid", "email"}},
|
||||
CreatedAt: now.Add(-1 * time.Hour),
|
||||
LastLogin: now.Add(-30 * time.Minute),
|
||||
}))
|
||||
|
||||
authReq := storage.AuthRequest{
|
||||
ID: storage.NewID(),
|
||||
ClientID: "client-b",
|
||||
ConnectorID: "mock",
|
||||
Scopes: []string{"openid", "email"},
|
||||
RedirectURI: "http://localhost/callback",
|
||||
MaxAge: -1,
|
||||
HMACKey: storage.NewHMACKey(crypto.SHA256),
|
||||
Expiry: now.Add(10 * time.Minute),
|
||||
}
|
||||
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq))
|
||||
|
||||
r := sessionCookieRequest("user-1", "mock", "test-nonce")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
session := s.getValidAuthSession(ctx, w, r, &authReq)
|
||||
require.NotNil(t, session)
|
||||
|
||||
_, ok := s.trySessionLoginWithSession(ctx, r, w, &authReq, session)
|
||||
assert.True(t, ok, "SSO login should succeed")
|
||||
|
||||
updated, err := s.storage.GetAuthSession(ctx, "user-1", "mock")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, updated.ClientStates, "client-b")
|
||||
assert.Equal(t, sourceExpiry, updated.ClientStates["client-b"].ExpiresAt,
|
||||
"derived state expiry should be capped at source state expiry")
|
||||
})
|
||||
|
||||
t.Run("SSO derived state uses configured lifetime when source expires later", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
s.skipApproval = true
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{"client-b"},
|
||||
}))
|
||||
|
||||
// Source state expires in 48 hours — more than AbsoluteLifetime (24h).
|
||||
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Nonce: "test-nonce",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(48 * time.Hour),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
},
|
||||
},
|
||||
CreatedAt: now.Add(-30 * time.Minute),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
IPAddress: "127.0.0.1",
|
||||
UserAgent: "test",
|
||||
AbsoluteExpiry: now.Add(24 * time.Hour),
|
||||
IdleExpiry: now.Add(59 * time.Minute),
|
||||
}))
|
||||
|
||||
require.NoError(t, s.storage.CreateUserIdentity(ctx, storage.UserIdentity{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Claims: storage.Claims{
|
||||
UserID: "user-1",
|
||||
Username: "testuser",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
Consents: map[string][]string{"client-b": {"openid", "email"}},
|
||||
CreatedAt: now.Add(-1 * time.Hour),
|
||||
LastLogin: now.Add(-30 * time.Minute),
|
||||
}))
|
||||
|
||||
authReq := storage.AuthRequest{
|
||||
ID: storage.NewID(),
|
||||
ClientID: "client-b",
|
||||
ConnectorID: "mock",
|
||||
Scopes: []string{"openid", "email"},
|
||||
RedirectURI: "http://localhost/callback",
|
||||
MaxAge: -1,
|
||||
HMACKey: storage.NewHMACKey(crypto.SHA256),
|
||||
Expiry: now.Add(10 * time.Minute),
|
||||
}
|
||||
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq))
|
||||
|
||||
r := sessionCookieRequest("user-1", "mock", "test-nonce")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
session := s.getValidAuthSession(ctx, w, r, &authReq)
|
||||
require.NotNil(t, session)
|
||||
|
||||
_, ok := s.trySessionLoginWithSession(ctx, r, w, &authReq, session)
|
||||
assert.True(t, ok, "SSO login should succeed")
|
||||
|
||||
updated, err := s.storage.GetAuthSession(ctx, "user-1", "mock")
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, updated.ClientStates, "client-b")
|
||||
assert.Equal(t, now.Add(s.sessionConfig.AbsoluteLifetime), updated.ClientStates["client-b"].ExpiresAt,
|
||||
"derived state expiry should use configured AbsoluteLifetime when source expires later")
|
||||
})
|
||||
|
||||
t.Run("no SSO when client does not share", func(t *testing.T) {
|
||||
s := newTestSessionServer(t)
|
||||
now := s.now()
|
||||
|
||||
require.NoError(t, s.storage.CreateClient(ctx, storage.Client{
|
||||
ID: "client-a",
|
||||
Secret: "secret",
|
||||
Name: "Client A",
|
||||
SSOSharedWith: []string{}, // Shares with nobody
|
||||
}))
|
||||
|
||||
require.NoError(t, s.storage.CreateAuthSession(ctx, storage.AuthSession{
|
||||
UserID: "user-1",
|
||||
ConnectorID: "mock",
|
||||
Nonce: "test-nonce",
|
||||
ClientStates: map[string]*storage.ClientAuthState{
|
||||
"client-a": {
|
||||
Active: true,
|
||||
ExpiresAt: now.Add(24 * time.Hour),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
},
|
||||
},
|
||||
CreatedAt: now.Add(-30 * time.Minute),
|
||||
LastActivity: now.Add(-1 * time.Minute),
|
||||
IPAddress: "127.0.0.1",
|
||||
UserAgent: "test",
|
||||
AbsoluteExpiry: now.Add(24 * time.Hour),
|
||||
IdleExpiry: now.Add(59 * time.Minute),
|
||||
}))
|
||||
|
||||
authReq := storage.AuthRequest{
|
||||
ID: storage.NewID(),
|
||||
ClientID: "client-b",
|
||||
ConnectorID: "mock",
|
||||
MaxAge: -1,
|
||||
HMACKey: storage.NewHMACKey(crypto.SHA256),
|
||||
Expiry: now.Add(10 * time.Minute),
|
||||
}
|
||||
require.NoError(t, s.storage.CreateAuthRequest(ctx, authReq))
|
||||
|
||||
r := sessionCookieRequest("user-1", "mock", "test-nonce")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
session := s.getValidAuthSession(ctx, w, r, &authReq)
|
||||
require.NotNil(t, session)
|
||||
|
||||
_, ok := s.trySessionLoginWithSession(ctx, r, w, &authReq, session)
|
||||
assert.False(t, ok, "SSO login should fail when client does not share")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -369,6 +369,41 @@ func testClientCRUD(t *testing.T, s storage.Storage) {
|
||||
c1.Secret = newSecret
|
||||
getAndCompare(id1, c1)
|
||||
|
||||
// Verify SSOSharedWith nil vs empty slice roundtrip.
|
||||
err = s.UpdateClient(ctx, id1, func(old storage.Client) (storage.Client, error) {
|
||||
old.SSOSharedWith = []string{}
|
||||
return old, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update client ssoSharedWith to empty: %v", err)
|
||||
}
|
||||
gc, err := s.GetClient(ctx, id1)
|
||||
if err != nil {
|
||||
t.Fatalf("get client: %v", err)
|
||||
}
|
||||
if gc.SSOSharedWith == nil {
|
||||
t.Error("expected empty slice for SSOSharedWith, got nil")
|
||||
}
|
||||
if len(gc.SSOSharedWith) != 0 {
|
||||
t.Errorf("expected empty SSOSharedWith, got %v", gc.SSOSharedWith)
|
||||
}
|
||||
|
||||
// Verify nil SSOSharedWith stays nil after roundtrip.
|
||||
err = s.UpdateClient(ctx, id1, func(old storage.Client) (storage.Client, error) {
|
||||
old.SSOSharedWith = nil
|
||||
return old, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update client ssoSharedWith to nil: %v", err)
|
||||
}
|
||||
gc, err = s.GetClient(ctx, id1)
|
||||
if err != nil {
|
||||
t.Fatalf("get client: %v", err)
|
||||
}
|
||||
if gc.SSOSharedWith != nil {
|
||||
t.Errorf("expected nil SSOSharedWith, got %v", gc.SSOSharedWith)
|
||||
}
|
||||
|
||||
if err := s.DeleteClient(ctx, id1); err != nil {
|
||||
t.Fatalf("delete client: %v", err)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ func (d *Database) CreateClient(ctx context.Context, client storage.Client) erro
|
||||
SetAllowedConnectors(client.AllowedConnectors).
|
||||
SetMfaChain(client.MFAChain).
|
||||
SetPostLogoutRedirectUris(client.PostLogoutRedirectURIs).
|
||||
SetSSOSharedWith(client.SSOSharedWith).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return convertDBError("create oauth2 client: %w", err)
|
||||
@@ -85,6 +86,7 @@ func (d *Database) UpdateClient(ctx context.Context, id string, updater func(old
|
||||
SetAllowedConnectors(newClient.AllowedConnectors).
|
||||
SetMfaChain(newClient.MFAChain).
|
||||
SetPostLogoutRedirectUris(newClient.PostLogoutRedirectURIs).
|
||||
SetSSOSharedWith(newClient.SSOSharedWith).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return rollback(tx, "update client uploading: %w", err)
|
||||
|
||||
@@ -97,6 +97,7 @@ func toStorageClient(c *db.OAuth2Client) storage.Client {
|
||||
AllowedConnectors: c.AllowedConnectors,
|
||||
MFAChain: c.MfaChain,
|
||||
PostLogoutRedirectURIs: c.PostLogoutRedirectUris,
|
||||
SSOSharedWith: c.SSOSharedWith,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ var (
|
||||
{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},
|
||||
{Name: "sso_shared_with", Type: field.TypeJSON, Nullable: true},
|
||||
}
|
||||
// Oauth2clientsTable holds the schema information for the "oauth2clients" table.
|
||||
Oauth2clientsTable = &schema.Table{
|
||||
|
||||
@@ -6470,6 +6470,8 @@ type OAuth2ClientMutation struct {
|
||||
appendmfa_chain []string
|
||||
post_logout_redirect_uris *[]string
|
||||
appendpost_logout_redirect_uris []string
|
||||
sso_shared_with *[]string
|
||||
appendsso_shared_with []string
|
||||
clearedFields map[string]struct{}
|
||||
done bool
|
||||
oldValue func(context.Context) (*OAuth2Client, error)
|
||||
@@ -7049,6 +7051,71 @@ func (m *OAuth2ClientMutation) ResetPostLogoutRedirectUris() {
|
||||
delete(m.clearedFields, oauth2client.FieldPostLogoutRedirectUris)
|
||||
}
|
||||
|
||||
// SetSSOSharedWith sets the "sso_shared_with" field.
|
||||
func (m *OAuth2ClientMutation) SetSSOSharedWith(s []string) {
|
||||
m.sso_shared_with = &s
|
||||
m.appendsso_shared_with = nil
|
||||
}
|
||||
|
||||
// SSOSharedWith returns the value of the "sso_shared_with" field in the mutation.
|
||||
func (m *OAuth2ClientMutation) SSOSharedWith() (r []string, exists bool) {
|
||||
v := m.sso_shared_with
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldSSOSharedWith returns the old "sso_shared_with" 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) OldSSOSharedWith(ctx context.Context) (v []string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldSSOSharedWith is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldSSOSharedWith requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldSSOSharedWith: %w", err)
|
||||
}
|
||||
return oldValue.SSOSharedWith, nil
|
||||
}
|
||||
|
||||
// AppendSSOSharedWith adds s to the "sso_shared_with" field.
|
||||
func (m *OAuth2ClientMutation) AppendSSOSharedWith(s []string) {
|
||||
m.appendsso_shared_with = append(m.appendsso_shared_with, s...)
|
||||
}
|
||||
|
||||
// AppendedSSOSharedWith returns the list of values that were appended to the "sso_shared_with" field in this mutation.
|
||||
func (m *OAuth2ClientMutation) AppendedSSOSharedWith() ([]string, bool) {
|
||||
if len(m.appendsso_shared_with) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return m.appendsso_shared_with, true
|
||||
}
|
||||
|
||||
// ClearSSOSharedWith clears the value of the "sso_shared_with" field.
|
||||
func (m *OAuth2ClientMutation) ClearSSOSharedWith() {
|
||||
m.sso_shared_with = nil
|
||||
m.appendsso_shared_with = nil
|
||||
m.clearedFields[oauth2client.FieldSSOSharedWith] = struct{}{}
|
||||
}
|
||||
|
||||
// SSOSharedWithCleared returns if the "sso_shared_with" field was cleared in this mutation.
|
||||
func (m *OAuth2ClientMutation) SSOSharedWithCleared() bool {
|
||||
_, ok := m.clearedFields[oauth2client.FieldSSOSharedWith]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetSSOSharedWith resets all changes to the "sso_shared_with" field.
|
||||
func (m *OAuth2ClientMutation) ResetSSOSharedWith() {
|
||||
m.sso_shared_with = nil
|
||||
m.appendsso_shared_with = nil
|
||||
delete(m.clearedFields, oauth2client.FieldSSOSharedWith)
|
||||
}
|
||||
|
||||
// Where appends a list predicates to the OAuth2ClientMutation builder.
|
||||
func (m *OAuth2ClientMutation) Where(ps ...predicate.OAuth2Client) {
|
||||
m.predicates = append(m.predicates, ps...)
|
||||
@@ -7083,7 +7150,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, 9)
|
||||
fields := make([]string, 0, 10)
|
||||
if m.secret != nil {
|
||||
fields = append(fields, oauth2client.FieldSecret)
|
||||
}
|
||||
@@ -7111,6 +7178,9 @@ func (m *OAuth2ClientMutation) Fields() []string {
|
||||
if m.post_logout_redirect_uris != nil {
|
||||
fields = append(fields, oauth2client.FieldPostLogoutRedirectUris)
|
||||
}
|
||||
if m.sso_shared_with != nil {
|
||||
fields = append(fields, oauth2client.FieldSSOSharedWith)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -7137,6 +7207,8 @@ func (m *OAuth2ClientMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.MfaChain()
|
||||
case oauth2client.FieldPostLogoutRedirectUris:
|
||||
return m.PostLogoutRedirectUris()
|
||||
case oauth2client.FieldSSOSharedWith:
|
||||
return m.SSOSharedWith()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -7164,6 +7236,8 @@ func (m *OAuth2ClientMutation) OldField(ctx context.Context, name string) (ent.V
|
||||
return m.OldMfaChain(ctx)
|
||||
case oauth2client.FieldPostLogoutRedirectUris:
|
||||
return m.OldPostLogoutRedirectUris(ctx)
|
||||
case oauth2client.FieldSSOSharedWith:
|
||||
return m.OldSSOSharedWith(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown OAuth2Client field %s", name)
|
||||
}
|
||||
@@ -7236,6 +7310,13 @@ func (m *OAuth2ClientMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetPostLogoutRedirectUris(v)
|
||||
return nil
|
||||
case oauth2client.FieldSSOSharedWith:
|
||||
v, ok := value.([]string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetSSOSharedWith(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown OAuth2Client field %s", name)
|
||||
}
|
||||
@@ -7281,6 +7362,9 @@ func (m *OAuth2ClientMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(oauth2client.FieldPostLogoutRedirectUris) {
|
||||
fields = append(fields, oauth2client.FieldPostLogoutRedirectUris)
|
||||
}
|
||||
if m.FieldCleared(oauth2client.FieldSSOSharedWith) {
|
||||
fields = append(fields, oauth2client.FieldSSOSharedWith)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -7310,6 +7394,9 @@ func (m *OAuth2ClientMutation) ClearField(name string) error {
|
||||
case oauth2client.FieldPostLogoutRedirectUris:
|
||||
m.ClearPostLogoutRedirectUris()
|
||||
return nil
|
||||
case oauth2client.FieldSSOSharedWith:
|
||||
m.ClearSSOSharedWith()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown OAuth2Client nullable field %s", name)
|
||||
}
|
||||
@@ -7345,6 +7432,9 @@ func (m *OAuth2ClientMutation) ResetField(name string) error {
|
||||
case oauth2client.FieldPostLogoutRedirectUris:
|
||||
m.ResetPostLogoutRedirectUris()
|
||||
return nil
|
||||
case oauth2client.FieldSSOSharedWith:
|
||||
m.ResetSSOSharedWith()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown OAuth2Client field %s", name)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ type OAuth2Client struct {
|
||||
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
|
||||
// SSOSharedWith holds the value of the "sso_shared_with" field.
|
||||
SSOSharedWith []string `json:"sso_shared_with,omitempty"`
|
||||
selectValues sql.SelectValues
|
||||
}
|
||||
|
||||
// scanValues returns the types for scanning values from sql.Rows.
|
||||
@@ -43,7 +45,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, oauth2client.FieldPostLogoutRedirectUris:
|
||||
case oauth2client.FieldRedirectUris, oauth2client.FieldTrustedPeers, oauth2client.FieldAllowedConnectors, oauth2client.FieldMfaChain, oauth2client.FieldPostLogoutRedirectUris, oauth2client.FieldSSOSharedWith:
|
||||
values[i] = new([]byte)
|
||||
case oauth2client.FieldPublic:
|
||||
values[i] = new(sql.NullBool)
|
||||
@@ -134,6 +136,14 @@ func (_m *OAuth2Client) assignValues(columns []string, values []any) error {
|
||||
return fmt.Errorf("unmarshal field post_logout_redirect_uris: %w", err)
|
||||
}
|
||||
}
|
||||
case oauth2client.FieldSSOSharedWith:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field sso_shared_with", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &_m.SSOSharedWith); err != nil {
|
||||
return fmt.Errorf("unmarshal field sso_shared_with: %w", err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
_m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -196,6 +206,9 @@ func (_m *OAuth2Client) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("post_logout_redirect_uris=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.PostLogoutRedirectUris))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("sso_shared_with=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.SSOSharedWith))
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
FieldMfaChain = "mfa_chain"
|
||||
// FieldPostLogoutRedirectUris holds the string denoting the post_logout_redirect_uris field in the database.
|
||||
FieldPostLogoutRedirectUris = "post_logout_redirect_uris"
|
||||
// FieldSSOSharedWith holds the string denoting the sso_shared_with field in the database.
|
||||
FieldSSOSharedWith = "sso_shared_with"
|
||||
// Table holds the table name of the oauth2client in the database.
|
||||
Table = "oauth2clients"
|
||||
)
|
||||
@@ -45,6 +47,7 @@ var Columns = []string{
|
||||
FieldAllowedConnectors,
|
||||
FieldMfaChain,
|
||||
FieldPostLogoutRedirectUris,
|
||||
FieldSSOSharedWith,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
|
||||
@@ -337,6 +337,16 @@ func PostLogoutRedirectUrisNotNil() predicate.OAuth2Client {
|
||||
return predicate.OAuth2Client(sql.FieldNotNull(FieldPostLogoutRedirectUris))
|
||||
}
|
||||
|
||||
// SSOSharedWithIsNil applies the IsNil predicate on the "sso_shared_with" field.
|
||||
func SSOSharedWithIsNil() predicate.OAuth2Client {
|
||||
return predicate.OAuth2Client(sql.FieldIsNull(FieldSSOSharedWith))
|
||||
}
|
||||
|
||||
// SSOSharedWithNotNil applies the NotNil predicate on the "sso_shared_with" field.
|
||||
func SSOSharedWithNotNil() predicate.OAuth2Client {
|
||||
return predicate.OAuth2Client(sql.FieldNotNull(FieldSSOSharedWith))
|
||||
}
|
||||
|
||||
// And groups predicates with the AND operator between them.
|
||||
func And(predicates ...predicate.OAuth2Client) predicate.OAuth2Client {
|
||||
return predicate.OAuth2Client(sql.AndPredicates(predicates...))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user