diff --git a/docs/Configuring.md b/docs/Configuring.md index f5232857c4..cf5ece54d0 100644 --- a/docs/Configuring.md +++ b/docs/Configuring.md @@ -125,7 +125,8 @@ Root level key `server` | `auth.dpopskew` | The amount of time drift allowed between when the client generated a dpop proof and the server time. | `1h` | OPENTDF_SERVER_AUTH | | `auth.skew` | The amount of time drift allowed between a tokens `exp` claim and the server time. | `1m` | OPENTDF_SERVER_AUTH_SKEW | | `auth.public_client_id` | [DEPRECATED] The oidc client id. This is leveraged by otdfctl. | | OPENTDF_SERVER_AUTH_PUBLIC_CLIENT_ID | -| `auth.enforceDPoP` | If true, DPoP bindings on Access Tokens are enforced. | `false` | OPENTDF_SERVER_AUTH_ENFORCEDPOP | +| `auth.dpop.enforce` | If true, DPoP bindings on Access Tokens are enforced. | `false` | OPENTDF_SERVER_AUTH_DPOP_ENFORCE | +| `auth.enforceDPoP` | [DEPRECATED] Use `auth.dpop.enforce`. Still honored: DPoP is enforced when either field is true. | `false` | OPENTDF_SERVER_AUTH_ENFORCEDPOP | | `cryptoProvider` | A list of public/private keypairs and their use. Described [below](#crypto-provider) | empty | | | `enable_pprof` | Enable golang performance profiling | `false` | OPENTDF_SERVER_ENABLE_PPROF | | `grpc.reflection` | The configuration for the grpc server. | `true` | OPENTDF_SERVER_GRPC_REFLECTION | @@ -585,7 +586,8 @@ opentdf-example.yaml: server: auth: enabled: true - enforceDPoP: false + dpop: + enforce: false # public_client_id: 'opentdf-public' # DEPRECATED audience: 'http://localhost:8080' issuer: http://keycloak:8888/auth/realms/opentdf diff --git a/opentdf-core-mode.yaml b/opentdf-core-mode.yaml index 30b00e2033..534baf5f80 100644 --- a/opentdf-core-mode.yaml +++ b/opentdf-core-mode.yaml @@ -29,7 +29,8 @@ audit: server: auth: enabled: false - enforceDPoP: false + dpop: + enforce: false audience: 'http://localhost:8080' issuer: http://localhost:8888/auth/realms/tdf cors: diff --git a/opentdf-dev.yaml b/opentdf-dev.yaml index 0c03cfdc03..bfc5ada082 100644 --- a/opentdf-dev.yaml +++ b/opentdf-dev.yaml @@ -74,7 +74,8 @@ server: key: ./keys/platform-key.pem auth: enabled: true - enforceDPoP: false + dpop: + enforce: false audience: "http://localhost:8080" issuer: http://localhost:8888/auth/realms/opentdf policy: diff --git a/opentdf-ers-mode.yaml b/opentdf-ers-mode.yaml index 95186c759b..157023454d 100644 --- a/opentdf-ers-mode.yaml +++ b/opentdf-ers-mode.yaml @@ -28,7 +28,8 @@ services: server: auth: enabled: true - enforceDPoP: false + dpop: + enforce: false audience: "http://localhost:8080" issuer: http://localhost:8888/auth/realms/opentdf policy: diff --git a/opentdf-example.yaml b/opentdf-example.yaml index 1218a976cc..0ade73b819 100644 --- a/opentdf-example.yaml +++ b/opentdf-example.yaml @@ -53,7 +53,8 @@ services: server: auth: enabled: true - enforceDPoP: false + dpop: + enforce: false audience: "http://localhost:8080" issuer: http://keycloak:8888/auth/realms/opentdf policy: diff --git a/opentdf-kas-mode.yaml b/opentdf-kas-mode.yaml index bee0a5a1a1..dc5deafa37 100644 --- a/opentdf-kas-mode.yaml +++ b/opentdf-kas-mode.yaml @@ -55,7 +55,8 @@ server: key: ./keys/platform-key.pem auth: enabled: true - enforceDPoP: false + dpop: + enforce: false audience: "http://localhost:8080" issuer: http://localhost:8888/auth/realms/opentdf policy: diff --git a/service/internal/auth/authn.go b/service/internal/auth/authn.go index 860f92dc63..2a6fc87d89 100644 --- a/service/internal/auth/authn.go +++ b/service/internal/auth/authn.go @@ -209,7 +209,7 @@ func NewAuthenticator(ctx context.Context, cfg Config, logger *logger.Logger, we } a := &Authentication{ - enforceDPoP: cfg.EnforceDPoP, + enforceDPoP: cfg.dpopEnforced(), strictDPoPHTU: cfg.DPoP.StrictHTU, logger: logger, dpopNonceManager: newDPoPNonceManager(cfg.DPoP.RequireNonce, cfg.DPoP.NonceExpiration), diff --git a/service/internal/auth/authn_test.go b/service/internal/auth/authn_test.go index b17aff60b3..73aa942120 100644 --- a/service/internal/auth/authn_test.go +++ b/service/internal/auth/authn_test.go @@ -205,12 +205,12 @@ func (s *AuthSuite) SetupTest() { context.Background(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: true, - Issuer: s.server.URL, - Audience: "test", - DPoPSkew: time.Hour, - TokenSkew: time.Minute, - Policy: policyCfg, + Issuer: s.server.URL, + Audience: "test", + DPoPSkew: time.Hour, + TokenSkew: time.Minute, + Policy: policyCfg, + DPoP: DPoPConfig{Enforce: true}, }, PublicRoutes: []string{ "/public", @@ -496,10 +496,9 @@ func (s *AuthSuite) Test_MuxHandler_RoleProviderErrorReturnsInternalServerError( s.Require().NoError(defaults.Set(&policyCfg)) auth, err := NewAuthenticator(s.T().Context(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: false, - Issuer: s.server.URL, - Audience: "test", - Policy: policyCfg, + Issuer: s.server.URL, + Audience: "test", + Policy: policyCfg, }, RoleProvider: staticProvider{err: errors.New("role provider unavailable")}, }, logger.CreateTestLogger(), func(_ string, _ any) error { return nil }) @@ -532,10 +531,9 @@ func (s *AuthSuite) Test_MuxHandler_UsesAuthorizer() { s.Require().NoError(defaults.Set(&policyCfg)) auth, err := NewAuthenticator(s.T().Context(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: false, - Issuer: s.server.URL, - Audience: "test", - Policy: policyCfg, + Issuer: s.server.URL, + Audience: "test", + Policy: policyCfg, }, }, logger.CreateTestLogger(), func(_ string, _ any) error { return nil }) s.Require().NoError(err) @@ -575,10 +573,9 @@ func (s *AuthSuite) Test_MuxHandler_NilAuthorizerReturnsInternalServerError() { s.Require().NoError(defaults.Set(&policyCfg)) auth, err := NewAuthenticator(s.T().Context(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: false, - Issuer: s.server.URL, - Audience: "test", - Policy: policyCfg, + Issuer: s.server.URL, + Audience: "test", + Policy: policyCfg, }, }, logger.CreateTestLogger(), func(_ string, _ any) error { return nil }) s.Require().NoError(err) @@ -611,10 +608,9 @@ func (s *AuthSuite) Test_MuxHandler_NilAuthorizerDecisionReturnsInternalServerEr s.Require().NoError(defaults.Set(&policyCfg)) auth, err := NewAuthenticator(s.T().Context(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: false, - Issuer: s.server.URL, - Audience: "test", - Policy: policyCfg, + Issuer: s.server.URL, + Audience: "test", + Policy: policyCfg, }, }, logger.CreateTestLogger(), func(_ string, _ any) error { return nil }) s.Require().NoError(err) @@ -655,10 +651,9 @@ func (s *AuthSuite) Test_MuxHandler_V2WithoutResourceContextFailsClosedForDimens s.Require().NoError(defaults.Set(&policyCfg)) auth, err := NewAuthenticator(s.T().Context(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: false, - Issuer: s.server.URL, - Audience: "test", - Policy: policyCfg, + Issuer: s.server.URL, + Audience: "test", + Policy: policyCfg, }, }, logger.CreateTestLogger(), func(_ string, _ any) error { return nil }) s.Require().NoError(err) @@ -1379,9 +1374,8 @@ func makeDPoPToken(t *testing.T, tc dpopTestCase) string { func (s *AuthSuite) Test_Allowing_Auth_With_No_DPoP() { authnConfig := AuthNConfig{ - EnforceDPoP: false, - Issuer: s.server.URL, - Audience: "test", + Issuer: s.server.URL, + Audience: "test", } config := Config{} config.AuthNConfig = authnConfig diff --git a/service/internal/auth/config.go b/service/internal/auth/config.go index 5f9e15b3ac..1290a28613 100644 --- a/service/internal/auth/config.go +++ b/service/internal/auth/config.go @@ -24,6 +24,8 @@ type Config struct { // AuthNConfig is the configuration need for the platform to validate tokens type AuthNConfig struct { //nolint:revive // AuthNConfig is a valid name + // Deprecated: use DPoP.Enforce (server.auth.dpop.enforce) instead. Still honored + // during the migration window: DPoP is enforced when either field is true. EnforceDPoP bool `mapstructure:"enforceDPoP" json:"enforceDPoP" default:"false"` Issuer string `mapstructure:"issuer" json:"issuer"` Audience string `mapstructure:"audience" json:"audience"` @@ -34,7 +36,16 @@ type AuthNConfig struct { //nolint:revive // AuthNConfig is a valid name DPoP DPoPConfig `mapstructure:"dpop" json:"dpop"` } +// dpopEnforced reports whether DPoP-bound tokens are required. Enforcement is on +// when either the new DPoP.Enforce field or the deprecated EnforceDPoP field is set. +func (c AuthNConfig) dpopEnforced() bool { + return c.DPoP.Enforce || c.EnforceDPoP // honor deprecated EnforceDPoP during migration +} + type DPoPConfig struct { + // Enforce requires access tokens to be DPoP-bound. Replaces the deprecated + // top-level server.auth.enforceDPoP field. + Enforce bool `mapstructure:"enforce" json:"enforce" default:"false"` RequireNonce bool `mapstructure:"require_nonce" json:"require_nonce" default:"false"` NonceExpiration time.Duration `mapstructure:"nonce_expiration" json:"nonce_expiration" default:"5m"` // StrictHTU requires the htu claim in DPoP JWTs to include the origin @@ -59,8 +70,12 @@ func (c AuthNConfig) validateAuthNConfig(logger *logger.Logger) error { return errors.New("config Auth.Audience is required") } - if !c.EnforceDPoP { - logger.Warn("config Auth.EnforceDPoP is false. DPoP will not be enforced.") + if c.EnforceDPoP { + logger.Warn("config server.auth.enforceDPoP is deprecated; use server.auth.dpop.enforce instead") + } + + if !c.dpopEnforced() { + logger.Warn("DPoP is not enforced; set server.auth.dpop.enforce: true to require DPoP-bound tokens") } if err := c.DPoP.Validate(); err != nil { diff --git a/service/internal/auth/dpop_nonce_test.go b/service/internal/auth/dpop_nonce_test.go index 3a4329cdb6..39b6a5d3a6 100644 --- a/service/internal/auth/dpop_nonce_test.go +++ b/service/internal/auth/dpop_nonce_test.go @@ -129,6 +129,22 @@ func TestDPoPProofError(t *testing.T) { }) } +func TestDPoPEnforcement_Migration(t *testing.T) { + t.Run("new dpop.enforce field enables enforcement", func(t *testing.T) { + cfg := AuthNConfig{DPoP: DPoPConfig{Enforce: true}} + assert.True(t, cfg.dpopEnforced()) + }) + + t.Run("deprecated enforceDPoP field still enables enforcement", func(t *testing.T) { + cfg := AuthNConfig{EnforceDPoP: true} // verifying the deprecated field is still honored + assert.True(t, cfg.dpopEnforced()) + }) + + t.Run("neither set means not enforced", func(t *testing.T) { + assert.False(t, AuthNConfig{}.dpopEnforced()) + }) +} + func TestDPoPAlgorithmRestrictions(t *testing.T) { testCases := []struct { alg jwa.SignatureAlgorithm @@ -167,12 +183,12 @@ func (s *AuthSuite) newAuthDPoP(requireNonce bool) *Authentication { context.Background(), Config{ AuthNConfig: AuthNConfig{ - EnforceDPoP: true, - Issuer: s.server.URL, - Audience: "test", - DPoPSkew: time.Hour, - TokenSkew: time.Minute, + Issuer: s.server.URL, + Audience: "test", + DPoPSkew: time.Hour, + TokenSkew: time.Minute, DPoP: DPoPConfig{ + Enforce: true, RequireNonce: requireNonce, NonceExpiration: 5 * time.Minute, StrictHTU: false, diff --git a/service/internal/server/server.go b/service/internal/server/server.go index d5ce91d46a..955ce6ade4 100644 --- a/service/internal/server/server.go +++ b/service/internal/server/server.go @@ -276,7 +276,7 @@ func NewOpenTDFServer(config Config, logger *logger.Logger, cacheManager *cache. } logger.Debug("authentication interceptor enabled") } else { - logger.Warn("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `enforceDPoP = false`") + logger.Warn("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `server.auth.dpop.enforce = false`") } var ipcAuthInts []connect.Interceptor @@ -349,7 +349,7 @@ func newHTTPServer(c Config, connectRPC http.Handler, extraHTTP http.Handler, a if c.Auth.Enabled { httpHandler = a.MuxHandler(httpHandler) } else { - l.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `enforceDPoP = false`") + l.Error("disabling authentication. this is deprecated and will be removed. if you are using an IdP without DPoP set `server.auth.dpop.enforce = false`") } // CORS diff --git a/service/pkg/server/testdata/all-no-config.yaml b/service/pkg/server/testdata/all-no-config.yaml index 29dcd5fe8c..c946add3d8 100644 --- a/service/pkg/server/testdata/all-no-config.yaml +++ b/service/pkg/server/testdata/all-no-config.yaml @@ -40,7 +40,7 @@ server: key: ./keys/platform-key.pem auth: enabled: true - enforceDPoP: false + enforceDPoP: false # deprecated alias for dpop.enforce; kept here for backward-compat parse coverage audience: "http://localhost:8080" issuer: http://localhost:8888/auth/realms/opentdf policy: