Skip to content

Commit 7850337

Browse files
authored
feat: refactor signer configuration with local and vault options (#4532)
Signed-off-by: maksim.nabokikh <max.nabokih@gmail.com>
1 parent d90827c commit 7850337

20 files changed

+583
-265
lines changed

cmd/dex/config.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/dexidp/dex/pkg/featureflags"
1616
"github.com/dexidp/dex/server"
17+
"github.com/dexidp/dex/server/signer"
1718
"github.com/dexidp/dex/storage"
1819
"github.com/dexidp/dex/storage/ent"
1920
"github.com/dexidp/dex/storage/etcd"
@@ -36,7 +37,7 @@ type Config struct {
3637
Frontend server.WebConfig `json:"frontend"`
3738

3839
// Signer configuration controls signing of JWT tokens issued by Dex.
39-
Signer server.SignerConfig `json:"signer"`
40+
Signer Signer `json:"signer"`
4041

4142
// StaticConnectors are user defined connectors specified in the ConfigMap
4243
// Write operations, like updating a connector, will fail.
@@ -373,6 +374,74 @@ func (s *Storage) UnmarshalJSON(b []byte) error {
373374
return nil
374375
}
375376

377+
// Signer holds app's signer configuration.
378+
type Signer struct {
379+
Type string `json:"type"`
380+
Config SignerConfig `json:"config"`
381+
}
382+
383+
// SignerConfig is a configuration that can create a signer.
384+
type SignerConfig interface{}
385+
386+
var (
387+
_ SignerConfig = (*signer.LocalConfig)(nil)
388+
_ SignerConfig = (*signer.VaultConfig)(nil)
389+
)
390+
391+
var signerConfigs = map[string]func() SignerConfig{
392+
"local": func() SignerConfig { return new(signer.LocalConfig) },
393+
"vault": func() SignerConfig { return new(signer.VaultConfig) },
394+
}
395+
396+
// UnmarshalJSON allows Signer to implement the unmarshaler interface to
397+
// dynamically determine the type of the signer config.
398+
func (s *Signer) UnmarshalJSON(b []byte) error {
399+
var signerData struct {
400+
Type string `json:"type"`
401+
Config json.RawMessage `json:"config"`
402+
}
403+
if err := json.Unmarshal(b, &signerData); err != nil {
404+
return fmt.Errorf("parse signer: %v", err)
405+
}
406+
407+
f, ok := signerConfigs[signerData.Type]
408+
if !ok {
409+
return fmt.Errorf("unknown signer type %q", signerData.Type)
410+
}
411+
412+
signerConfig := f()
413+
if len(signerData.Config) != 0 {
414+
data := []byte(signerData.Config)
415+
if featureflags.ExpandEnv.Enabled() {
416+
var rawMap map[string]interface{}
417+
if err := json.Unmarshal(signerData.Config, &rawMap); err != nil {
418+
return fmt.Errorf("unmarshal config for env expansion: %v", err)
419+
}
420+
421+
// Recursively expand environment variables in the map
422+
expandEnvInMap(rawMap)
423+
424+
// Marshal the expanded map back to JSON
425+
expandedData, err := json.Marshal(rawMap)
426+
if err != nil {
427+
return fmt.Errorf("marshal expanded config: %v", err)
428+
}
429+
430+
data = expandedData
431+
}
432+
433+
if err := json.Unmarshal(data, signerConfig); err != nil {
434+
return fmt.Errorf("parse signer config: %v", err)
435+
}
436+
}
437+
438+
*s = Signer{
439+
Type: signerData.Type,
440+
Config: signerConfig,
441+
}
442+
return nil
443+
}
444+
376445
// Connector is a magical type that can unmarshal YAML dynamically. The
377446
// Type field determines the connector type, which is then customized for Config.
378447
type Connector struct {

cmd/dex/config_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"encoding/json"
45
"log/slog"
56
"os"
67
"testing"
@@ -11,6 +12,7 @@ import (
1112
"github.com/dexidp/dex/connector/mock"
1213
"github.com/dexidp/dex/connector/oidc"
1314
"github.com/dexidp/dex/server"
15+
"github.com/dexidp/dex/server/signer"
1416
"github.com/dexidp/dex/storage"
1517
"github.com/dexidp/dex/storage/sql"
1618
)
@@ -469,3 +471,117 @@ logger:
469471
t.Errorf("got!=want: %s", diff)
470472
}
471473
}
474+
475+
func TestSignerConfigUnmarshal(t *testing.T) {
476+
tests := []struct {
477+
name string
478+
config string
479+
wantErr bool
480+
check func(*Config) error
481+
}{
482+
{
483+
name: "local signer with rotation period",
484+
config: `
485+
issuer: http://127.0.0.1:5556/dex
486+
storage:
487+
type: memory
488+
web:
489+
http: 0.0.0.0:5556
490+
signer:
491+
type: local
492+
config:
493+
keysRotationPeriod: 6h
494+
enablePasswordDB: true
495+
`,
496+
wantErr: false,
497+
check: func(c *Config) error {
498+
if c.Signer.Type != "local" {
499+
t.Errorf("expected signer type 'local', got %q", c.Signer.Type)
500+
}
501+
if localConfig, ok := c.Signer.Config.(*signer.LocalConfig); !ok {
502+
t.Error("expected LocalConfig")
503+
} else if localConfig.KeysRotationPeriod != "6h" {
504+
t.Errorf("expected keys rotation period '6h', got %q", localConfig.KeysRotationPeriod)
505+
}
506+
return nil
507+
},
508+
},
509+
{
510+
name: "vault signer",
511+
config: `
512+
issuer: http://127.0.0.1:5556/dex
513+
storage:
514+
type: memory
515+
web:
516+
http: 0.0.0.0:5556
517+
signer:
518+
type: vault
519+
config:
520+
addr: http://localhost:8200
521+
token: test-token
522+
keyName: test-key
523+
enablePasswordDB: true
524+
`,
525+
wantErr: false,
526+
check: func(c *Config) error {
527+
if c.Signer.Type != "vault" {
528+
t.Errorf("expected signer type 'vault', got %q", c.Signer.Type)
529+
}
530+
if vaultConfig, ok := c.Signer.Config.(*signer.VaultConfig); !ok {
531+
t.Error("expected VaultConfig")
532+
} else {
533+
if vaultConfig.Addr != "http://localhost:8200" {
534+
t.Errorf("expected addr 'http://localhost:8200', got %q", vaultConfig.Addr)
535+
}
536+
if vaultConfig.Token != "test-token" {
537+
t.Errorf("expected token 'test-token', got %q", vaultConfig.Token)
538+
}
539+
if vaultConfig.KeyName != "test-key" {
540+
t.Errorf("expected keyName 'test-key', got %q", vaultConfig.KeyName)
541+
}
542+
}
543+
return nil
544+
},
545+
},
546+
{
547+
name: "default to local when no signer specified",
548+
config: `
549+
issuer: http://127.0.0.1:5556/dex
550+
storage:
551+
type: memory
552+
web:
553+
http: 0.0.0.0:5556
554+
enablePasswordDB: true
555+
`,
556+
wantErr: false,
557+
check: func(c *Config) error {
558+
if c.Signer.Type != "" {
559+
t.Errorf("expected signer type '', got %q", c.Signer.Type)
560+
}
561+
return nil
562+
},
563+
},
564+
}
565+
566+
for _, tt := range tests {
567+
t.Run(tt.name, func(t *testing.T) {
568+
var c Config
569+
data, err := yaml.YAMLToJSON([]byte(tt.config))
570+
if err != nil {
571+
t.Fatalf("failed to convert yaml to json: %v", err)
572+
}
573+
574+
err = json.Unmarshal(data, &c)
575+
if (err != nil) != tt.wantErr {
576+
t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
577+
return
578+
}
579+
580+
if err == nil && tt.check != nil {
581+
if err := tt.check(&c); err != nil {
582+
t.Errorf("check failed: %v", err)
583+
}
584+
}
585+
})
586+
}
587+
}

cmd/dex/serve.go

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/dexidp/dex/api/v2"
3838
"github.com/dexidp/dex/pkg/featureflags"
3939
"github.com/dexidp/dex/server"
40+
"github.com/dexidp/dex/server/signer"
4041
"github.com/dexidp/dex/storage"
4142
)
4243

@@ -290,6 +291,65 @@ func runServe(options serveOptions) error {
290291

291292
healthChecker := gosundheit.New()
292293

294+
// Parse expiry durations
295+
idTokensValidFor := 24 * time.Hour // default
296+
if c.Expiry.IDTokens != "" {
297+
var err error
298+
idTokensValidFor, err = time.ParseDuration(c.Expiry.IDTokens)
299+
if err != nil {
300+
return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
301+
}
302+
logger.Info("config id tokens", "valid_for", idTokensValidFor)
303+
}
304+
305+
// Create signer
306+
var signerInstance signer.Signer
307+
switch c.Signer.Type {
308+
case "vault":
309+
vaultConfig, ok := c.Signer.Config.(*signer.VaultConfig)
310+
if !ok {
311+
return fmt.Errorf("invalid vault signer config")
312+
}
313+
signerInstance, err = vaultConfig.Open(context.Background())
314+
if err != nil {
315+
return fmt.Errorf("failed to open vault signer: %v", err)
316+
}
317+
logger.Info("signer configured", "type", "vault")
318+
case "local":
319+
localConfig, ok := c.Signer.Config.(*signer.LocalConfig)
320+
if !ok {
321+
return fmt.Errorf("invalid local signer config")
322+
}
323+
if localConfig.KeysRotationPeriod == "" {
324+
return fmt.Errorf("failed to open local signer: signer.config.keysRotationPeriod must be specified")
325+
}
326+
if c.Expiry.SigningKeys != "" {
327+
logger.Warn("both expiry.signingKeys and signer.config.keysRotationPeriod specified, using signer.config.keysRotationPeriod")
328+
}
329+
signerInstance, err = localConfig.Open(context.Background(), s, idTokensValidFor, now, logger)
330+
if err != nil {
331+
return fmt.Errorf("failed to open local signer: %v", err)
332+
}
333+
logger.Info("signer configured", "type", "local", "keys_rotation_period", localConfig.KeysRotationPeriod)
334+
case "": // Default to local signer
335+
// Handle deprecated expiry.signingKeys configuration
336+
if c.Expiry.SigningKeys != "" {
337+
logger.Warn("config expiry.signingKeys will be removed in a future release",
338+
"use_instead", "signer.config.keysRotationPeriod",
339+
"current_value", c.Expiry.SigningKeys, "deprecated", true)
340+
} else {
341+
c.Expiry.SigningKeys = "6h"
342+
}
343+
localConfig := signer.LocalConfig{KeysRotationPeriod: c.Expiry.SigningKeys}
344+
signerInstance, err = localConfig.Open(context.Background(), s, idTokensValidFor, now, logger)
345+
if err != nil {
346+
return fmt.Errorf("failed to open local signer: %v", err)
347+
}
348+
logger.Info("signer configured", "type", "local", "keys_rotation_period", localConfig.KeysRotationPeriod)
349+
default:
350+
return fmt.Errorf("unknown signer type %q", c.Signer.Type)
351+
}
352+
293353
serverConfig := server.Config{
294354
AllowedGrantTypes: c.OAuth2.GrantTypes,
295355
SupportedResponseTypes: c.OAuth2.ResponseTypes,
@@ -307,24 +367,10 @@ func runServe(options serveOptions) error {
307367
PrometheusRegistry: prometheusRegistry,
308368
HealthChecker: healthChecker,
309369
ContinueOnConnectorFailure: featureflags.ContinueOnConnectorFailure.Enabled(),
310-
Signer: c.Signer,
311-
}
312-
if c.Expiry.SigningKeys != "" {
313-
signingKeys, err := time.ParseDuration(c.Expiry.SigningKeys)
314-
if err != nil {
315-
return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err)
316-
}
317-
logger.Info("config signing keys", "expire_after", signingKeys)
318-
serverConfig.RotateKeysAfter = signingKeys
319-
}
320-
if c.Expiry.IDTokens != "" {
321-
idTokens, err := time.ParseDuration(c.Expiry.IDTokens)
322-
if err != nil {
323-
return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
324-
}
325-
logger.Info("config id tokens", "valid_for", idTokens)
326-
serverConfig.IDTokensValidFor = idTokens
370+
Signer: signerInstance,
371+
IDTokensValidFor: idTokensValidFor,
327372
}
373+
328374
if c.Expiry.AuthRequests != "" {
329375
authRequests, err := time.ParseDuration(c.Expiry.AuthRequests)
330376
if err != nil {

examples/config-dev.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,13 @@ staticPasswords:
184184
# Settings for signing JWT tokens. Available options:
185185
# - "local": use local keys (only RSA keys supported)
186186
# - "vault": use Vault Transit backend (RSA and EC keys supported)
187-
# signer:
187+
signer:
188+
type: local
189+
config:
190+
keysRotationPeriod: "6h"
191+
# signer
188192
# type: vault
189-
# vault:
193+
# config:
190194
# addr: http://127.0.0.1:8200
191195
# token: root
192196
# keyName: dex-key

0 commit comments

Comments
 (0)