Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion cmd/dex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type Config struct {

// StaticClients cause the server to use this list of clients rather than
// querying the storage. Write operations, like creating a client, will fail.
StaticClients []storage.Client `json:"staticClients"`
StaticClients []staticClient `json:"staticClients"`

// If enabled, the server will maintain a list of passwords which can be used
// to identify a user.
Expand Down Expand Up @@ -229,6 +229,18 @@ func (p *password) UnmarshalJSON(b []byte) error {
return nil
}

// staticClient wraps storage.Client with optional per-client ID-JAG policy.
type staticClient struct {
storage.Client
IDJAGPolicies *IDJAGClientPolicy `json:"idJAGPolicies,omitempty"`
}

// IDJAGClientPolicy configures allowed audiences and scopes for ID-JAG exchange.
type IDJAGClientPolicy struct {
AllowedAudiences []string `json:"allowedAudiences"`
AllowedScopes []string `json:"allowedScopes"`
}

// OAuth2 describes enabled OAuth2 extensions.
type OAuth2 struct {
// list of allowed grant types,
Expand All @@ -245,6 +257,8 @@ type OAuth2 struct {
PasswordConnector string `json:"passwordConnector"`
// PKCE configuration
PKCE PKCE `json:"pkce"`
// TokenExchange configures Token Exchange support.
TokenExchange server.TokenExchangeConfig `json:"tokenExchange"`
}

// PKCE holds the PKCE (Proof Key for Code Exchange) configuration.
Expand Down Expand Up @@ -641,6 +655,9 @@ type Expiry struct {
// IdTokens defines the duration of time for which the IdTokens will be valid.
IDTokens string `json:"idTokens"`

// IDJAGTokens defines the duration of time for which ID-JAG tokens will be valid.
IDJAGTokens string `json:"idJAGTokens"`

// AuthRequests defines the duration of time for which the AuthRequests will be valid.
AuthRequests string `json:"authRequests"`

Expand Down
108 changes: 102 additions & 6 deletions cmd/dex/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,15 @@ additionalFeatures: [
"foo": "bar",
},
},
StaticClients: []storage.Client{
{
StaticClients: []staticClient{
{Client: storage.Client{
ID: "example-app",
Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0",
Name: "Example App",
RedirectURIs: []string{
"http://127.0.0.1:5555/callback",
},
},
}},
},
OAuth2: OAuth2{
AlwaysShowLoginScreen: true,
Expand Down Expand Up @@ -413,15 +413,15 @@ logger:
"foo": "bar",
},
},
StaticClients: []storage.Client{
{
StaticClients: []staticClient{
{Client: storage.Client{
ID: "example-app",
Secret: "ZXhhbXBsZS1hcHAtc2VjcmV0",
Name: "Example App",
RedirectURIs: []string{
"http://127.0.0.1:5555/callback",
},
},
}},
},
OAuth2: OAuth2{
AlwaysShowLoginScreen: true,
Expand Down Expand Up @@ -675,3 +675,99 @@ enablePasswordDB: true
})
}
}

func TestUnmarshalConfigWithIDJAGPolicies(t *testing.T) {
rawConfig := []byte(`
issuer: http://127.0.0.1:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556

oauth2:
grantTypes:
- authorization_code
- "urn:ietf:params:oauth:grant-type:token-exchange"
tokenExchange:
tokenTypes:
- "urn:ietf:params:oauth:token-type:id_token"
- "urn:ietf:params:oauth:token-type:id-jag"

expiry:
idJAGTokens: "10m"

staticClients:
- id: wiki-app
secret: wiki-secret
name: "Wiki Application"
redirectURIs:
- "https://wiki.example/callback"
idJAGPolicies:
allowedAudiences:
- "https://chat.example/"
- "https://calendar.example/"
allowedScopes:
- "chat.read"
- "calendar.read"
- id: plain-app
secret: plain-secret
name: "Plain Application"
redirectURIs:
- "https://plain.example/callback"

enablePasswordDB: true
`)

var c Config
data, err := yaml.YAMLToJSON(rawConfig)
if err != nil {
t.Fatalf("failed to convert yaml to json: %v", err)
}
if err := json.Unmarshal(data, &c); err != nil {
t.Fatalf("failed to unmarshal config: %v", err)
}

// Verify tokenExchange config.
if len(c.OAuth2.TokenExchange.TokenTypes) != 2 {
t.Fatalf("expected 2 token types, got %d", len(c.OAuth2.TokenExchange.TokenTypes))
}
if !c.OAuth2.TokenExchange.IDJAGEnabled() {
t.Fatal("expected ID-JAG to be enabled")
}

// Verify expiry.
if c.Expiry.IDJAGTokens != "10m" {
t.Errorf("expected IDJAGTokens=10m, got %q", c.Expiry.IDJAGTokens)
}

// Verify static clients with idJAGPolicies.
if len(c.StaticClients) != 2 {
t.Fatalf("expected 2 static clients, got %d", len(c.StaticClients))
}

wikiClient := c.StaticClients[0]
if wikiClient.Client.ID != "wiki-app" {
t.Errorf("expected wiki-app, got %q", wikiClient.Client.ID)
}
if wikiClient.IDJAGPolicies == nil {
t.Fatal("expected idJAGPolicies for wiki-app, got nil")
}
if len(wikiClient.IDJAGPolicies.AllowedAudiences) != 2 {
t.Fatalf("expected 2 allowed audiences, got %d", len(wikiClient.IDJAGPolicies.AllowedAudiences))
}
if wikiClient.IDJAGPolicies.AllowedAudiences[0] != "https://chat.example/" {
t.Errorf("expected first audience https://chat.example/, got %q", wikiClient.IDJAGPolicies.AllowedAudiences[0])
}
if len(wikiClient.IDJAGPolicies.AllowedScopes) != 2 {
t.Fatalf("expected 2 allowed scopes, got %d", len(wikiClient.IDJAGPolicies.AllowedScopes))
}
if wikiClient.IDJAGPolicies.AllowedScopes[0] != "chat.read" {
t.Errorf("expected first scope chat.read, got %q", wikiClient.IDJAGPolicies.AllowedScopes[0])
}

// Client without idJAGPolicies.
plainClient := c.StaticClients[1]
if plainClient.IDJAGPolicies != nil {
t.Errorf("expected nil idJAGPolicies for plain-app, got %+v", plainClient.IDJAGPolicies)
}
}
36 changes: 32 additions & 4 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ func runServe(options serveOptions) error {
logger.Info("config storage", "storage_type", c.Storage.Type)

if len(c.StaticClients) > 0 {
for i, client := range c.StaticClients {
storageClients := make([]storage.Client, len(c.StaticClients))
for i, sc := range c.StaticClients {
client := sc.Client
if client.Name == "" {
return fmt.Errorf("invalid config: Name field is required for a client")
}
Expand All @@ -225,7 +227,7 @@ func runServe(options serveOptions) error {
if client.ID != "" {
return fmt.Errorf("invalid config: ID and IDEnv fields are exclusive for client %q", client.ID)
}
c.StaticClients[i].ID = os.Getenv(client.IDEnv)
client.ID = os.Getenv(client.IDEnv)
}
if client.Secret == "" && client.SecretEnv == "" && !client.Public {
return fmt.Errorf("invalid config: Secret or SecretEnv field is required for client %q", client.ID)
Expand All @@ -234,11 +236,12 @@ func runServe(options serveOptions) error {
if client.Secret != "" {
return fmt.Errorf("invalid config: Secret and SecretEnv fields are exclusive for client %q", client.ID)
}
c.StaticClients[i].Secret = os.Getenv(client.SecretEnv)
client.Secret = os.Getenv(client.SecretEnv)
}
logger.Info("config static client", "client_name", client.Name)
storageClients[i] = client
}
s = storage.WithStaticClients(s, c.StaticClients)
s = storage.WithStaticClients(s, storageClients)
}
if len(c.StaticPasswords) > 0 {
passwords := make([]storage.Password, len(c.StaticPasswords))
Expand Down Expand Up @@ -387,6 +390,7 @@ func runServe(options serveOptions) error {
IDTokensValidFor: idTokensValidFor,
MFAProviders: buildMFAProviders(c.MFA.Authenticators, c.Issuer, logger),
DefaultMFAChain: c.MFA.DefaultMFAChain,
TokenExchange: c.OAuth2.TokenExchange,
}

if c.Expiry.AuthRequests != "" {
Expand All @@ -405,6 +409,30 @@ func runServe(options serveOptions) error {
logger.Info("config device requests", "valid_for", deviceRequests)
serverConfig.DeviceRequestsValidFor = deviceRequests
}
if c.Expiry.IDJAGTokens != "" {
idJAGTokens, err := time.ParseDuration(c.Expiry.IDJAGTokens)
if err != nil {
return fmt.Errorf("invalid config value %q for ID-JAG token expiry: %v", c.Expiry.IDJAGTokens, err)
}
logger.Info("config ID-JAG tokens", "valid_for", idJAGTokens)
serverConfig.IDJAGTokensValidFor = idJAGTokens
}

// Build per-client ID-JAG policies from static client config.
for _, sc := range c.StaticClients {
if sc.IDJAGPolicies != nil {
clientID := sc.Client.ID
if clientID == "" && sc.Client.IDEnv != "" {
clientID = os.Getenv(sc.Client.IDEnv)
}
serverConfig.IDJAGPolicies = append(serverConfig.IDJAGPolicies, server.TokenExchangePolicy{
ClientID: clientID,
AllowedAudiences: sc.IDJAGPolicies.AllowedAudiences,
AllowedScopes: sc.IDJAGPolicies.AllowedScopes,
})
}
}

refreshTokenPolicy, err := server.NewRefreshTokenPolicy(
logger,
c.Expiry.RefreshTokens.DisableRotation,
Expand Down
24 changes: 24 additions & 0 deletions config.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ web:
# deviceRequests: "5m"
# signingKeys: "6h" # deprecated, use signer.config.keysRotationPeriod
# idTokens: "24h"
# idJAGTokens: "5m" # default: 5m; independent of idTokens
# refreshTokens:
# disableRotation: false
# reuseInterval: "3s"
Expand Down Expand Up @@ -138,6 +139,14 @@ web:
# enforce: false
# # Supported code challenge methods. Defaults to ["S256", "plain"].
# codeChallengeMethodsSupported: ["S256", "plain"]
#
# # Token Exchange configuration
# tokenExchange:
# # List of token types enabled for exchange. Adding id-jag enables ID-JAG support.
# # Omitting it (default) disables ID-JAG without affecting other token exchange flows.
# tokenTypes:
# - urn:ietf:params:oauth:token-type:id_token
# - urn:ietf:params:oauth:token-type:id-jag

# Static clients registered in Dex by default.
#
Expand Down Expand Up @@ -186,6 +195,21 @@ web:
# ssoSharedWith:
# - "dashboard-app"
# - "admin-app"
#
# # Example of a client with ID-JAG token exchange policy
# - id: wiki-app
# secret: wiki-secret
# redirectURIs:
# - 'https://wiki.example/callback'
# name: 'Wiki Application'
# # Per-client ID-JAG policy. Clients without this section cannot obtain ID-JAG tokens.
# idJAGPolicies:
# allowedAudiences:
# - "https://chat.example/"
# - "https://calendar.example/"
# allowedScopes:
# - "chat.read"
# - "calendar.read"

# Connectors are used to authenticate users against upstream identity providers.
#
Expand Down
Loading
Loading