Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand All @@ -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})

Expand Down Expand Up @@ -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})

Expand Down Expand Up @@ -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})

Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions auth/api/iam/openid4vci.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 7 additions & 0 deletions auth/api/iam/openid4vci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
18 changes: 15 additions & 3 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -88,9 +89,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.
Expand Down Expand Up @@ -139,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 {
Expand Down
28 changes: 28 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}
12 changes: 11 additions & 1 deletion auth/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions auth/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func TestFlagSet(t *testing.T) {
ConfAutoUpdateIrmaSchemas,
ConfIrmaCorsOrigin,
ConfIrmaSchemeManager,
ConfOpenID4VCIEnabled,
ConfOpenID4VPEnabled,
}, keys)
}

Expand Down
34 changes: 27 additions & 7 deletions auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,40 @@ 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"`
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"`
}

Expand Down
6 changes: 4 additions & 2 deletions auth/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
42 changes: 28 additions & 14 deletions auth/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/deployment/recommended-deployment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading