Skip to content

Commit 3bf25fd

Browse files
authored
feat: add SSO sharing policy (#4705)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent 546e66c commit 3bf25fd

File tree

27 files changed

+1234
-286
lines changed

27 files changed

+1234
-286
lines changed

api/v2/api.pb.go

Lines changed: 295 additions & 264 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v2/api.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ message Client {
1515
string name = 6;
1616
string logo_url = 7;
1717
repeated string allowed_connectors = 8;
18+
repeated string sso_shared_with = 9;
1819
}
1920

2021
// ClientInfo represents an OAuth2 client without sensitive information.
@@ -26,6 +27,7 @@ message ClientInfo {
2627
string name = 5;
2728
string logo_url = 6;
2829
repeated string allowed_connectors = 7;
30+
repeated string sso_shared_with = 8;
2931
}
3032

3133
// GetClientReq is a request to retrieve client details.
@@ -69,6 +71,7 @@ message UpdateClientReq {
6971
string name = 4;
7072
string logo_url = 5;
7173
repeated string allowed_connectors = 6;
74+
repeated string sso_shared_with = 7;
7275
}
7376

7477
// UpdateClientResp returns the response from updating a client.

cmd/dex/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,9 @@ type Sessions struct {
687687
// Must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256.
688688
// If empty, cookies are not encrypted.
689689
CookieEncryptionKey string `json:"cookieEncryptionKey"`
690+
// SSOSharedWithDefault is the default SSO sharing policy for clients without explicit ssoSharedWith.
691+
// "all" = share with all clients, "none" = share with no one (default: "none").
692+
SSOSharedWithDefault string `json:"ssoSharedWithDefault"`
690693
}
691694

692695
// MFAAuthenticator defines a multi-factor authentication provider.

cmd/dex/serve.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,9 @@ func parseSessionConfig(s *Sessions) (*server.SessionConfig, error) {
807807
if s.CookieEncryptionKey != "" {
808808
sc.CookieEncryptionKey = []byte(s.CookieEncryptionKey)
809809
}
810+
if s.SSOSharedWithDefault != "" {
811+
sc.SSOSharedWithDefault = s.SSOSharedWithDefault
812+
}
810813
}
811814
if sc.AbsoluteLifetime <= 0 {
812815
return nil, fmt.Errorf("absoluteLifetime must be positive, got %v", sc.AbsoluteLifetime)
@@ -820,6 +823,12 @@ func parseSessionConfig(s *Sessions) (*server.SessionConfig, error) {
820823
if k := len(sc.CookieEncryptionKey); k > 0 && k != 16 && k != 24 && k != 32 {
821824
return nil, fmt.Errorf("cookieEncryptionKey must be 16, 24, or 32 bytes (AES-128/192/256), got %d", k)
822825
}
826+
switch sc.SSOSharedWithDefault {
827+
case "", "none", "all":
828+
// valid
829+
default:
830+
return nil, fmt.Errorf("ssoSharedWithDefault must be \"none\" or \"all\", got %q", sc.SSOSharedWithDefault)
831+
}
823832
return sc, nil
824833
}
825834

config.yaml.dist

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ web:
101101
# keysRotationPeriod: "6h"
102102
# algorithm: "RS256" # supported values: "RS256" (default) and "ES256"; changes apply on the next key rotation
103103

104+
# Authentication sessions configuration.
105+
# Requires DEX_SESSIONS_ENABLED=true feature flag.
106+
# sessions:
107+
# cookieName: "dex_session"
108+
# absoluteLifetime: "24h"
109+
# validIfNotUsedFor: "1h"
110+
# rememberMeCheckedByDefault: false
111+
# # AES key for encrypting session cookies. Must be 16, 24, or 32 bytes.
112+
# # If empty, cookies are not encrypted.
113+
# cookieEncryptionKey: ""
114+
# # Default SSO sharing policy for clients without explicit ssoSharedWith.
115+
# # "all" = share with all clients (Keycloak-like), "none" = no sharing (default).
116+
# ssoSharedWithDefault: "none"
117+
104118
# OAuth2 configuration
105119
# oauth2:
106120
# # use ["code", "token", "id_token"] to enable implicit flow for web-only clients
@@ -159,6 +173,19 @@ web:
159173
# allowedConnectors:
160174
# - github
161175
# - google
176+
#
177+
# # Example of SSO sharing between clients.
178+
# # ssoSharedWith defines which other clients can reuse this client's session.
179+
# # ["*"] = share with all, [] = share with no one.
180+
# # If omitted, ssoSharedWithDefault from sessions config is used.
181+
# - id: portal-app
182+
# secret: portal-secret
183+
# redirectURIs:
184+
# - 'https://portal.example.com/callback'
185+
# name: 'Portal'
186+
# ssoSharedWith:
187+
# - "dashboard-app"
188+
# - "admin-app"
162189

163190
# Connectors are used to authenticate users against upstream identity providers.
164191
#

examples/config-dev.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ telemetry:
102102
# absoluteLifetime: "24h"
103103
# validIfNotUsedFor: "1h"
104104
# rememberMeCheckedByDefault: false
105+
# # Default SSO sharing policy for clients without explicit ssoSharedWith.
106+
# # "all" = share with all clients (Keycloak-like), "none" = no sharing (default).
107+
# ssoSharedWithDefault: "none"
105108

106109
# Options for controlling the logger.
107110
# logger:
@@ -187,6 +190,11 @@ staticClients:
187190
# If omitted, mfa.defaultMFAChain is used.
188191
# mfaChain:
189192
# - totp-1
193+
# Optional: which other clients can reuse this client's authentication session (SSO).
194+
# ["*"] = share with all clients, [] = share with no one.
195+
# If omitted, ssoSharedWithDefault from sessions config is used.
196+
# ssoSharedWith:
197+
# - "*"
190198

191199
# Example using environment variables
192200
# Set DEX_CLIENT_ID and DEX_SECURE_CLIENT_SECRET before starting Dex

server/api.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func (d dexAPI) GetClient(ctx context.Context, req *api.GetClientReq) (*api.GetC
6666
Public: c.Public,
6767
LogoUrl: c.LogoURL,
6868
AllowedConnectors: c.AllowedConnectors,
69+
SsoSharedWith: c.SSOSharedWith,
6970
},
7071
}, nil
7172
}
@@ -91,6 +92,7 @@ func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*ap
9192
Name: req.Client.Name,
9293
LogoURL: req.Client.LogoUrl,
9394
AllowedConnectors: req.Client.AllowedConnectors,
95+
SSOSharedWith: req.Client.SsoSharedWith,
9496
}
9597
if err := d.s.CreateClient(ctx, c); err != nil {
9698
if err == storage.ErrAlreadyExists {
@@ -126,6 +128,9 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap
126128
if req.AllowedConnectors != nil {
127129
old.AllowedConnectors = req.AllowedConnectors
128130
}
131+
if req.SsoSharedWith != nil {
132+
old.SSOSharedWith = req.SsoSharedWith
133+
}
129134
return old, nil
130135
})
131136
if err != nil {
@@ -167,6 +172,7 @@ func (d dexAPI) ListClients(ctx context.Context, req *api.ListClientReq) (*api.L
167172
Public: client.Public,
168173
LogoUrl: client.LogoURL,
169174
AllowedConnectors: client.AllowedConnectors,
175+
SsoSharedWith: client.SSOSharedWith,
170176
}
171177
clients = append(clients, &c)
172178
}

server/handlers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
246246
default:
247247
panic("unsupported error type")
248248
}
249+
return
249250
}
250251
prompt, err := ParsePrompt(authReq.Prompt)
251252
if err != nil {

server/handlers_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,30 @@ func TestHandleAuthorizationConnectorGrantTypeFiltering(t *testing.T) {
16041604
}
16051605
}
16061606

1607+
func TestHandleAuthorizationInvalidRequestWithSessions(t *testing.T) {
1608+
ctx := t.Context()
1609+
httpServer, s := newTestServerMultipleConnectors(t, func(c *Config) {
1610+
c.SessionConfig = &SessionConfig{
1611+
CookieName: "dex_session",
1612+
AbsoluteLifetime: 24 * time.Hour,
1613+
ValidIfNotUsedFor: 1 * time.Hour,
1614+
}
1615+
c.Storage.CreateClient(ctx, storage.Client{
1616+
ID: "test",
1617+
RedirectURIs: []string{"http://example.com/callback"},
1618+
})
1619+
})
1620+
defer httpServer.Close()
1621+
1622+
// Send a request with an unregistered redirect_uri — should not panic.
1623+
rr := httptest.NewRecorder()
1624+
reqURL := fmt.Sprintf("%s/auth?response_type=code&client_id=test&redirect_uri=http://evil.com/callback&scope=openid", httpServer.URL)
1625+
req := httptest.NewRequest(http.MethodGet, reqURL, nil)
1626+
s.handleAuthorization(rr, req)
1627+
1628+
require.Equal(t, http.StatusBadRequest, rr.Code)
1629+
}
1630+
16071631
func TestHandleConnectorLoginGrantTypeRejection(t *testing.T) {
16081632
ctx := t.Context()
16091633
httpServer, s := newTestServer(t, func(c *Config) {

server/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ type SessionConfig struct {
156156
AbsoluteLifetime time.Duration
157157
ValidIfNotUsedFor time.Duration
158158
RememberMeCheckedByDefault bool
159+
// SSOSharedWithDefault is the default SSO sharing policy for clients without explicit SSOSharedWith.
160+
// "all" = share with all clients, "none" or "" = share with no one (default).
161+
SSOSharedWithDefault string
159162
}
160163

161164
// WebConfig holds the server's frontend templates and asset configuration.

0 commit comments

Comments
 (0)