Skip to content

Commit 098ab60

Browse files
feat: support ES256 local signer (#4682)
Signed-off-by: Ilia Andreev <ilia.andreev@palark.com> Co-authored-by: Ilia Andreev <ilia.andreev@palark.com>
1 parent 08dc8ee commit 098ab60

File tree

17 files changed

+1021
-227
lines changed

17 files changed

+1021
-227
lines changed

cmd/dex/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os"
1212
"strings"
1313

14+
"github.com/go-jose/go-jose/v4"
1415
"golang.org/x/crypto/bcrypt"
1516

1617
"github.com/dexidp/dex/pkg/featureflags"
@@ -525,6 +526,11 @@ func (s *Signer) UnmarshalJSON(b []byte) error {
525526
return fmt.Errorf("parse signer config: %v", err)
526527
}
527528
}
529+
if localConfig, ok := signerConfig.(*signer.LocalConfig); ok {
530+
if err := normalizeLocalSignerConfig(localConfig); err != nil {
531+
return fmt.Errorf("parse signer config: %v", err)
532+
}
533+
}
528534

529535
*s = Signer{
530536
Type: signerData.Type,
@@ -533,6 +539,17 @@ func (s *Signer) UnmarshalJSON(b []byte) error {
533539
return nil
534540
}
535541

542+
func normalizeLocalSignerConfig(c *signer.LocalConfig) error {
543+
if c.Algorithm == "" {
544+
c.Algorithm = jose.RS256
545+
return nil
546+
}
547+
if c.Algorithm == jose.RS256 || c.Algorithm == jose.ES256 {
548+
return nil
549+
}
550+
return fmt.Errorf("unsupported local signer algorithm %q", c.Algorithm)
551+
}
552+
536553
// Connector is a magical type that can unmarshal YAML dynamically. The
537554
// Type field determines the connector type, which is then customized for Config.
538555
type Connector struct {

cmd/dex/config_test.go

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"encoding/json"
55
"log/slog"
66
"os"
7+
"strings"
78
"testing"
89

910
"github.com/ghodss/yaml"
11+
"github.com/go-jose/go-jose/v4"
1012
"github.com/kylelemons/godebug/pretty"
1113

1214
"github.com/dexidp/dex/connector/mock"
@@ -481,10 +483,11 @@ logger:
481483

482484
func TestSignerConfigUnmarshal(t *testing.T) {
483485
tests := []struct {
484-
name string
485-
config string
486-
wantErr bool
487-
check func(*Config) error
486+
name string
487+
config string
488+
wantErr bool
489+
errContains string
490+
check func(*Config) error
488491
}{
489492
{
490493
name: "local signer with rotation period",
@@ -507,8 +510,84 @@ enablePasswordDB: true
507510
}
508511
if localConfig, ok := c.Signer.Config.(*signer.LocalConfig); !ok {
509512
t.Error("expected LocalConfig")
510-
} else if localConfig.KeysRotationPeriod != "6h" {
511-
t.Errorf("expected keys rotation period '6h', got %q", localConfig.KeysRotationPeriod)
513+
} else {
514+
if localConfig.KeysRotationPeriod != "6h" {
515+
t.Errorf("expected keys rotation period '6h', got %q", localConfig.KeysRotationPeriod)
516+
}
517+
if localConfig.Algorithm != jose.RS256 {
518+
t.Errorf("expected default algorithm 'RS256', got %q", localConfig.Algorithm)
519+
}
520+
}
521+
return nil
522+
},
523+
},
524+
{
525+
name: "local signer with ES256 algorithm",
526+
config: `
527+
issuer: http://127.0.0.1:5556/dex
528+
storage:
529+
type: memory
530+
web:
531+
http: 0.0.0.0:5556
532+
signer:
533+
type: local
534+
config:
535+
keysRotationPeriod: 6h
536+
algorithm: ES256
537+
enablePasswordDB: true
538+
`,
539+
wantErr: false,
540+
check: func(c *Config) error {
541+
localConfig, ok := c.Signer.Config.(*signer.LocalConfig)
542+
if !ok {
543+
t.Error("expected LocalConfig")
544+
return nil
545+
}
546+
if localConfig.Algorithm != jose.ES256 {
547+
t.Errorf("expected algorithm 'ES256', got %q", localConfig.Algorithm)
548+
}
549+
return nil
550+
},
551+
},
552+
{
553+
name: "local signer with invalid algorithm",
554+
config: `
555+
issuer: http://127.0.0.1:5556/dex
556+
storage:
557+
type: memory
558+
web:
559+
http: 0.0.0.0:5556
560+
signer:
561+
type: local
562+
config:
563+
keysRotationPeriod: 6h
564+
algorithm: ES512
565+
enablePasswordDB: true
566+
`,
567+
wantErr: true,
568+
errContains: `parse signer config: unsupported local signer algorithm "ES512"`,
569+
},
570+
{
571+
name: "local signer without config",
572+
config: `
573+
issuer: http://127.0.0.1:5556/dex
574+
storage:
575+
type: memory
576+
web:
577+
http: 0.0.0.0:5556
578+
signer:
579+
type: local
580+
enablePasswordDB: true
581+
`,
582+
wantErr: false,
583+
check: func(c *Config) error {
584+
localConfig, ok := c.Signer.Config.(*signer.LocalConfig)
585+
if !ok {
586+
t.Error("expected LocalConfig")
587+
return nil
588+
}
589+
if localConfig.Algorithm != jose.RS256 {
590+
t.Errorf("expected default algorithm 'RS256', got %q", localConfig.Algorithm)
512591
}
513592
return nil
514593
},
@@ -583,6 +662,10 @@ enablePasswordDB: true
583662
t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
584663
return
585664
}
665+
if tt.errContains != "" && (err == nil || !strings.Contains(err.Error(), tt.errContains)) {
666+
t.Errorf("Unmarshal() error = %v, want substring %q", err, tt.errContains)
667+
return
668+
}
586669

587670
if err == nil && tt.check != nil {
588671
if err := tt.check(&c); err != nil {

config.docker.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ telemetry:
2222

2323
expiry:
2424
deviceRequests: {{ getenv "DEX_EXPIRY_DEVICE_REQUESTS" "5m" }}
25-
signingKeys: {{ getenv "DEX_EXPIRY_SIGNING_KEYS" "6h" }}
2625
idTokens: {{ getenv "DEX_EXPIRY_ID_TOKENS" "24h" }}
2726
authRequests: {{ getenv "DEX_EXPIRY_AUTH_REQUESTS" "24h" }}
2827

28+
signer:
29+
type: local
30+
config:
31+
keysRotationPeriod: {{ getenv "DEX_EXPIRY_SIGNING_KEYS" "6h" }}
32+
algorithm: {{ getenv "DEX_SIGNER_LOCAL_ALGORITHM" "RS256" }}
33+
2934
logger:
3035
level: {{ getenv "DEX_LOG_LEVEL" "info" }}
3136
format: {{ getenv "DEX_LOG_FORMAT" "text" }}

config.yaml.dist

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,19 @@ web:
8787
# Expiration configuration for tokens, signing keys, etc.
8888
# expiry:
8989
# deviceRequests: "5m"
90-
# signingKeys: "6h"
90+
# signingKeys: "6h" # deprecated, use signer.config.keysRotationPeriod
9191
# idTokens: "24h"
9292
# refreshTokens:
9393
# disableRotation: false
9494
# reuseInterval: "3s"
9595
# validIfNotUsedFor: "2160h" # 90 days
9696
# absoluteLifetime: "3960h" # 165 days
97+
#
98+
# signer:
99+
# type: local
100+
# config:
101+
# keysRotationPeriod: "6h"
102+
# algorithm: "RS256" # supported values: "RS256" (default) and "ES256"; changes apply on the next key rotation
97103

98104
# OAuth2 configuration
99105
# oauth2:

examples/config-dev.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ telemetry:
8888
# Is possible to specify units using only s, m and h suffixes.
8989
# expiry:
9090
# deviceRequests: "5m"
91-
# signingKeys: "6h"
91+
# signingKeys: "6h" # deprecated, use signer.config.keysRotationPeriod
9292
# idTokens: "24h"
9393
# refreshTokens:
9494
# reuseInterval: "3s"
@@ -239,13 +239,14 @@ staticPasswords:
239239
- "team-a/admins"
240240
userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
241241

242-
# Settings for signing JWT tokens. Available options:
243-
# - "local": use local keys (only RSA keys supported)
244-
# - "vault": use Vault Transit backend (RSA and EC keys supported)
242+
# Configuration for signing JWT tokens.
243+
# - "local": use local keys (supports RS256 (default) and ES256)
244+
# - "vault": use Vault Transit backend (supports RSA, ECDSA, and Ed25519)
245245
signer:
246246
type: local
247247
config:
248248
keysRotationPeriod: "6h"
249+
algorithm: "RS256" # changes apply on the next key rotation
249250
# signer
250251
# type: vault
251252
# config:

server/handlers_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ import (
2020
gosundheit "github.com/AppsFlyer/go-sundheit"
2121
"github.com/AppsFlyer/go-sundheit/checks"
2222
"github.com/coreos/go-oidc/v3/oidc"
23+
"github.com/go-jose/go-jose/v4"
2324
"github.com/stretchr/testify/require"
2425
"golang.org/x/crypto/bcrypt"
2526
"golang.org/x/oauth2"
2627

2728
"github.com/dexidp/dex/connector"
2829
"github.com/dexidp/dex/server/internal"
30+
"github.com/dexidp/dex/server/signer"
2931
"github.com/dexidp/dex/storage"
3032
)
3133

@@ -112,6 +114,29 @@ func TestHandleDiscovery(t *testing.T) {
112114
}, res)
113115
}
114116

117+
func TestHandleDiscoveryWithES256LocalSigner(t *testing.T) {
118+
httpServer, server := newTestServer(t, func(c *Config) {
119+
localConfig := signer.LocalConfig{
120+
KeysRotationPeriod: time.Hour.String(),
121+
Algorithm: jose.ES256,
122+
}
123+
124+
sig, err := localConfig.Open(context.Background(), c.Storage, time.Hour, time.Now, c.Logger)
125+
require.NoError(t, err)
126+
c.Signer = sig
127+
})
128+
defer httpServer.Close()
129+
130+
rr := httptest.NewRecorder()
131+
server.ServeHTTP(rr, httptest.NewRequest("GET", "/.well-known/openid-configuration", nil))
132+
require.Equal(t, http.StatusOK, rr.Code)
133+
134+
var res discovery
135+
err := json.NewDecoder(rr.Result().Body).Decode(&res)
136+
require.NoError(t, err)
137+
require.Equal(t, []string{string(jose.ES256)}, res.IDTokenAlgs)
138+
}
139+
115140
func TestHandleHealthFailure(t *testing.T) {
116141
httpServer, server := newTestServer(t, func(c *Config) {
117142
c.HealthChecker = gosundheit.New()

0 commit comments

Comments
 (0)