diff --git a/README.rst b/README.rst index 86fdb9166b..5c92e4c00a 100644 --- a/README.rst +++ b/README.rst @@ -179,6 +179,8 @@ The following options can be configured on the server: url Public facing URL of the server (required). Must be HTTPS when strictmode is set. 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. **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/auth/api/auth/v1/api_test.go b/auth/api/auth/v1/api_test.go index 05cd115299..a66ed5d417 100644 --- a/auth/api/auth/v1/api_test.go +++ b/auth/api/auth/v1/api_test.go @@ -58,6 +58,8 @@ type TestContext struct { audit context.Context } +var _ pkg2.AuthenticationServices = &mockAuthClient{} + type mockAuthClient struct { ctrl *gomock.Controller authzServer *oauth.MockAuthorizationServer @@ -66,6 +68,10 @@ type mockAuthClient struct { relyingParty *oauth.MockRelyingParty } +func (m *mockAuthClient) AuthorizationEndpointEnabled() bool { + return true +} + func (m *mockAuthClient) AuthzServer() oauth.AuthorizationServer { return m.authzServer } diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index 407624b49b..0adced35b1 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -71,8 +71,6 @@ type httpRequestContextKey struct{} // TODO: Might want to make this configurable at some point const accessTokenValidity = 15 * time.Minute -const oid4vciSessionValidity = 15 * time.Minute - // cacheControlMaxAgeURLs holds API endpoints that should have a max-age cache control header set. var cacheControlMaxAgeURLs = []string{ "/oauth2/:did/presentation_definition", @@ -241,6 +239,13 @@ func (r Wrapper) HandleTokenRequest(ctx context.Context, request HandleTokenRequ } 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. + return nil, oauth.OAuth2Error{ + Code: oauth.InvalidRequest, + Description: "callback endpoint is disabled", + } + } // validate request // check did in path ownDID, err := r.toOwnedDID(ctx, request.Did) @@ -419,6 +424,12 @@ 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() { + return nil, oauth.OAuth2Error{ + Code: oauth.InvalidRequest, + Description: "authorization endpoint is disabled", + } + } ownDID, err := r.toOwnedDIDForOAuth2(ctx, request.Did) if err != nil { return nil, err @@ -601,6 +612,9 @@ func (r Wrapper) oauthAuthorizationServerMetadata(ownDID *did.DID) (*oauth.Autho return nil, err } md := authorizationServerMetadata(*ownDID, issuerURL) + if !r.auth.AuthorizationEndpointEnabled() { + md.AuthorizationEndpoint = "" + } return &md, nil } diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 6a32c00a53..7b2d539f6c 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -83,13 +83,23 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { require.NoError(t, err) assert.IsType(t, OAuthAuthorizationServerMetadata200JSONResponse{}, res) + assert.NotEmpty(t, res.(OAuthAuthorizationServerMetadata200JSONResponse).AuthorizationEndpoint) }) + t.Run("authorization endpoint disabled", func(t *testing.T) { + ctx := newCustomTestClient(t, verifierURL, false) + ctx.documentOwner.EXPECT().IsOwner(nil, webDID).Return(true, nil) + + res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{Did: webDID.String()}) + require.NoError(t, err) + assert.IsType(t, OAuthAuthorizationServerMetadata200JSONResponse{}, res) + assert.Empty(t, res.(OAuthAuthorizationServerMetadata200JSONResponse).AuthorizationEndpoint) + }) t.Run("base URL (prepended before /iam)", func(t *testing.T) { var webDID = did.MustParseDID("did:web:example.com:base:iam:123") // 200 baseURL := test.MustParseURL("https://example.com/base") - ctx := newTestClientWithBaseURL(t, baseURL) + ctx := newCustomTestClient(t, baseURL, false) ctx.documentOwner.EXPECT().IsOwner(nil, webDID).Return(true, nil) res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{Did: webDID.String()}) @@ -221,7 +231,14 @@ func TestWrapper_PresentationDefinition(t *testing.T) { } func TestWrapper_HandleAuthorizeRequest(t *testing.T) { + t.Run("disabled", func(t *testing.T) { + ctx := newCustomTestClient(t, verifierURL, false) + response, err := ctx.client.HandleAuthorizeRequest(nil, HandleAuthorizeRequestRequestObject{Did: verifierDID.String()}) + + requireOAuthError(t, err, oauth.InvalidRequest, "authorization endpoint is disabled") + assert.Nil(t, response) + }) t.Run("ok - response_type=code", func(t *testing.T) { ctx := newTestClient(t) @@ -238,7 +255,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) { oauth.CodeChallengeParam: "code_challenge", oauth.CodeChallengeMethodParam: "S256", } - ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil).MinTimes(1) + ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil) ctx.jar.EXPECT().Parse(gomock.Any(), gomock.Any(), url.Values{"key": []string{"test_value"}}).Return(requestParams, nil) // handleAuthorizeRequestFromHolder @@ -333,7 +350,7 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) { oauth.ResponseTypeParam: "unsupported", } ctx.jar.EXPECT().Parse(gomock.Any(), gomock.Any(), gomock.Any()).Return(requestParams, nil) - ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil).MinTimes(1) + ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil) res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]interface{}{}), HandleAuthorizeRequestRequestObject{Did: verifierDID.String()}) @@ -377,7 +394,14 @@ func TestWrapper_Callback(t *testing.T) { OtherDID: &verifierDID, TokenEndpoint: "https://example.com/token", } + t.Run("disabled", func(t *testing.T) { + ctx := newCustomTestClient(t, verifierURL, false) + response, err := ctx.client.Callback(nil, CallbackRequestObject{Did: holderDID.String()}) + + requireOAuthError(t, err, oauth.InvalidRequest, "callback endpoint is disabled") + assert.Nil(t, response) + }) t.Run("ok - error flow", func(t *testing.T) { ctx := newTestClient(t) ctx.documentOwner.EXPECT().IsOwner(gomock.Any(), holderDID).Return(true, nil) @@ -1449,10 +1473,10 @@ type testCtx struct { func newTestClient(t testing.TB) *testCtx { publicURL, _ := url.Parse("https://example.com") - return newTestClientWithBaseURL(t, publicURL) + return newCustomTestClient(t, publicURL, true) } -func newTestClientWithBaseURL(t testing.TB, publicURL *url.URL) *testCtx { +func newCustomTestClient(t testing.TB, publicURL *url.URL, authEndpointEnabled bool) *testCtx { ctrl := gomock.NewController(t) storageEngine := storage.NewTestStorageEngine(t) authnServices := auth.NewMockAuthenticationServices(ctrl) @@ -1476,6 +1500,7 @@ func newTestClientWithBaseURL(t testing.TB, publicURL *url.URL) *testCtx { mockVCR.EXPECT().Verifier().Return(vcVerifier).AnyTimes() mockVCR.EXPECT().Wallet().Return(mockWallet).AnyTimes() authnServices.EXPECT().IAMClient().Return(iamClient).AnyTimes() + authnServices.EXPECT().AuthorizationEndpointEnabled().Return(authEndpointEnabled).AnyTimes() mockVDR.EXPECT().Resolver().Return(mockResolver).AnyTimes() mockVDR.EXPECT().DocumentOwner().Return(mockDocumentOwner).AnyTimes() diff --git a/auth/auth.go b/auth/auth.go index 6df0457f4d..9448abfe76 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -77,6 +77,11 @@ 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 +} + // ContractNotary returns an implementation of the ContractNotary interface. func (auth *Auth) ContractNotary() services.ContractNotary { return auth.contractNotary diff --git a/auth/cmd/cmd.go b/auth/cmd/cmd.go index 50832a1036..a76ba6fbf1 100644 --- a/auth/cmd/cmd.go +++ b/auth/cmd/cmd.go @@ -41,6 +41,9 @@ 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 +const ConfAuthEndpointEnabled = "auth.authorizationendpoint.enabled" + // FlagSet returns the configuration flags supported by this module. func FlagSet() *pflag.FlagSet { flags := pflag.NewFlagSet("auth", pflag.ContinueOnError) @@ -52,6 +55,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.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 3293844996..edbc38f45b 100644 --- a/auth/cmd/cmd_test.go +++ b/auth/cmd/cmd_test.go @@ -43,6 +43,7 @@ func TestFlagSet(t *testing.T) { assert.Equal(t, []string{ ConfAccessTokenLifeSpan, + ConfAuthEndpointEnabled, ConfClockSkew, ConfContractValidators, ConfHTTPTimeout, diff --git a/auth/config.go b/auth/config.go index e482853026..71cbc7d75c 100644 --- a/auth/config.go +++ b/auth/config.go @@ -26,11 +26,21 @@ 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"` + AuthorizationEndpoint AuthorizationEndpointConfig `koanf:"authorizationendpoint"` +} + +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. + Enabled bool `koanf:"enabled"` } type IrmaConfig struct { diff --git a/auth/interface.go b/auth/interface.go index 5d86358a2e..ee1cad0c65 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -40,4 +40,6 @@ 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 } diff --git a/auth/mock.go b/auth/mock.go index b35aae47c4..a9beb62de3 100644 --- a/auth/mock.go +++ b/auth/mock.go @@ -42,6 +42,20 @@ 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() diff --git a/docs/generate_docs.go b/docs/generate_docs.go index bd6f2b91c8..9a025269e0 100644 --- a/docs/generate_docs.go +++ b/docs/generate_docs.go @@ -111,7 +111,11 @@ func generateServerOptions(system *core.System) { return strings.HasPrefix(f.Name, "events.") }, func(f *pflag.Flag) bool { - return strings.HasPrefix(f.Name, "auth.") + // Auth engine + return strings.HasPrefix(f.Name, "auth.irma") || + strings.HasPrefix(f.Name, "auth.clockskew") || + strings.HasPrefix(f.Name, "auth.contractvalidators") || + strings.HasPrefix(f.Name, "auth.accesstokenlifespan") }, func(f *pflag.Flag) bool { return strings.HasPrefix(f.Name, "tls.") diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 9cc2f3f7fd..806774f4db 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -14,6 +14,8 @@ url Public facing URL of the server (required). Must be HTTPS when strictmode is set. 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. **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/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml b/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml index a4c197ba4c..eef6fe6a4a 100644 --- a/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml +++ b/e2e-tests/browser/openid4vp_employeecredential/config/nuts.yaml @@ -8,6 +8,8 @@ http: auth: contractvalidators: - dummy + authorizationendpoint: + enabled: true policy: directory: /opt/nuts/policy vdr: diff --git a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml index 33749ab6b9..2496034a39 100644 --- a/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml +++ b/e2e-tests/oauth-flow/openid4vp/node-A/nuts.yaml @@ -11,6 +11,8 @@ auth: - dummy irma: autoupdateschemas: false + authorizationendpoint: + enabled: true policy: directory: /opt/nuts/policies vdr: diff --git a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml index b3a7f0f701..b7d5d86c4c 100644 --- a/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/openid4vp/node-B/nuts.yaml @@ -10,6 +10,8 @@ auth: - dummy irma: autoupdateschemas: false + authorizationendpoint: + enabled: true policy: directory: /opt/nuts/policies tls: