From 54efb2bd8fe9cf4859762110701b53a0cbacc7ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:03:53 +0000 Subject: [PATCH 1/7] Initial plan From 1cb2d2b16126f94ff894e002fd5c5f5f9ebf4982 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:18:54 +0000 Subject: [PATCH 2/7] Auth: Allow enabling/disabling OpenID4VP and OpenID4VCI protocols separately Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> Agent-Logs-Url: https://github.com/nuts-foundation/nuts-node/sessions/97962131-050a-4a34-9f6d-efe01eef57ac --- auth/api/auth/v1/api_test.go | 6 ++++- auth/api/iam/api.go | 6 ++--- auth/api/iam/api_test.go | 15 ++++++------ auth/api/iam/openid4vci.go | 3 +++ auth/api/iam/openid4vci_test.go | 7 ++++++ auth/auth.go | 13 +++++++--- auth/auth_test.go | 28 ++++++++++++++++++++++ auth/cmd/cmd.go | 12 +++++++++- auth/cmd/cmd_test.go | 2 ++ auth/config.go | 24 +++++++++++++++++-- auth/interface.go | 6 +++-- auth/mock.go | 42 ++++++++++++++++++++++----------- go.sum | 2 -- 13 files changed, 131 insertions(+), 35 deletions(-) diff --git a/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 3310ad2ba9..c6893eb300 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -70,7 +70,11 @@ type mockAuthClient struct { supportedDIDMethods []string } -func (m *mockAuthClient) AuthorizationEndpointEnabled() bool { +func (m *mockAuthClient) OpenID4VPEnabled() bool { + return true +} + +func (m *mockAuthClient) OpenID4VCIEnabled() bool { return true } diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 339d4b0086..b2c20d395f 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -252,7 +252,7 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ } func (r Wrapper) Callback(ctx context.Context, request CallbackRequestObject) (CallbackResponseObject, error) { - if !r.auth.AuthorizationEndpointEnabled() { + if !r.auth.OpenID4VPEnabled() && !r.auth.OpenID4VCIEnabled() { // Callback endpoint is only used by flows initiated through the authorization endpoint. return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, @@ -444,7 +444,7 @@ func (r Wrapper) introspectAccessToken(input string) (*ExtendedTokenIntrospectio // HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow. func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) { - if !r.auth.AuthorizationEndpointEnabled() { + if !r.auth.OpenID4VPEnabled() { return nil, oauth.OAuth2Error{ Code: oauth.InvalidRequest, Description: "authorization endpoint is disabled", @@ -616,7 +616,7 @@ func (r Wrapper) OAuthAuthorizationServerMetadata(_ context.Context, request OAu func (r Wrapper) oauthAuthorizationServerMetadata(clientID url.URL) (*oauth.AuthorizationServerMetadata, error) { md := authorizationServerMetadata(&clientID, r.auth.SupportedDIDMethods()) - if !r.auth.AuthorizationEndpointEnabled() { + if !r.auth.OpenID4VPEnabled() { md.AuthorizationEndpoint = "" } return &md, nil diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 35efca16da..b5615b3c78 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -90,7 +90,7 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { assert.NotEmpty(t, res.(OAuthAuthorizationServerMetadata200JSONResponse).AuthorizationEndpoint) }) t.Run("authorization endpoint disabled", func(t *testing.T) { - ctx := newCustomTestClient(t, verifierURL, false) + ctx := newCustomTestClient(t, verifierURL, false, false) res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{SubjectID: verifierSubject}) @@ -101,7 +101,7 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { t.Run("base URL (prepended before /iam)", func(t *testing.T) { // 200 baseURL := test.MustParseURL("https://example.com/base") - ctx := newCustomTestClient(t, baseURL, false) + ctx := newCustomTestClient(t, baseURL, false, false) res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{SubjectID: verifierSubject}) @@ -250,7 +250,7 @@ func TestWrapper_PresentationDefinition(t *testing.T) { func TestWrapper_HandleAuthorizeRequest(t *testing.T) { t.Run("disabled", func(t *testing.T) { - ctx := newCustomTestClient(t, verifierURL, false) + ctx := newCustomTestClient(t, verifierURL, false, false) response, err := ctx.client.HandleAuthorizeRequest(nil, HandleAuthorizeRequestRequestObject{SubjectID: verifierSubject}) @@ -441,7 +441,7 @@ func TestWrapper_Callback(t *testing.T) { TokenEndpoint: "https://example.com/token", } t.Run("disabled", func(t *testing.T) { - ctx := newCustomTestClient(t, verifierURL, false) + ctx := newCustomTestClient(t, verifierURL, false, false) response, err := ctx.client.Callback(nil, CallbackRequestObject{SubjectID: holderSubjectID}) @@ -1592,10 +1592,10 @@ type testCtx struct { func newTestClient(t testing.TB) *testCtx { publicURL, _ := url.Parse("https://example.com") - return newCustomTestClient(t, publicURL, true) + return newCustomTestClient(t, publicURL, true, true) } -func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled bool) *testCtx { +func newCustomTestClient(t testing.TB, publicURL *url.URL, openID4VPEnabled bool, openID4VCIEnabled bool) *testCtx { ctrl := gomock.NewController(t) storageEngine := storage.NewTestStorageEngine(t) authnServices := auth.NewMockAuthenticationServices(ctrl) @@ -1620,7 +1620,8 @@ func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled b mockVCR.EXPECT().Verifier().Return(vcVerifier).AnyTimes() mockVCR.EXPECT().Wallet().Return(mockWallet).AnyTimes() authnServices.EXPECT().IAMClient().Return(iamClient).AnyTimes() - authnServices.EXPECT().AuthorizationEndpointEnabled().Return(authEndpointEnabled).AnyTimes() + authnServices.EXPECT().OpenID4VPEnabled().Return(openID4VPEnabled).AnyTimes() + authnServices.EXPECT().OpenID4VCIEnabled().Return(openID4VCIEnabled).AnyTimes() subjectManager.EXPECT().ListDIDs(gomock.Any(), holderSubjectID).Return([]did.DID{holderDID}, nil).AnyTimes() subjectManager.EXPECT().ListDIDs(gomock.Any(), unknownSubjectID).Return(nil, didsubject.ErrSubjectNotFound).AnyTimes() diff --git a/auth/api/iam/openid4vci.go b/auth/api/iam/openid4vci.go index 021c1b7463..96ddd271e2 100644 --- a/auth/api/iam/openid4vci.go +++ b/auth/api/iam/openid4vci.go @@ -43,6 +43,9 @@ var timeFunc = time.Now const jwtTypeOpenID4VCIProof = "openid4vci-proof+jwt" func (r Wrapper) RequestOpenid4VCICredentialIssuance(ctx context.Context, request RequestOpenid4VCICredentialIssuanceRequestObject) (RequestOpenid4VCICredentialIssuanceResponseObject, error) { + if !r.auth.OpenID4VCIEnabled() { + return nil, core.Error(http.StatusBadRequest, "OpenID4VCI is disabled") + } walletDID, err := did.ParseDID(request.Body.WalletDid) if err != nil { return nil, core.InvalidInputError("invalid wallet DID") diff --git a/auth/api/iam/openid4vci_test.go b/auth/api/iam/openid4vci_test.go index 957595cbc8..721d327c4b 100644 --- a/auth/api/iam/openid4vci_test.go +++ b/auth/api/iam/openid4vci_test.go @@ -30,6 +30,7 @@ import ( "github.com/nuts-foundation/nuts-node/auth/client/iam" "github.com/nuts-foundation/nuts-node/auth/oauth" "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/test" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -50,6 +51,12 @@ func TestWrapper_RequestOpenid4VCICredentialIssuance(t *testing.T) { TokenEndpoint: "https://auth.server/token", ClientIdSchemesSupported: clientIdSchemesSupported, } + t.Run("disabled", func(t *testing.T) { + ctx := newCustomTestClient(t, test.MustParseURL("https://example.com"), false, false) + response, err := ctx.client.RequestOpenid4VCICredentialIssuance(nil, requestCredentials(holderSubjectID, issuerClientID, redirectURI)) + assert.EqualError(t, err, "OpenID4VCI is disabled") + assert.Nil(t, response) + }) t.Run("ok", func(t *testing.T) { ctx := newTestClient(t) ctx.iamClient.EXPECT().OpenIdCredentialIssuerMetadata(nil, issuerClientID).Return(&metadata, nil) diff --git a/auth/auth.go b/auth/auth.go index f135335c01..d26d608d14 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -88,9 +88,16 @@ func (auth *Auth) PublicURL() *url.URL { return auth.publicURL } -// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled. -func (auth *Auth) AuthorizationEndpointEnabled() bool { - return auth.config.AuthorizationEndpoint.Enabled +// OpenID4VPEnabled returns whether OpenID4VP is enabled. +// For backward compatibility, the old auth.authorizationendpoint.enabled flag also enables OpenID4VP. +func (auth *Auth) OpenID4VPEnabled() bool { + return auth.config.OpenID4VP.Enabled || auth.config.AuthorizationEndpoint.Enabled +} + +// OpenID4VCIEnabled returns whether OpenID4VCI (client) is enabled. +// For backward compatibility, the old auth.authorizationendpoint.enabled flag also enables OpenID4VCI. +func (auth *Auth) OpenID4VCIEnabled() bool { + return auth.config.OpenID4VCI.Enabled || auth.config.AuthorizationEndpoint.Enabled } // ContractNotary returns an implementation of the ContractNotary interface. diff --git a/auth/auth_test.go b/auth/auth_test.go index 968ea61ef8..24941a3a97 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -145,3 +145,31 @@ func TestAuth_SupportedDIDMethods(t *testing.T) { assert.Contains(t, (&Auth{configuredDIDMethods: []string{"web"}}).SupportedDIDMethods(), "web") }) } + +func TestAuth_OpenID4VPEnabled(t *testing.T) { + t.Run("false by default", func(t *testing.T) { + assert.False(t, (&Auth{}).OpenID4VPEnabled()) + }) + t.Run("true when auth.openid4vp.enabled is true", func(t *testing.T) { + a := &Auth{config: Config{OpenID4VP: OpenID4VPConfig{Enabled: true}}} + assert.True(t, a.OpenID4VPEnabled()) + }) + t.Run("true when deprecated auth.authorizationendpoint.enabled is true", func(t *testing.T) { + a := &Auth{config: Config{AuthorizationEndpoint: AuthorizationEndpointConfig{Enabled: true}}} + assert.True(t, a.OpenID4VPEnabled()) + }) +} + +func TestAuth_OpenID4VCIEnabled(t *testing.T) { + t.Run("false by default", func(t *testing.T) { + assert.False(t, (&Auth{}).OpenID4VCIEnabled()) + }) + t.Run("true when auth.openid4vci.enabled is true", func(t *testing.T) { + a := &Auth{config: Config{OpenID4VCI: OpenID4VCIConfig{Enabled: true}}} + assert.True(t, a.OpenID4VCIEnabled()) + }) + t.Run("true when deprecated auth.authorizationendpoint.enabled is true", func(t *testing.T) { + a := &Auth{config: Config{AuthorizationEndpoint: AuthorizationEndpointConfig{Enabled: true}}} + assert.True(t, a.OpenID4VCIEnabled()) + }) +} diff --git a/auth/cmd/cmd.go b/auth/cmd/cmd.go index 5a2abf01e5..4af7970bad 100644 --- a/auth/cmd/cmd.go +++ b/auth/cmd/cmd.go @@ -45,8 +45,15 @@ const ConfHTTPTimeout = "auth.http.timeout" const ConfAccessTokenLifeSpan = "auth.accesstokenlifespan" // ConfAuthEndpointEnabled is the config key for enabling the Auth v2 API's Authorization Endpoint +// Deprecated: use ConfOpenID4VPEnabled and ConfOpenID4VCIEnabled instead. const ConfAuthEndpointEnabled = "auth.authorizationendpoint.enabled" +// ConfOpenID4VPEnabled is the config key for enabling OpenID4VP +const ConfOpenID4VPEnabled = "auth.openid4vp.enabled" + +// ConfOpenID4VCIEnabled is the config key for enabling OpenID4VCI (client) +const ConfOpenID4VCIEnabled = "auth.openid4vci.enabled" + // FlagSet returns the configuration flags supported by this module. func FlagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("auth", pflag.ContinueOnError) @@ -60,7 +67,10 @@ func FlagSet() *pflag.FlagSet { flags.Int(ConfAccessTokenLifeSpan, defs.AccessTokenLifeSpan, "defines how long (in seconds) an access token is valid. Uses default in strict mode.") flags.StringSlice(ConfContractValidators, defs.ContractValidators, "sets the different contract validators to use") flags.Bool(ConfAuthEndpointEnabled, defs.AuthorizationEndpoint.Enabled, "enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. "+ - "This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature.") + "Deprecated: use auth.openid4vp.enabled and auth.openid4vci.enabled instead.") + flags.Bool(ConfOpenID4VPEnabled, defs.OpenID4VP.Enabled, "enables OpenID4VP, allowing the node to act as an OpenID4VP verifier and wallet.") + flags.Bool(ConfOpenID4VCIEnabled, defs.OpenID4VCI.Enabled, "enables OpenID4VCI (client), allowing the node to act as an OpenID4VCI wallet.") + _ = flags.MarkDeprecated(ConfAuthEndpointEnabled, "use auth.openid4vp.enabled and auth.openid4vci.enabled instead") _ = flags.MarkDeprecated("auth.http.timeout", "use httpclient.timeout instead") return flags diff --git a/auth/cmd/cmd_test.go b/auth/cmd/cmd_test.go index 5fe44f4c19..fa99a535c8 100644 --- a/auth/cmd/cmd_test.go +++ b/auth/cmd/cmd_test.go @@ -50,6 +50,8 @@ func TestFlagSet(t *testing.T) { ConfAutoUpdateIrmaSchemas, ConfIrmaCorsOrigin, ConfIrmaSchemeManager, + ConfOpenID4VCIEnabled, + ConfOpenID4VPEnabled, }, keys) } diff --git a/auth/config.go b/auth/config.go index 0f30bd3c95..62d6ee3cab 100644 --- a/auth/config.go +++ b/auth/config.go @@ -31,15 +31,35 @@ type Config struct { ClockSkew int `koanf:"clockskew"` ContractValidators []string `koanf:"contractvalidators"` AccessTokenLifeSpan int `koanf:"accesstokenlifespan"` + // Deprecated: use OpenID4VP.Enabled and OpenID4VCI.Enabled instead AuthorizationEndpoint AuthorizationEndpointConfig `koanf:"authorizationendpoint"` + OpenID4VP OpenID4VPConfig `koanf:"openid4vp"` + OpenID4VCI OpenID4VCIConfig `koanf:"openid4vci"` } +// AuthorizationEndpointConfig is deprecated. Use OpenID4VPConfig and OpenID4VCIConfig instead. type AuthorizationEndpointConfig struct { // Enabled is a flag to enable or disable the v2 API's Authorization Endpoint (/authorize), used for: // - As OpenID4VP verifier: to authenticate clients (that initiate the Authorized Code flow) using OpenID4VP // - As OpenID4VP wallet: to authenticate verifiers using OpenID4VP - // - As OpenID4VCI wallet: to support dynamic credential requests (currently not supported) - // Disabling the authorization endpoint will also disable to callback endpoint and removes the endpoint from the metadata. + // - As OpenID4VCI wallet: to support dynamic credential requests + // Deprecated: use auth.openid4vp.enabled and auth.openid4vci.enabled instead. + Enabled bool `koanf:"enabled"` +} + +// OpenID4VPConfig holds configuration for the OpenID4VP protocol. +type OpenID4VPConfig struct { + // Enabled controls whether OpenID4VP is enabled. + // When enabled, the node acts as an OpenID4VP verifier and wallet: + // - As OpenID4VP verifier: authenticate clients using OpenID4VP (Authorization Code Flow) + // - As OpenID4VP wallet: authenticate verifiers using OpenID4VP + Enabled bool `koanf:"enabled"` +} + +// OpenID4VCIConfig holds configuration for the OpenID4VCI (client) protocol. +type OpenID4VCIConfig struct { + // Enabled controls whether OpenID4VCI (client) is enabled. + // When enabled, the node acts as an OpenID4VCI wallet client, supporting dynamic credential requests. Enabled bool `koanf:"enabled"` } diff --git a/auth/interface.go b/auth/interface.go index 6a0cd7eecb..04725b6241 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -40,8 +40,10 @@ type AuthenticationServices interface { ContractNotary() services.ContractNotary // PublicURL returns the public URL of the node. PublicURL() *url.URL - // AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled. - AuthorizationEndpointEnabled() bool + // OpenID4VPEnabled returns whether OpenID4VP is enabled. + OpenID4VPEnabled() bool + // OpenID4VCIEnabled returns whether OpenID4VCI (client) is enabled. + OpenID4VCIEnabled() bool // SupportedDIDMethods lists the DID methods the Nuts node can resolve. SupportedDIDMethods() []string } diff --git a/auth/mock.go b/auth/mock.go index e92db3f34a..01310e6bf7 100644 --- a/auth/mock.go +++ b/auth/mock.go @@ -43,20 +43,6 @@ func (m *MockAuthenticationServices) EXPECT() *MockAuthenticationServicesMockRec return m.recorder } -// AuthorizationEndpointEnabled mocks base method. -func (m *MockAuthenticationServices) AuthorizationEndpointEnabled() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AuthorizationEndpointEnabled") - ret0, _ := ret[0].(bool) - return ret0 -} - -// AuthorizationEndpointEnabled indicates an expected call of AuthorizationEndpointEnabled. -func (mr *MockAuthenticationServicesMockRecorder) AuthorizationEndpointEnabled() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationEndpointEnabled", reflect.TypeOf((*MockAuthenticationServices)(nil).AuthorizationEndpointEnabled)) -} - // AuthzServer mocks base method. func (m *MockAuthenticationServices) AuthzServer() oauth.AuthorizationServer { m.ctrl.T.Helper() @@ -99,6 +85,34 @@ func (mr *MockAuthenticationServicesMockRecorder) IAMClient() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IAMClient", reflect.TypeOf((*MockAuthenticationServices)(nil).IAMClient)) } +// OpenID4VCIEnabled mocks base method. +func (m *MockAuthenticationServices) OpenID4VCIEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OpenID4VCIEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// OpenID4VCIEnabled indicates an expected call of OpenID4VCIEnabled. +func (mr *MockAuthenticationServicesMockRecorder) OpenID4VCIEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenID4VCIEnabled", reflect.TypeOf((*MockAuthenticationServices)(nil).OpenID4VCIEnabled)) +} + +// OpenID4VPEnabled mocks base method. +func (m *MockAuthenticationServices) OpenID4VPEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OpenID4VPEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// OpenID4VPEnabled indicates an expected call of OpenID4VPEnabled. +func (mr *MockAuthenticationServicesMockRecorder) OpenID4VPEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenID4VPEnabled", reflect.TypeOf((*MockAuthenticationServices)(nil).OpenID4VPEnabled)) +} + // PublicURL mocks base method. func (m *MockAuthenticationServices) PublicURL() *url.URL { m.ctrl.T.Helper() diff --git a/go.sum b/go.sum index 2ebaaae442..80d16f4f43 100644 --- a/go.sum +++ b/go.sum @@ -398,8 +398,6 @@ github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b h1:80 github.com/nuts-foundation/crypto-ecies v0.0.0-20211207143025-5b84f9efce2b/go.mod h1:6YUioYirD6/8IahZkoS4Ypc8xbeJW76Xdk1QKcziNTM= github.com/nuts-foundation/go-did v0.18.0 h1:IB0X8PrzDulpR1zAgDpaHfwoSjJpIhx5u1Tg8I2nnb8= github.com/nuts-foundation/go-did v0.18.0/go.mod h1:4od1gAmCi9HjHTQGEvHC8pLeuXdXACxidAcdA52YScc= -github.com/nuts-foundation/go-leia/v4 v4.2.0 h1:o/bgYVCyTgsfgtaKmlrcUaJ2z4NwetERC98SUWwYajM= -github.com/nuts-foundation/go-leia/v4 v4.2.0/go.mod h1:Gw6bXqJLOAmHSiXJJYbVoj+Mowp/PoBRywO0ZPsVzA0= github.com/nuts-foundation/go-leia/v4 v4.3.0 h1:R0qGISIeg2q/PCQTC9cuoBtA6cFu4WBV2DbmSOWKZyM= github.com/nuts-foundation/go-leia/v4 v4.3.0/go.mod h1:Gw6bXqJLOAmHSiXJJYbVoj+Mowp/PoBRywO0ZPsVzA0= github.com/nuts-foundation/go-stoabs v1.11.0 h1:q18jVruPdFcVhodDrnKuhq/24i0pUC/YXgzJS0glKUU= From 2c4ddd57c6a1c6d53cf95ef85ff4c7be3cf73cc4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:49:18 +0000 Subject: [PATCH 3/7] Log deprecation warning when auth.authorizationendpoint.enabled is used Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> Agent-Logs-Url: https://github.com/nuts-foundation/nuts-node/sessions/c497969e-5b23-418a-82f4-5e6af4be6b6e --- auth/auth.go | 5 +++++ auth/auth_test.go | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/auth/auth.go b/auth/auth.go index d26d608d14..54a048b169 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -22,6 +22,7 @@ import ( "crypto/tls" "errors" "github.com/nuts-foundation/nuts-node/auth/client/iam" + "github.com/nuts-foundation/nuts-node/auth/log" "github.com/nuts-foundation/nuts-node/vdr" "github.com/nuts-foundation/nuts-node/vdr/didjwk" "github.com/nuts-foundation/nuts-node/vdr/didkey" @@ -146,6 +147,10 @@ func (auth *Auth) Configure(config core.ServerConfig) error { return errors.New("in strictmode the only valid irma-scheme-manager is 'pbdf'") } + if auth.config.AuthorizationEndpoint.Enabled { + log.Logger().Warn("auth.authorizationendpoint.enabled is deprecated, use auth.openid4vp.enabled and auth.openid4vci.enabled instead") + } + var err error auth.publicURL, err = config.ServerURL() if err != nil { diff --git a/auth/auth_test.go b/auth/auth_test.go index 24941a3a97..c5d5fa30d7 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -19,12 +19,14 @@ package auth import ( + logTest "github.com/sirupsen/logrus/hooks/test" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/pki" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vdr" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -66,6 +68,19 @@ func TestAuth_Configure(t *testing.T) { require.NoError(t, i.Configure(tlsServerConfig)) }) + t.Run("deprecated auth.authorizationendpoint.enabled logs warning", func(t *testing.T) { + hook := &logTest.Hook{} + logrus.AddHook(hook) + defer func() { logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) }() + authCfg := TestConfig() + authCfg.AuthorizationEndpoint.Enabled = true + i := testInstance(t, authCfg) + require.NoError(t, i.Configure(tlsServerConfig)) + require.NotNil(t, hook.LastEntry()) + assert.Equal(t, logrus.WarnLevel, hook.LastEntry().Level) + assert.Contains(t, hook.LastEntry().Message, "auth.authorizationendpoint.enabled is deprecated") + }) + t.Run("error - IRMA config failure", func(t *testing.T) { authCfg := TestConfig() authCfg.Irma.SchemeManager = "non-existing" From 521eb5b2792e225845f26fa6f8bcbd0363406810 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:29:30 +0000 Subject: [PATCH 4/7] Remove unnecessary test for deprecation warning Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> Agent-Logs-Url: https://github.com/nuts-foundation/nuts-node/sessions/eace0514-6d68-46f9-9c5c-b1a1b5b9532e --- auth/auth_test.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/auth/auth_test.go b/auth/auth_test.go index c5d5fa30d7..24941a3a97 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -19,14 +19,12 @@ package auth import ( - logTest "github.com/sirupsen/logrus/hooks/test" "github.com/nuts-foundation/nuts-node/core" "github.com/nuts-foundation/nuts-node/crypto" "github.com/nuts-foundation/nuts-node/jsonld" "github.com/nuts-foundation/nuts-node/pki" "github.com/nuts-foundation/nuts-node/vcr" "github.com/nuts-foundation/nuts-node/vdr" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -68,19 +66,6 @@ func TestAuth_Configure(t *testing.T) { require.NoError(t, i.Configure(tlsServerConfig)) }) - t.Run("deprecated auth.authorizationendpoint.enabled logs warning", func(t *testing.T) { - hook := &logTest.Hook{} - logrus.AddHook(hook) - defer func() { logrus.StandardLogger().ReplaceHooks(logrus.LevelHooks{}) }() - authCfg := TestConfig() - authCfg.AuthorizationEndpoint.Enabled = true - i := testInstance(t, authCfg) - require.NoError(t, i.Configure(tlsServerConfig)) - require.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.WarnLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "auth.authorizationendpoint.enabled is deprecated") - }) - t.Run("error - IRMA config failure", func(t *testing.T) { authCfg := TestConfig() authCfg.Irma.SchemeManager = "non-existing" From 3e6f62cbf3a958c057fc0f43f4738290896b5d3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:31:06 +0000 Subject: [PATCH 5/7] Replace deprecated auth.authorizationendpoint with auth.openid4vp in e2e test configs Co-authored-by: reinkrul <1481228+reinkrul@users.noreply.github.com> Agent-Logs-Url: https://github.com/nuts-foundation/nuts-node/sessions/9decfe7c-69e1-48c8-9fb4-02d1217789f2 --- e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml | 2 +- e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml | 2 +- e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml b/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml index 3f31e3dd60..4156a7ac64 100644 --- a/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml +++ b/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml @@ -9,7 +9,7 @@ http: auth: contractvalidators: - dummy - authorizationendpoint: + openid4vp: enabled: true policy: directory: /opt/nuts/policy \ No newline at end of file diff --git a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml index e6e6a13d66..34b8d08029 100644 --- a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml +++ b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml @@ -12,7 +12,7 @@ auth: - dummy irma: autoupdateschemas: false - authorizationendpoint: + openid4vp: enabled: true policy: directory: /opt/nuts/policies \ No newline at end of file diff --git a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml index ae8598702b..1a171a7c22 100644 --- a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml @@ -11,7 +11,7 @@ auth: - dummy irma: autoupdateschemas: false - authorizationendpoint: + openid4vp: enabled: true policy: directory: /opt/nuts/policies From c8f96bd82bcf0e64de86ee062c034b36865ca21b Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Thu, 26 Mar 2026 16:25:26 +0100 Subject: [PATCH 6/7] Add OpenID4VCI documentation and update config references - Add integrating/openid4vci.rst: how-to guide for requesting credentials over OpenID4VCI (wallet-initiated flow) - Regenerate server_options.rst: replace deprecated auth.authorizationendpoint.enabled with auth.openid4vci.enabled and auth.openid4vp.enabled - Update recommended-deployment.rst to reference the new config keys - Register openid4vci.rst in the docs index Co-Authored-By: Claude Sonnet 4.6 --- docs/index.rst | 1 + .../deployment/recommended-deployment.rst | 2 +- docs/pages/deployment/server_options.rst | 3 +- docs/pages/integrating/openid4vci.rst | 108 ++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 docs/pages/integrating/openid4vci.rst diff --git a/docs/index.rst b/docs/index.rst index c6316da266..5a77bab718 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,7 @@ Nuts documentation pages/integrating/api.rst pages/integrating/api-authentication.rst pages/integrating/vc.rst + pages/integrating/openid4vci.rst pages/integrating/supported-protocols-formats.rst pages/integrating/version-incompatibilities.rst pages/release_notes.rst diff --git a/docs/pages/deployment/recommended-deployment.rst b/docs/pages/deployment/recommended-deployment.rst index 6f8735c68a..b26f854b15 100644 --- a/docs/pages/deployment/recommended-deployment.rst +++ b/docs/pages/deployment/recommended-deployment.rst @@ -58,7 +58,7 @@ These HTTP endpoints are available on ``:8080``. *Security*: HTTPS with **publicly trusted** server certificate (on proxy). -* **/oauth2**: for accessing OAuth2 and OpenID services. ``/callback`` and ``/authorize`` are disabled by default on the Nuts node. They can be enabled by setting ``auth.authorizationendpoint.enabled`` to ``true``. +* **/oauth2**: for accessing OAuth2 and OpenID services. ``/callback`` and ``/authorize`` are disabled by default on the Nuts node. They can be enabled by setting ``auth.openid4vci.enabled`` and/or ``auth.openid4vp.enabled`` to ``true``. Use this only for experimental OpenID4VCI and OpenID4VP use cases. *Users*: Verifiable Credential issuers and verifiers, OAuth2 client applications (e.g. other Nuts nodes, resource viewers) diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index a281ac14d6..5d0ac0a495 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -16,7 +16,8 @@ verbosity info Log level (trace, debug, info, warn, error) httpclient.timeout 30s Request time-out for HTTP clients, such as '10s'. Refer to Golang's 'time.Duration' syntax for a more elaborate description of the syntax. **Auth** - auth.authorizationendpoint.enabled false enables the v2 API's OAuth2 Authorization Endpoint, used by OpenID4VP and OpenID4VCI. This flag might be removed in a future version (or its default become 'true') as the use cases and implementation of OpenID4VP and OpenID4VCI mature. + auth.openid4vci.enabled false enables OpenID4VCI (client), allowing the node to act as an OpenID4VCI wallet. + auth.openid4vp.enabled false enables OpenID4VP, allowing the node to act as an OpenID4VP verifier and wallet. **Crypto** crypto.storage Storage to use, 'fs' for file system (for development purposes), 'vaultkv' for HashiCorp Vault KV store, 'azure-keyvault' for Azure Key Vault, 'external' for an external backend (deprecated). crypto.azurekv.hsm false Whether to store the key in a hardware security module (HSM). If true, the Azure Key Vault must be configured for HSM usage. Default: false diff --git a/docs/pages/integrating/openid4vci.rst b/docs/pages/integrating/openid4vci.rst new file mode 100644 index 0000000000..bee79a65ec --- /dev/null +++ b/docs/pages/integrating/openid4vci.rst @@ -0,0 +1,108 @@ +.. _openid4vci: + +Requesting Credentials over OpenID4VCI +####################################### + +The Nuts node supports receiving Verifiable Credentials from an external issuer over `OpenID for Verifiable Credential Issuance (OpenID4VCI) `_. +Your application triggers the credential request, the user authorizes it at the issuer, and the node receives and stores the credential. + +Configuration +************* + +To enable receiving credentials over OpenID4VCI, set ``auth.openid4vci.enabled`` to ``true``: + +.. code-block:: yaml + + auth: + openid4vci: + enabled: true + +When enabled, the node exposes: + +- ``POST /internal/auth/v2/{subjectID}/request-credential`` — to initiate a wallet-initiated credential request. +- ``GET /oauth2/{subjectID}/callback`` — the redirect endpoint the issuer redirects back to after user authorization. + +The ``url`` server option must be set to the node's publicly reachable base URL (e.g. ``https://example.com``), +since it is used to construct the callback URL sent to the issuer. + +Flow +**** + +Your application initiates a request for a credential from a specific issuer. +The node handles the OAuth2 authorization code flow with PKCE, obtains an access token, and uses it +to retrieve and store the credential. + +The flow proceeds as follows: + +1. Your application calls ``POST /internal/auth/v2/{subjectID}/request-credential``. +2. The node returns a redirect URL pointing to the issuer's authorization endpoint. +3. Your application redirects the user-agent (browser) to that URL. +4. The user authorizes the credential issuance at the issuer. +5. The issuer redirects the user-agent back to the node's ``/oauth2/{subjectID}/callback`` endpoint. +6. The node exchanges the authorization code for an access token, then requests and stores the credential. +7. The node redirects the user-agent to the ``redirect_uri`` provided in step 1. + +Step 1: Initiate the credential request +======================================== + +Call ``POST /internal/auth/v2/{subjectID}/request-credential`` with the following body: + +.. code-block:: json + + { + "wallet_did": "did:web:example.com:iam:9bc7d8e2", + "issuer": "https://issuer.example.com/oauth2", + "authorization_details": [ + { + "type": "openid_credential", + "credential_configuration_id": "HealthcareProviderRoleTypeCredential" + } + ], + "redirect_uri": "https://my-xis.example.com/callback" + } + +Fields: + +- ``wallet_did``: The DID that will be the subject of the issued credential. Must be a DID owned by the given ``subjectID``. +- ``issuer``: The OAuth2 Authorization Server URL of the credential issuer (as defined in RFC 8414), used to discover the issuer's endpoints (e.g. ``https://issuer.example.com/oauth2``). +- ``authorization_details``: Array of `authorization_details` objects (RFC 9396) describing the requested credentials. The ``credential_configuration_id`` value must match a credential configuration supported by the issuer; consult the issuer's credential issuer metadata (``/.well-known/openid-credential-issuer``) for supported values. +- ``redirect_uri``: The URL to which the user-agent is redirected after the node has received and stored the credential. + +The node responds with a redirect URL: + +.. code-block:: json + + { + "redirect_uri": "https://issuer.example.com/oauth2/authorize?response_type=code&client_id=..." + } + +Step 2: Redirect the user-agent +================================ + +Redirect the user-agent (browser) to the returned ``redirect_uri``. The user will be prompted to authorize +the credential issuance at the issuer. + +After authorization, the issuer redirects the user-agent back to the node's callback URL +(``{url}/oauth2/{subjectID}/callback``). The node exchanges the authorization code for an access token, +requests the credential, verifies it, and stores it in the wallet. + +Finally, the node redirects the user-agent to the ``redirect_uri`` you provided in step 1. + +Checking for errors +=================== + +If an error occurs during the callback phase (e.g. the issuer returns an error, or the credential cannot be verified), +the node redirects to the application's ``redirect_uri`` with an ``error`` query parameter: + +.. code-block:: + + https://my-xis.example.com/callback?error=access_denied&error_description=... + +Your application should check for the ``error`` query parameter and handle it accordingly. + +Using issued credentials +************************ + +Credentials received over OpenID4VCI are stored in the node's wallet. Once stored, they can be used like any +other credential held by the node — for example, to create Verifiable Presentations for authentication or +authorization flows. From dc2750050224aa9c3909fce2c4eef697b142c976 Mon Sep 17 00:00:00 2001 From: "qltysh[bot]" <168846912+qltysh[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:27:12 +0000 Subject: [PATCH 7/7] qlty fmt --- auth/config.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth/config.go b/auth/config.go index 62d6ee3cab..dcb2068c75 100644 --- a/auth/config.go +++ b/auth/config.go @@ -26,11 +26,11 @@ import ( // Config holds all the configuration params type Config struct { - Irma IrmaConfig `koanf:"irma"` - HTTPTimeout int `koanf:"http.timeout"` - ClockSkew int `koanf:"clockskew"` - ContractValidators []string `koanf:"contractvalidators"` - AccessTokenLifeSpan int `koanf:"accesstokenlifespan"` + Irma IrmaConfig `koanf:"irma"` + HTTPTimeout int `koanf:"http.timeout"` + ClockSkew int `koanf:"clockskew"` + ContractValidators []string `koanf:"contractvalidators"` + AccessTokenLifeSpan int `koanf:"accesstokenlifespan"` // Deprecated: use OpenID4VP.Enabled and OpenID4VCI.Enabled instead AuthorizationEndpoint AuthorizationEndpointConfig `koanf:"authorizationendpoint"` OpenID4VP OpenID4VPConfig `koanf:"openid4vp"`