Skip to content

Commit 9c95ae6

Browse files
feat(service/auth): move DPoP enforcement into dpop.enforce config (DSPX-3397)
Enforcement lived at the top of the auth block (server.auth.enforceDPoP) while every other DPoP knob is nested under server.auth.dpop. Consolidate it: add server.auth.dpop.enforce and deprecate the old top-level field. The old field keeps working during the migration window. Both are defaulted bools, so mapstructure cannot distinguish "explicitly false" from "unset"; enforcement therefore uses OR semantics via a new dpopEnforced() helper (DPoP.Enforce || EnforceDPoP), and config validation warns when the deprecated field is set. - config.go: add DPoPConfig.Enforce, deprecate AuthNConfig.EnforceDPoP, add dpopEnforced(); update validateAuthNConfig warnings. - authn.go: NewAuthenticator uses cfg.dpopEnforced(). - server.go: warning strings reference server.auth.dpop.enforce. - example configs + docs/Configuring.md: use the nested dpop.enforce form; keep testdata/all-no-config.yaml on the legacy key for back-compat coverage. - tests: migrate to DPoP.Enforce and add TestDPoPEnforcement_Migration. Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
1 parent d7caacd commit 9c95ae6

12 files changed

Lines changed: 79 additions & 47 deletions

File tree

docs/Configuring.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ Root level key `server`
125125
| `auth.dpopskew` | The amount of time drift allowed between when the client generated a dpop proof and the server time. | `1h` | OPENTDF_SERVER_AUTH |
126126
| `auth.skew` | The amount of time drift allowed between a tokens `exp` claim and the server time. | `1m` | OPENTDF_SERVER_AUTH_SKEW |
127127
| `auth.public_client_id` | [DEPRECATED] The oidc client id. This is leveraged by otdfctl. | | OPENTDF_SERVER_AUTH_PUBLIC_CLIENT_ID |
128-
| `auth.enforceDPoP` | If true, DPoP bindings on Access Tokens are enforced. | `false` | OPENTDF_SERVER_AUTH_ENFORCEDPOP |
128+
| `auth.dpop.enforce` | If true, DPoP bindings on Access Tokens are enforced. | `false` | OPENTDF_SERVER_AUTH_DPOP_ENFORCE |
129+
| `auth.enforceDPoP` | [DEPRECATED] Use `auth.dpop.enforce`. Still honored: DPoP is enforced when either field is true. | `false` | OPENTDF_SERVER_AUTH_ENFORCEDPOP |
129130
| `cryptoProvider` | A list of public/private keypairs and their use. Described [below](#crypto-provider) | empty | |
130131
| `enable_pprof` | Enable golang performance profiling | `false` | OPENTDF_SERVER_ENABLE_PPROF |
131132
| `grpc.reflection` | The configuration for the grpc server. | `true` | OPENTDF_SERVER_GRPC_REFLECTION |
@@ -585,7 +586,8 @@ opentdf-example.yaml:
585586
server:
586587
auth:
587588
enabled: true
588-
enforceDPoP: false
589+
dpop:
590+
enforce: false
589591
# public_client_id: 'opentdf-public' # DEPRECATED
590592
audience: 'http://localhost:8080'
591593
issuer: http://keycloak:8888/auth/realms/opentdf

opentdf-core-mode.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ audit:
2929
server:
3030
auth:
3131
enabled: false
32-
enforceDPoP: false
32+
dpop:
33+
enforce: false
3334
audience: 'http://localhost:8080'
3435
issuer: http://localhost:8888/auth/realms/tdf
3536
cors:

opentdf-dev.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ server:
7474
key: ./keys/platform-key.pem
7575
auth:
7676
enabled: true
77-
enforceDPoP: false
77+
dpop:
78+
enforce: false
7879
audience: "http://localhost:8080"
7980
issuer: http://localhost:8888/auth/realms/opentdf
8081
policy:

opentdf-ers-mode.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ services:
2828
server:
2929
auth:
3030
enabled: true
31-
enforceDPoP: false
31+
dpop:
32+
enforce: false
3233
audience: "http://localhost:8080"
3334
issuer: http://localhost:8888/auth/realms/opentdf
3435
policy:

opentdf-example.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ services:
5353
server:
5454
auth:
5555
enabled: true
56-
enforceDPoP: false
56+
dpop:
57+
enforce: false
5758
audience: "http://localhost:8080"
5859
issuer: http://keycloak:8888/auth/realms/opentdf
5960
policy:

opentdf-kas-mode.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ server:
5555
key: ./keys/platform-key.pem
5656
auth:
5757
enabled: true
58-
enforceDPoP: false
58+
dpop:
59+
enforce: false
5960
audience: "http://localhost:8080"
6061
issuer: http://localhost:8888/auth/realms/opentdf
6162
policy:

service/internal/auth/authn.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func NewAuthenticator(ctx context.Context, cfg Config, logger *logger.Logger, we
209209
}
210210

211211
a := &Authentication{
212-
enforceDPoP: cfg.EnforceDPoP,
212+
enforceDPoP: cfg.dpopEnforced(),
213213
strictDPoPHTU: cfg.DPoP.StrictHTU,
214214
logger: logger,
215215
dpopNonceManager: newDPoPNonceManager(cfg.DPoP.RequireNonce, cfg.DPoP.NonceExpiration),

service/internal/auth/authn_test.go

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -205,12 +205,12 @@ func (s *AuthSuite) SetupTest() {
205205
context.Background(),
206206
Config{
207207
AuthNConfig: AuthNConfig{
208-
EnforceDPoP: true,
209-
Issuer: s.server.URL,
210-
Audience: "test",
211-
DPoPSkew: time.Hour,
212-
TokenSkew: time.Minute,
213-
Policy: policyCfg,
208+
Issuer: s.server.URL,
209+
Audience: "test",
210+
DPoPSkew: time.Hour,
211+
TokenSkew: time.Minute,
212+
Policy: policyCfg,
213+
DPoP: DPoPConfig{Enforce: true},
214214
},
215215
PublicRoutes: []string{
216216
"/public",
@@ -496,10 +496,9 @@ func (s *AuthSuite) Test_MuxHandler_RoleProviderErrorReturnsInternalServerError(
496496
s.Require().NoError(defaults.Set(&policyCfg))
497497
auth, err := NewAuthenticator(s.T().Context(), Config{
498498
AuthNConfig: AuthNConfig{
499-
EnforceDPoP: false,
500-
Issuer: s.server.URL,
501-
Audience: "test",
502-
Policy: policyCfg,
499+
Issuer: s.server.URL,
500+
Audience: "test",
501+
Policy: policyCfg,
503502
},
504503
RoleProvider: staticProvider{err: errors.New("role provider unavailable")},
505504
}, logger.CreateTestLogger(), func(_ string, _ any) error { return nil })
@@ -532,10 +531,9 @@ func (s *AuthSuite) Test_MuxHandler_UsesAuthorizer() {
532531
s.Require().NoError(defaults.Set(&policyCfg))
533532
auth, err := NewAuthenticator(s.T().Context(), Config{
534533
AuthNConfig: AuthNConfig{
535-
EnforceDPoP: false,
536-
Issuer: s.server.URL,
537-
Audience: "test",
538-
Policy: policyCfg,
534+
Issuer: s.server.URL,
535+
Audience: "test",
536+
Policy: policyCfg,
539537
},
540538
}, logger.CreateTestLogger(), func(_ string, _ any) error { return nil })
541539
s.Require().NoError(err)
@@ -575,10 +573,9 @@ func (s *AuthSuite) Test_MuxHandler_NilAuthorizerReturnsInternalServerError() {
575573
s.Require().NoError(defaults.Set(&policyCfg))
576574
auth, err := NewAuthenticator(s.T().Context(), Config{
577575
AuthNConfig: AuthNConfig{
578-
EnforceDPoP: false,
579-
Issuer: s.server.URL,
580-
Audience: "test",
581-
Policy: policyCfg,
576+
Issuer: s.server.URL,
577+
Audience: "test",
578+
Policy: policyCfg,
582579
},
583580
}, logger.CreateTestLogger(), func(_ string, _ any) error { return nil })
584581
s.Require().NoError(err)
@@ -611,10 +608,9 @@ func (s *AuthSuite) Test_MuxHandler_NilAuthorizerDecisionReturnsInternalServerEr
611608
s.Require().NoError(defaults.Set(&policyCfg))
612609
auth, err := NewAuthenticator(s.T().Context(), Config{
613610
AuthNConfig: AuthNConfig{
614-
EnforceDPoP: false,
615-
Issuer: s.server.URL,
616-
Audience: "test",
617-
Policy: policyCfg,
611+
Issuer: s.server.URL,
612+
Audience: "test",
613+
Policy: policyCfg,
618614
},
619615
}, logger.CreateTestLogger(), func(_ string, _ any) error { return nil })
620616
s.Require().NoError(err)
@@ -655,10 +651,9 @@ func (s *AuthSuite) Test_MuxHandler_V2WithoutResourceContextFailsClosedForDimens
655651
s.Require().NoError(defaults.Set(&policyCfg))
656652
auth, err := NewAuthenticator(s.T().Context(), Config{
657653
AuthNConfig: AuthNConfig{
658-
EnforceDPoP: false,
659-
Issuer: s.server.URL,
660-
Audience: "test",
661-
Policy: policyCfg,
654+
Issuer: s.server.URL,
655+
Audience: "test",
656+
Policy: policyCfg,
662657
},
663658
}, logger.CreateTestLogger(), func(_ string, _ any) error { return nil })
664659
s.Require().NoError(err)
@@ -1379,9 +1374,8 @@ func makeDPoPToken(t *testing.T, tc dpopTestCase) string {
13791374

13801375
func (s *AuthSuite) Test_Allowing_Auth_With_No_DPoP() {
13811376
authnConfig := AuthNConfig{
1382-
EnforceDPoP: false,
1383-
Issuer: s.server.URL,
1384-
Audience: "test",
1377+
Issuer: s.server.URL,
1378+
Audience: "test",
13851379
}
13861380
config := Config{}
13871381
config.AuthNConfig = authnConfig

service/internal/auth/config.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type Config struct {
2424

2525
// AuthNConfig is the configuration need for the platform to validate tokens
2626
type AuthNConfig struct { //nolint:revive // AuthNConfig is a valid name
27+
// Deprecated: use DPoP.Enforce (server.auth.dpop.enforce) instead. Still honored
28+
// during the migration window: DPoP is enforced when either field is true.
2729
EnforceDPoP bool `mapstructure:"enforceDPoP" json:"enforceDPoP" default:"false"`
2830
Issuer string `mapstructure:"issuer" json:"issuer"`
2931
Audience string `mapstructure:"audience" json:"audience"`
@@ -34,7 +36,16 @@ type AuthNConfig struct { //nolint:revive // AuthNConfig is a valid name
3436
DPoP DPoPConfig `mapstructure:"dpop" json:"dpop"`
3537
}
3638

39+
// dpopEnforced reports whether DPoP-bound tokens are required. Enforcement is on
40+
// when either the new DPoP.Enforce field or the deprecated EnforceDPoP field is set.
41+
func (c AuthNConfig) dpopEnforced() bool {
42+
return c.DPoP.Enforce || c.EnforceDPoP // honor deprecated EnforceDPoP during migration
43+
}
44+
3745
type DPoPConfig struct {
46+
// Enforce requires access tokens to be DPoP-bound. Replaces the deprecated
47+
// top-level server.auth.enforceDPoP field.
48+
Enforce bool `mapstructure:"enforce" json:"enforce" default:"false"`
3849
RequireNonce bool `mapstructure:"require_nonce" json:"require_nonce" default:"false"`
3950
NonceExpiration time.Duration `mapstructure:"nonce_expiration" json:"nonce_expiration" default:"5m"`
4051
// StrictHTU requires the htu claim in DPoP JWTs to include the origin
@@ -59,8 +70,12 @@ func (c AuthNConfig) validateAuthNConfig(logger *logger.Logger) error {
5970
return errors.New("config Auth.Audience is required")
6071
}
6172

62-
if !c.EnforceDPoP {
63-
logger.Warn("config Auth.EnforceDPoP is false. DPoP will not be enforced.")
73+
if c.EnforceDPoP {
74+
logger.Warn("config server.auth.enforceDPoP is deprecated; use server.auth.dpop.enforce instead")
75+
}
76+
77+
if !c.dpopEnforced() {
78+
logger.Warn("DPoP is not enforced; set server.auth.dpop.enforce: true to require DPoP-bound tokens")
6479
}
6580

6681
if err := c.DPoP.Validate(); err != nil {

service/internal/auth/dpop_nonce_test.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ func TestDPoPProofError(t *testing.T) {
129129
})
130130
}
131131

132+
func TestDPoPEnforcement_Migration(t *testing.T) {
133+
t.Run("new dpop.enforce field enables enforcement", func(t *testing.T) {
134+
cfg := AuthNConfig{DPoP: DPoPConfig{Enforce: true}}
135+
assert.True(t, cfg.dpopEnforced())
136+
})
137+
138+
t.Run("deprecated enforceDPoP field still enables enforcement", func(t *testing.T) {
139+
cfg := AuthNConfig{EnforceDPoP: true} // verifying the deprecated field is still honored
140+
assert.True(t, cfg.dpopEnforced())
141+
})
142+
143+
t.Run("neither set means not enforced", func(t *testing.T) {
144+
assert.False(t, AuthNConfig{}.dpopEnforced())
145+
})
146+
}
147+
132148
func TestDPoPAlgorithmRestrictions(t *testing.T) {
133149
testCases := []struct {
134150
alg jwa.SignatureAlgorithm
@@ -167,12 +183,12 @@ func (s *AuthSuite) newAuthDPoP(requireNonce bool) *Authentication {
167183
context.Background(),
168184
Config{
169185
AuthNConfig: AuthNConfig{
170-
EnforceDPoP: true,
171-
Issuer: s.server.URL,
172-
Audience: "test",
173-
DPoPSkew: time.Hour,
174-
TokenSkew: time.Minute,
186+
Issuer: s.server.URL,
187+
Audience: "test",
188+
DPoPSkew: time.Hour,
189+
TokenSkew: time.Minute,
175190
DPoP: DPoPConfig{
191+
Enforce: true,
176192
RequireNonce: requireNonce,
177193
NonceExpiration: 5 * time.Minute,
178194
StrictHTU: false,

0 commit comments

Comments
 (0)