Skip to content
Draft
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
4 changes: 4 additions & 0 deletions auth/api/auth/v1/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ func (m *mockAuthClient) AuthorizationEndpointEnabled() bool {
return true
}

func (m *mockAuthClient) OpenID4VCIEnabled() bool {
return true
}

func (m *mockAuthClient) AuthzServer() oauth.AuthorizationServer {
return m.authzServer
}
Expand Down
42 changes: 37 additions & 5 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
"encoding/json"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/core/to"
"html/template"
"net/http"
"net/url"
"slices"
"strings"
"time"

"github.com/nuts-foundation/nuts-node/core/to"

"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
Expand Down Expand Up @@ -252,8 +253,9 @@
}

func (r Wrapper) Callback(ctx context.Context, request CallbackRequestObject) (CallbackResponseObject, error) {
if !r.auth.AuthorizationEndpointEnabled() {
// Callback endpoint is only used by flows initiated through the authorization endpoint.
// Callback endpoint is only used by flows initiated through the authorization endpoint.
// It's used by both OpenID4VP and OpenID4VCI flows
if !r.auth.OpenID4VPEnabled() && !r.auth.OpenID4VCIEnabled() {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "callback endpoint is disabled",
Expand Down Expand Up @@ -297,8 +299,14 @@
// continue flow
switch oauthSession.ClientFlow {
case credentialRequestClientFlow:
if !r.auth.OpenID4VCIEnabled() {
return nil, withCallbackURI(oauthError(oauth.InvalidRequest, "openid4vci is disabled"), oauthSession.redirectURI())
}
return r.handleOpenID4VCICallback(ctx, *request.Params.Code, oauthSession)
case accessTokenRequestClientFlow:
if !r.auth.OpenID4VPEnabled() {
return nil, withCallbackURI(oauthError(oauth.InvalidRequest, "openid4vp is disabled"), oauthSession.redirectURI())
}
return r.handleCallback(ctx, *request.Params.Code, oauthSession)
default:
// programming error, should never happen
Expand Down Expand Up @@ -444,7 +452,8 @@

// 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() {
// Check if either OpenID4VP or OpenID4VCI authorization endpoint is enabled
if !r.auth.OpenID4VPEnabled() && !r.auth.OpenID4VCIEnabled() {
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "authorization endpoint is disabled",
Expand Down Expand Up @@ -482,6 +491,18 @@
// - Regular authorization code flow for EHR data access through access token, authentication of end-user using OpenID4VP.
// - OpenID4VCI; authorization code flow for credential issuance to (end-user) wallet

// Check if authorization endpoint is enabled for code flow.
// Since we can't distinguish between the two use cases at this point (see TODO below),
// we allow the request if either OpenID4VP or OpenID4VCI is enabled.
if !r.auth.AuthorizationEndpointEnabled() && !r.auth.OpenID4VCIEnabled() {

Check failure on line 497 in auth/api/iam/api.go

View workflow job for this annotation

GitHub Actions / Run govulncheck

r.auth.AuthorizationEndpointEnabled undefined (type "github.com/nuts-foundation/nuts-node/auth".AuthenticationServices has no field or method AuthorizationEndpointEnabled)
redirectURI, _ := url.Parse(requestObject.get(oauth.RedirectURIParam))
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "authorization endpoint is disabled",
RedirectURI: redirectURI,
}
}

// TODO: officially flow switching has to be determined by the client_id
// registered client_ids should list which flow they support
// client registration could be done via rfc7591....
Expand All @@ -490,6 +511,16 @@
case oauth.VPTokenResponseType:
// Options:
// - OpenID4VP flow, vp_token is sent in Authorization Response
// Check if OpenID4VP authorization endpoint is enabled for vp_token flow
if !r.auth.AuthorizationEndpointEnabled() {

Check failure on line 515 in auth/api/iam/api.go

View workflow job for this annotation

GitHub Actions / Run govulncheck

r.auth.AuthorizationEndpointEnabled undefined (type "github.com/nuts-foundation/nuts-node/auth".AuthenticationServices has no field or method AuthorizationEndpointEnabled)
redirectURI, _ := url.Parse(requestObject.get(oauth.RedirectURIParam))
return nil, oauth.OAuth2Error{
Code: oauth.InvalidRequest,
Description: "authorization endpoint for OpenID4VP is disabled",
RedirectURI: redirectURI,
}
}

// non-spec: if the scheme is openid4vp (URL starts with openid4vp:), the OpenID4VP request should be handled by a user wallet, rather than an organization wallet.
// Requests to user wallets can then be rendered as QR-code (or use a cloud wallet).
// Note that it can't be called from the outside, but only by internal dispatch (since Echo doesn't handle openid4vp:, obviously).
Expand Down Expand Up @@ -616,7 +647,8 @@

func (r Wrapper) oauthAuthorizationServerMetadata(clientID url.URL) (*oauth.AuthorizationServerMetadata, error) {
md := authorizationServerMetadata(&clientID, r.auth.SupportedDIDMethods())
if !r.auth.AuthorizationEndpointEnabled() {
// Remove authorization endpoint from metadata if both OpenID4VP and OpenID4VCI authorization endpoints are disabled
if !r.auth.AuthorizationEndpointEnabled() && !r.auth.OpenID4VCIEnabled() {

Check failure on line 651 in auth/api/iam/api.go

View workflow job for this annotation

GitHub Actions / Run govulncheck

r.auth.AuthorizationEndpointEnabled undefined (type "github.com/nuts-foundation/nuts-node/auth".AuthenticationServices has no field or method AuthorizationEndpointEnabled)
md.AuthorizationEndpoint = ""
}
return &md, nil
Expand Down
41 changes: 34 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,13 +250,39 @@ 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})

requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint is disabled")
assert.Nil(t, response)
})
t.Run("OpenID4VCI and OpenID4VP disabled, response_type=code - should fail", func(t *testing.T) {
ctx := newCustomTestClient(t, verifierURL, false, false) // Both disabled

// This should fail at the outer check (before parsing) since both configs are disabled
res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{"key": "test_value"}),
HandleAuthorizeRequestRequestObject{SubjectID: verifierSubject})

requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint is disabled")
assert.Nil(t, res)
})
t.Run("OpenID4VP disabled, response_type=vp_token - should fail", func(t *testing.T) {
ctx := newCustomTestClient(t, verifierURL, false, true) // Only OpenID4VCI enabled

// HandleAuthorizeRequest
requestParams := oauthParameters{
oauth.RedirectURIParam: "https://example.com",
oauth.ResponseTypeParam: oauth.VPTokenResponseType,
}
ctx.jar.EXPECT().Parse(gomock.Any(), gomock.Any(), url.Values{"key": []string{"test_value"}}).Return(requestParams, nil)

res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{"key": "test_value"}),
HandleAuthorizeRequestRequestObject{SubjectID: verifierSubject})

requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint for OpenID4VP is disabled")
assert.Nil(t, res)
})
t.Run("ok - response_type=code", func(t *testing.T) {
ctx := newTestClient(t)

Expand Down Expand Up @@ -428,7 +454,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 @@ -1579,10 +1605,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 @@ -1607,7 +1633,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().AuthorizationEndpointEnabled().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
7 changes: 6 additions & 1 deletion auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

const contractValidity = 60 * time.Minute

var _ AuthenticationServices = (*Auth)(nil)

Check failure on line 51 in auth/auth.go

View workflow job for this annotation

GitHub Actions / Run govulncheck

cannot use (*Auth)(nil) (value of type *Auth) as AuthenticationServices value in variable declaration: *Auth does not implement AuthenticationServices (missing method OpenID4VPEnabled)

// Auth is the main struct of the Auth service
type Auth struct {
Expand Down Expand Up @@ -88,11 +88,16 @@
return auth.publicURL
}

// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled.
// AuthorizationEndpointEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled for OpenID4VP flows.
func (auth *Auth) AuthorizationEndpointEnabled() bool {
return auth.config.AuthorizationEndpoint.Enabled

Check failure on line 93 in auth/auth.go

View workflow job for this annotation

GitHub Actions / Run govulncheck

auth.config.AuthorizationEndpoint undefined (type Config has no field or method AuthorizationEndpoint)
}

// OpenID4VCIEnabled returns whether the v2 API's OAuth2 Authorization Endpoint is enabled for OpenID4VCI flows.
func (auth *Auth) OpenID4VCIEnabled() bool {
return auth.config.OpenID4VCI.Enabled
}

// ContractNotary returns an implementation of the ContractNotary interface.
func (auth *Auth) ContractNotary() services.ContractNotary {
return auth.contractNotary
Expand Down
12 changes: 9 additions & 3 deletions auth/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,15 @@ const ConfHTTPTimeout = "auth.http.timeout"
// ConfAccessTokenLifeSpan defines how long (in seconds) an access token is valid
const ConfAccessTokenLifeSpan = "auth.accesstokenlifespan"

// ConfAuthEndpointEnabled is the config key for enabling the Auth v2 API's Authorization Endpoint
// ConfAuthEndpointEnabled is the config key for enabling the Auth v2 API's Authorization Endpoint for OpenID4VP flows
const ConfAuthEndpointEnabled = "auth.authorizationendpoint.enabled"

// ConfOpenID4VCIEnabled is the config key for enabling OpenID4VCI.
const ConfOpenID4VCIEnabled = "auth.openid4vci.enabled"

// ConfOpenID4VPEnabled is the config key for enabling OpenID4VP.
const ConfOpenID4VPEnabled = "auth.openid4vp.enabled"

// FlagSet returns the configuration flags supported by this module.
func FlagSet() *pflag.FlagSet {
flags := pflag.NewFlagSet("auth", pflag.ContinueOnError)
Expand All @@ -59,8 +65,8 @@ func FlagSet() *pflag.FlagSet {
flags.Int(ConfClockSkew, defs.ClockSkew, "allowed JWT Clock skew in milliseconds")
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.")
flags.Bool(ConfOpenID4VCIEnabled, defs.OpenID4VCI.Enabled, "enables OpenID4VCI (experimental) support, allowing credential issuance be requested for local wallets through OpenID4VCI.")
flags.Bool(ConfOpenID4VPEnabled, defs.OpenID4VP.Enabled, "enables OpenID4VCI (experimental) support, allowing; authentication of clients using OpenID4VP (as verifier) and responding to OpenID4VP requests from OpenID4VP verifiers (as wallet).")
_ = flags.MarkDeprecated("auth.http.timeout", "use httpclient.timeout instead")

return flags
Expand Down
1 change: 1 addition & 0 deletions auth/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func TestFlagSet(t *testing.T) {
ConfAutoUpdateIrmaSchemas,
ConfIrmaCorsOrigin,
ConfIrmaSchemeManager,
ConfOpenID4VCIEnabled,
}, keys)
}

Expand Down
29 changes: 17 additions & 12 deletions auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,25 @@ 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"`
AuthorizationEndpoint AuthorizationEndpointConfig `koanf:"authorizationendpoint"`
Irma IrmaConfig `koanf:"irma"`
HTTPTimeout int `koanf:"http.timeout"`
ClockSkew int `koanf:"clockskew"`
ContractValidators []string `koanf:"contractvalidators"`
AccessTokenLifeSpan int `koanf:"accesstokenlifespan"`
OpenID4VCI OpenID4VCIConfig `koanf:"openid4vci"`
OpenID4VP OpenID4VPConfig `koanf:"openid4vp"`
}

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.
type OpenID4VPConfig struct {
// Enabled is a flag to enable or disable OpenID4VP support:
// - As OpenID4VP verifier: to authenticate clients using OpenID4VP
// - As OpenID4VP wallet: to respond to OpenID4VP requests to from OpenID4VP verifiers.
Enabled bool `koanf:"enabled"`
}

type OpenID4VCIConfig struct {
// Enabled is a flag to enable OpenID4VCI support.
// If enabled, credential issuance can be requested for local wallets through OpenID4VCI.
Enabled bool `koanf:"enabled"`
}

Expand Down
9 changes: 6 additions & 3 deletions auth/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
package auth

import (
"net/url"

"github.com/nuts-foundation/nuts-node/auth/client/iam"
"github.com/nuts-foundation/nuts-node/auth/services"
"github.com/nuts-foundation/nuts-node/auth/services/oauth"
"net/url"
)

// ModuleName contains the name of this module
Expand All @@ -40,8 +41,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
// OpenID4VCIEnabled returns whether OpenID4VCI is enabled.
OpenID4VCIEnabled() bool
// OpenID4VPEnabled returns whether OpenID4VP is enabled.
OpenID4VPEnabled() bool
// SupportedDIDMethods lists the DID methods the Nuts node can resolve.
SupportedDIDMethods() []string
}
14 changes: 14 additions & 0 deletions auth/mock.go

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

Loading
Loading