diff --git a/auth/api/iam/api.go b/auth/api/iam/api.go index e0d002a280..c4d6d6dcaf 100644 --- a/auth/api/iam/api.go +++ b/auth/api/iam/api.go @@ -153,7 +153,7 @@ func (r Wrapper) HandleTokenRequest(_ context.Context, request HandleTokenReques } // IntrospectAccessToken allows the resource server (XIS/EHR) to introspect details of an access token issued by this node -func (r Wrapper) IntrospectAccessToken(ctx context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) { +func (r Wrapper) IntrospectAccessToken(_ context.Context, request IntrospectAccessTokenRequestObject) (IntrospectAccessTokenResponseObject, error) { // Validate token if request.Body.Token == "" { // Return 200 + 'Active = false' when token is invalid or malformed @@ -232,9 +232,10 @@ func toAnyMap(input any) (*map[string]any, error) { // HandleAuthorizeRequest handles calls to the authorization endpoint for starting an authorization code flow. func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAuthorizeRequestRequestObject) (HandleAuthorizeRequestResponseObject, error) { - // TODO: must be web DID once web DID creation and DB are implemented - ownDID := idToNutsDID(request.Id) - // Create session object to be passed to handler + ownDID, err := r.idToOwnedDID(ctx, request.Id) + if err != nil { + return nil, err + } // Workaround: deepmap codegen doesn't support dynamic query parameters. // See https://github.com/deepmap/oapi-codegen/issues/1129 @@ -243,7 +244,7 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho for key, value := range httpRequest.URL.Query() { params[key] = value[0] } - session := createSession(params, ownDID) + session := createSession(params, *ownDID) if session.RedirectURI == "" { // TODO: Spec says that the redirect URI is optional, but it's not clear what to do if it's not provided. // Threat models say it's unsafe to omit redirect_uri. @@ -253,15 +254,31 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho Description: "redirect_uri is required", } } + // todo: store session in database? switch session.ResponseType { case responseTypeCode: // Options: // - 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 - // - OpenID4VP, vp_token is sent in Token Response; authorization code flow for presentation exchange (not required a.t.m.) - // TODO: Switch on parameters to right flow - panic("not implemented") + + // 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.... + // for now we switch on client_id format. + // when client_id is a did:web, it is a cloud/server wallet + // otherwise it's a normal registered client which we do not support yet + // Note: this is the user facing OpenID4VP flow with a "vp_token" responseType, the demo uses the "vp_token id_token" responseType + clientId := session.ClientID + if strings.HasPrefix(clientId, "did:web:") { + // client is a cloud wallet with user + return r.handleAuthorizeRequestFromHolder(ctx, *ownDID, params) + } else { + return nil, oauth.OAuth2Error{ + Code: oauth.InvalidRequest, + Description: "client_id must be a did:web", + } + } case responseTypeVPToken: // Options: // - OpenID4VP flow, vp_token is sent in Authorization Response @@ -283,19 +300,10 @@ func (r Wrapper) HandleAuthorizeRequest(ctx context.Context, request HandleAutho // OAuthAuthorizationServerMetadata returns the Authorization Server's metadata func (r Wrapper) OAuthAuthorizationServerMetadata(ctx context.Context, request OAuthAuthorizationServerMetadataRequestObject) (OAuthAuthorizationServerMetadataResponseObject, error) { - ownDID := r.idToDID(request.Id) - owned, err := r.vdr.IsOwner(ctx, ownDID) + _, err := r.idToOwnedDID(ctx, request.Id) if err != nil { - if resolver.IsFunctionalResolveError(err) { - return nil, core.NotFoundError("authz server metadata: %w", err) - } - log.Logger().WithField("did", ownDID.String()).Errorf("authz server metadata: failed to assert ownership of did: %s", err.Error()) - return nil, core.Error(500, "authz server metadata: %w", err) - } - if !owned { - return nil, core.NotFoundError("authz server metadata: did not owned") + return nil, err } - identity := r.auth.PublicURL().JoinPath("iam", request.Id) return OAuthAuthorizationServerMetadata200JSONResponse(authorizationServerMetadata(*identity)), nil @@ -317,15 +325,9 @@ func (r Wrapper) GetWebDID(_ context.Context, request GetWebDIDRequestObject) (G // OAuthClientMetadata returns the OAuth2 Client metadata for the request.Id if it is managed by this node. func (r Wrapper) OAuthClientMetadata(ctx context.Context, request OAuthClientMetadataRequestObject) (OAuthClientMetadataResponseObject, error) { - // TODO: must be web DID once web DID creation and DB are implemented - ownDID := idToNutsDID(request.Id) - owned, err := r.vdr.IsOwner(ctx, ownDID) + _, err := r.idToOwnedDID(ctx, request.Id) if err != nil { - log.Logger().WithField("did", ownDID.String()).Errorf("oauth metadata: failed to assert ownership of did: %s", err.Error()) - return nil, core.Error(500, err.Error()) - } - if !owned { - return nil, core.NotFoundError("did not owned") + return nil, err } identity := r.auth.PublicURL().JoinPath("iam", request.Id) @@ -365,17 +367,29 @@ func createSession(params map[string]string, ownDID did.DID) *Session { } return session } -func (r Wrapper) idToDID(id string) did.DID { - url := r.auth.PublicURL().JoinPath("iam", id) - did, _ := didweb.URLToDID(*url) - return *did -} -func idToNutsDID(id string) did.DID { - return did.DID{ - // should be changed to web when migrated to web DID - Method: "nuts", - ID: id, - DecodedID: id, +func (r Wrapper) idToOwnedDID(ctx context.Context, id string) (*did.DID, error) { + ownDID := r.idToDID(id) + + owned, err := r.vdr.IsOwner(ctx, ownDID) + if err != nil { + if resolver.IsFunctionalResolveError(err) { + return nil, core.NotFoundError("authz server metadata: %w", err) + } + log.Logger().WithField("did", ownDID.String()).Errorf("authz server metadata: failed to assert ownership of did: %s", err.Error()) + return nil, oauth.OAuth2Error{ + Code: oauth.ServerError, + Description: "failed to assert ownership of did", + } + } + if !owned { + return nil, core.NotFoundError("authz server metadata: did not owned") } + return &ownDID, nil +} + +func (r Wrapper) idToDID(id string) did.DID { + url := r.auth.PublicURL().JoinPath("iam", id) + webDID, _ := didweb.URLToDID(*url) + return *webDID } diff --git a/auth/api/iam/api_test.go b/auth/api/iam/api_test.go index 1c322b56fe..c661faa1e7 100644 --- a/auth/api/iam/api_test.go +++ b/auth/api/iam/api_test.go @@ -46,9 +46,9 @@ import ( "time" ) -var nutsDID = did.MustParseDID("did:nuts:123") var webDID = did.MustParseDID("did:web:example.com:iam:123") var webIDPart = "123" +var verifierDID = did.MustParseDID("did:web:example.com:iam:verifier") func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { t.Run("ok", func(t *testing.T) { @@ -92,14 +92,12 @@ func TestWrapper_OAuthAuthorizationServerMetadata(t *testing.T) { res, err := ctx.client.OAuthAuthorizationServerMetadata(nil, OAuthAuthorizationServerMetadataRequestObject{Id: webIDPart}) assert.Equal(t, 500, statusCodeFrom(err)) - assert.EqualError(t, err, "authz server metadata: unknown error") + assert.EqualError(t, err, "server_error - failed to assert ownership of did") assert.Nil(t, res) }) } func TestWrapper_GetWebDID(t *testing.T) { - webDID := did.MustParseDID("did:web:example.com:iam:123") - id := "123" ctx := audit.TestContext() expectedWebDIDDoc := did.Document{ ID: webDID, @@ -112,7 +110,7 @@ func TestWrapper_GetWebDID(t *testing.T) { test := newTestClient(t) test.vdr.EXPECT().ResolveManaged(webDID).Return(&expectedWebDIDDoc, nil) - response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{id}) + response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webIDPart}) assert.NoError(t, err) assert.Equal(t, expectedWebDIDDoc, did.Document(response.(GetWebDID200JSONResponse))) @@ -121,7 +119,7 @@ func TestWrapper_GetWebDID(t *testing.T) { test := newTestClient(t) test.vdr.EXPECT().ResolveManaged(webDID).Return(nil, resolver.ErrNotFound) - response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{id}) + response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webIDPart}) assert.NoError(t, err) assert.IsType(t, GetWebDID404Response{}, response) @@ -130,7 +128,7 @@ func TestWrapper_GetWebDID(t *testing.T) { test := newTestClient(t) test.vdr.EXPECT().ResolveManaged(webDID).Return(nil, errors.New("failed")) - response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{id}) + response, err := test.client.GetWebDID(ctx, GetWebDIDRequestObject{webIDPart}) assert.EqualError(t, err, "unable to resolve DID") assert.Nil(t, response) @@ -140,30 +138,30 @@ func TestWrapper_GetWebDID(t *testing.T) { func TestWrapper_GetOAuthClientMetadata(t *testing.T) { t.Run("ok", func(t *testing.T) { ctx := newTestClient(t) - ctx.vdr.EXPECT().IsOwner(nil, nutsDID).Return(true, nil) + ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(true, nil) - res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID}) + res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart}) require.NoError(t, err) assert.IsType(t, OAuthClientMetadata200JSONResponse{}, res) }) t.Run("error - did not managed by this node", func(t *testing.T) { ctx := newTestClient(t) - ctx.vdr.EXPECT().IsOwner(nil, nutsDID) + ctx.vdr.EXPECT().IsOwner(nil, webDID) - res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID}) + res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart}) assert.Equal(t, 404, statusCodeFrom(err)) assert.Nil(t, res) }) t.Run("error - internal error 500", func(t *testing.T) { ctx := newTestClient(t) - ctx.vdr.EXPECT().IsOwner(nil, nutsDID).Return(false, errors.New("unknown error")) + ctx.vdr.EXPECT().IsOwner(nil, webDID).Return(false, errors.New("unknown error")) - res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: nutsDID.ID}) + res, err := ctx.client.OAuthClientMetadata(nil, OAuthClientMetadataRequestObject{Id: webIDPart}) assert.Equal(t, 500, statusCodeFrom(err)) - assert.EqualError(t, err, "unknown error") + assert.EqualError(t, err, "server_error - failed to assert ownership of did") assert.Nil(t, res) }) } @@ -209,11 +207,41 @@ func TestWrapper_PresentationDefinition(t *testing.T) { } func TestWrapper_HandleAuthorizeRequest(t *testing.T) { + metadata := oauth.AuthorizationServerMetadata{ + AuthorizationEndpoint: "https://example.com/holder/authorize", + } + t.Run("ok - from holder", func(t *testing.T) { + ctx := newTestClient(t) + ctx.vdr.EXPECT().IsOwner(gomock.Any(), verifierDID).Return(true, nil) + ctx.relyingParty.EXPECT().AuthorizationServerMetadata(gomock.Any(), holderDID).Return(&metadata, nil) + + res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{ + clientIDParam: holderDID.String(), + redirectURIParam: "https://example.com", + responseTypeParam: "code", + scopeParam: "test", + }), HandleAuthorizeRequestRequestObject{ + Id: "verifier", + }) + + require.NoError(t, err) + assert.IsType(t, HandleAuthorizeRequest302Response{}, res) + location := res.(HandleAuthorizeRequest302Response).Headers.Location + assert.Contains(t, location, "https://example.com/holder/authorize") + assert.Contains(t, location, "client_id=did%3Aweb%3Aexample.com%3Aiam%3Averifier") + assert.Contains(t, location, "nonce=") + assert.Contains(t, location, "presentation_definition_uri=https%3A%2F%2Fexample.com%2Fiam%2Fverifier%2Fpresentation_definition%3Fscope%3Dtest") + assert.Contains(t, location, "redirect_uri=https%3A%2F%2Fexample.com%2Fiam%2Fverifier%2Fresponse") + assert.Contains(t, location, "response_mode=direct_post") + assert.Contains(t, location, "response_type=vp_token") + + }) t.Run("missing redirect_uri", func(t *testing.T) { ctx := newTestClient(t) + ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil) res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{}), HandleAuthorizeRequestRequestObject{ - Id: nutsDID.String(), + Id: webIDPart, }) requireOAuthError(t, err, oauth.InvalidRequest, "redirect_uri is required") @@ -221,12 +249,13 @@ func TestWrapper_HandleAuthorizeRequest(t *testing.T) { }) t.Run("unsupported response type", func(t *testing.T) { ctx := newTestClient(t) + ctx.vdr.EXPECT().IsOwner(gomock.Any(), webDID).Return(true, nil) res, err := ctx.client.HandleAuthorizeRequest(requestContext(map[string]string{ "redirect_uri": "https://example.com", "response_type": "unsupported", }), HandleAuthorizeRequestRequestObject{ - Id: nutsDID.String(), + Id: webIDPart, }) requireOAuthError(t, err, oauth.UnsupportedResponseType, "") @@ -239,7 +268,7 @@ func TestWrapper_HandleTokenRequest(t *testing.T) { ctx := newTestClient(t) res, err := ctx.client.HandleTokenRequest(nil, HandleTokenRequestRequestObject{ - Id: nutsDID.String(), + Id: webDID.String(), Body: &HandleTokenRequestFormdataRequestBody{ GrantType: "unsupported", }, diff --git a/auth/api/iam/openid4vp.go b/auth/api/iam/openid4vp.go index dfeb007d66..17fcff5d0e 100644 --- a/auth/api/iam/openid4vp.go +++ b/auth/api/iam/openid4vp.go @@ -30,19 +30,110 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/auth/oauth" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/storage" "github.com/nuts-foundation/nuts-node/vcr/credential" "github.com/nuts-foundation/nuts-node/vcr/holder" + "github.com/nuts-foundation/nuts-node/vdr/didweb" "net/http" "net/url" "strings" "time" ) -const sessionExpiry = 5 * time.Minute +const ( + oAuthFlowTimeout = time.Minute +) + +var oauthNonceKey = []string{"oauth", "nonce"} + +func (r Wrapper) handleAuthorizeRequestFromHolder(ctx context.Context, verifier did.DID, params map[string]string) (HandleAuthorizeRequestResponseObject, error) { + // we expect a generic OAuth2 request like this: + // GET /iam/123/authorize?response_type=token&client_id=did:web:example.com:iam:456&state=xyz + // &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 + // Host: server.com + // The following parameters are expected + // response_type, REQUIRED. Value MUST be set to "token". + // client_id, REQUIRED. This must be a did:web + // redirect_uri, OPTIONAL. This must be the client or other node url (client for regular flow, node for popup) + // scope, OPTIONAL. The scope that maps to a presentation definition, if not set we just want an empty VP + // state, RECOMMENDED. Opaque value used to maintain state between the request and the callback. + + // GET authorization server metadata for wallet + walletID, ok := params[clientIDParam] + if !ok { + return nil, oauthError(oauth.InvalidRequest, "missing client_id parameter") + } + // the walletDID must be a did:web + walletDID, err := did.ParseDID(walletID) + if err != nil || walletDID.Method != "web" { + return nil, oauthError(oauth.InvalidRequest, "invalid client_id parameter") + } + metadata, err := r.auth.RelyingParty().AuthorizationServerMetadata(ctx, *walletDID) + if err != nil { + return nil, oauthError(oauth.ServerError, "failed to get authorization server metadata (holder)") + } + // own generic endpoint + ownURL, err := didweb.DIDToURL(verifier) + if err != nil { + return nil, oauthError(oauth.ServerError, "failed to translate own did to URL") + } + // generate presentation_definition_uri based on own presentation_definition endpoint + scope + pdURL := ownURL.JoinPath("presentation_definition") + presentationDefinitionURI := AddQueryParams(*pdURL, map[string]string{ + "scope": params[scopeParam], + }) + + // redirect to wallet authorization endpoint, use direct_post mode + // like this: + // GET /authorize? + // response_type=vp_token + // &client_id=did:web:example.com:iam:123 + // &redirect_uri=https%3A%2F%2Fexample.com%2Fiam%2F123%2F%2Fresponse + // &presentation_definition_uri=... + // &response_mode=direct_post + // &nonce=n-0S6_WzA2Mj HTTP/1.1 + walletURL, err := url.Parse(metadata.AuthorizationEndpoint) + if err != nil || len(metadata.AuthorizationEndpoint) == 0 { + return nil, oauthError(oauth.InvalidRequest, "invalid authorization_endpoint (holder)") + } + nonce := crypto.GenerateNonce() + callbackURL := ownURL + callbackURL.Path, err = url.JoinPath(callbackURL.Path, "response") + if err != nil { + return nil, oauthError(oauth.ServerError, "failed to construct redirect path") + } + + redirectURL := AddQueryParams(*walletURL, map[string]string{ + responseTypeParam: responseTypeVPToken, + clientIDParam: verifier.String(), + redirectURIParam: callbackURL.String(), + presentationDefUriParam: presentationDefinitionURI.String(), + responseModeParam: responseModeDirectPost, + nonceParam: nonce, + }) + openid4vpRequest := Session{ + ClientID: verifier.String(), + Scope: params[scopeParam], + OwnDID: verifier, + ClientState: nonce, + RedirectURI: redirectURL.String(), + } + // use nonce to store authorization request in session store + if err = r.oauthNonceStore().Put(nonce, openid4vpRequest); err != nil { + return nil, oauthError(oauth.ServerError, "failed to store server state") + } + + return HandleAuthorizeRequest302Response{ + Headers: HandleAuthorizeRequest302ResponseHeaders{ + Location: redirectURL.String(), + }, + }, nil +} // createPresentationRequest creates a new Authorization Request as specified by OpenID4VP: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html. // It is sent by a verifier to a wallet, to request one or more verifiable credentials as verifiable presentation from the wallet. -func (r *Wrapper) sendPresentationRequest(ctx context.Context, response http.ResponseWriter, scope string, +func (r Wrapper) sendPresentationRequest(ctx context.Context, response http.ResponseWriter, scope string, redirectURL url.URL, verifierIdentifier url.URL, walletIdentifier url.URL) error { // TODO: Lookup wallet metadata for correct authorization endpoint. But for Nuts nodes, we derive it from the walletIdentifier authzEndpoint := walletIdentifier.JoinPath("/authorize") @@ -63,7 +154,7 @@ func (r *Wrapper) sendPresentationRequest(ctx context.Context, response http.Res // handlePresentationRequest handles an Authorization Request as specified by OpenID4VP: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html. // It is handled by a wallet, called by a verifier who wants the wallet to present one or more verifiable credentials. -func (r *Wrapper) handlePresentationRequest(params map[string]string, session *Session) (HandleAuthorizeRequestResponseObject, error) { +func (r Wrapper) handlePresentationRequest(params map[string]string, session *Session) (HandleAuthorizeRequestResponseObject, error) { ctx := context.TODO() // Presentation definition is always derived from the scope. // Later on, we might support presentation_definition and/or presentation_definition_uri parameters instead of scope as well. @@ -172,7 +263,7 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S } // handleAuthConsent handles the authorization consent form submission. -func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error { +func (r Wrapper) handlePresentationRequestAccept(c echo.Context) error { // TODO: Needs authentication? sessionID := c.FormValue("sessionID") if sessionID == "" { @@ -232,7 +323,7 @@ func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error { return c.Redirect(http.StatusFound, session.CreateRedirectURI(resultParams)) } -func (r *Wrapper) handlePresentationRequestCompleted(ctx echo.Context) error { +func (r Wrapper) handlePresentationRequestCompleted(ctx echo.Context) error { // TODO: response direct_post mode vpToken := ctx.QueryParams()[vpTokenParam] if len(vpToken) == 0 { @@ -260,6 +351,10 @@ func (r *Wrapper) handlePresentationRequestCompleted(ctx echo.Context) error { return ctx.HTML(http.StatusOK, buf.String()) } +func (r *Wrapper) oauthNonceStore() storage.SessionStore { + return r.storageEngine.GetSessionDatabase().GetStore(oAuthFlowTimeout, oauthNonceKey...) +} + func assertParamPresent(params map[string]string, param ...string) error { for _, curr := range param { if len(params[curr]) == 0 { @@ -277,3 +372,10 @@ func assertParamNotPresent(params map[string]string, param ...string) error { } return nil } + +func oauthError(code oauth.ErrorCode, description string) oauth.OAuth2Error { + return oauth.OAuth2Error{ + Code: code, + Description: description, + } +} diff --git a/auth/api/iam/openid4vp_demo.go b/auth/api/iam/openid4vp_demo.go index ddf14d2eae..8ee7290964 100644 --- a/auth/api/iam/openid4vp_demo.go +++ b/auth/api/iam/openid4vp_demo.go @@ -27,7 +27,7 @@ import ( "strings" ) -func (r *Wrapper) handleOpenID4VPDemoLanding(echoCtx echo.Context) error { +func (r Wrapper) handleOpenID4VPDemoLanding(echoCtx echo.Context) error { requestURL := *echoCtx.Request().URL requestURL.Host = echoCtx.Request().Host requestURL.Scheme = "http" @@ -47,7 +47,7 @@ func (r *Wrapper) handleOpenID4VPDemoLanding(echoCtx echo.Context) error { return echoCtx.HTML(http.StatusOK, buf.String()) } -func (r *Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error { +func (r Wrapper) handleOpenID4VPDemoSendRequest(echoCtx echo.Context) error { verifierID := echoCtx.FormValue("verifier_id") if verifierID == "" { return errors.New("missing verifier_id") diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index eb55988dfc..467b3e1648 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -43,6 +43,63 @@ import ( var holderDID = did.MustParseDID("did:web:example.com:holder") var issuerDID = did.MustParseDID("did:web:example.com:issuer") +func TestWrapper_handleAuthorizeRequestFromHolder(t *testing.T) { + defaultParams := func() map[string]string { + return map[string]string{ + clientIDParam: holderDID.String(), + redirectURIParam: "https://example.com", + responseTypeParam: "code", + scopeParam: "test", + } + } + + t.Run("missing client_id", func(t *testing.T) { + ctx := newTestClient(t) + params := defaultParams() + delete(params, clientIDParam) + + _, err := ctx.client.handleAuthorizeRequestFromHolder(context.Background(), verifierDID, params) + + requireOAuthError(t, err, oauth.InvalidRequest, "missing client_id parameter") + }) + t.Run("invalid client_id", func(t *testing.T) { + ctx := newTestClient(t) + params := defaultParams() + params[clientIDParam] = "did:nuts:1" + + _, err := ctx.client.handleAuthorizeRequestFromHolder(context.Background(), verifierDID, params) + + requireOAuthError(t, err, oauth.InvalidRequest, "invalid client_id parameter") + }) + t.Run("error on authorization server metadata", func(t *testing.T) { + ctx := newTestClient(t) + ctx.relyingParty.EXPECT().AuthorizationServerMetadata(gomock.Any(), holderDID).Return(nil, assert.AnError) + + _, err := ctx.client.handleAuthorizeRequestFromHolder(context.Background(), verifierDID, defaultParams()) + + requireOAuthError(t, err, oauth.ServerError, "failed to get authorization server metadata (holder)") + }) + t.Run("failed to generate verifier web url", func(t *testing.T) { + ctx := newTestClient(t) + verifierDID := did.MustParseDID("did:notweb:example.com:verifier") + ctx.relyingParty.EXPECT().AuthorizationServerMetadata(gomock.Any(), holderDID).Return(&oauth.AuthorizationServerMetadata{}, nil) + + _, err := ctx.client.handleAuthorizeRequestFromHolder(context.Background(), verifierDID, defaultParams()) + + requireOAuthError(t, err, oauth.ServerError, "failed to translate own did to URL") + }) + t.Run("incorrect holder AuthorizationEndpoint URL", func(t *testing.T) { + ctx := newTestClient(t) + ctx.relyingParty.EXPECT().AuthorizationServerMetadata(gomock.Any(), holderDID).Return(&oauth.AuthorizationServerMetadata{ + AuthorizationEndpoint: "://example.com", + }, nil) + + _, err := ctx.client.handleAuthorizeRequestFromHolder(context.Background(), verifierDID, defaultParams()) + + requireOAuthError(t, err, oauth.InvalidRequest, "invalid authorization_endpoint (holder)") + }) +} + func TestWrapper_sendPresentationRequest(t *testing.T) { instance := New(nil, nil, nil, nil) diff --git a/auth/api/iam/s2s_vptoken.go b/auth/api/iam/s2s_vptoken.go index 4fb1bd6807..72f378f06b 100644 --- a/auth/api/iam/s2s_vptoken.go +++ b/auth/api/iam/s2s_vptoken.go @@ -24,10 +24,8 @@ import ( "fmt" "github.com/nuts-foundation/nuts-node/auth/oauth" "github.com/nuts-foundation/nuts-node/crypto" - "net/http" "time" - "github.com/labstack/echo/v4" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/core" @@ -40,42 +38,6 @@ import ( // TODO: Might want to make this configurable at some point const accessTokenValidity = 15 * time.Minute -// serviceToService adds support for service-to-service OAuth2 flows, -// which uses a custom vp_token grant to authenticate calls to the token endpoint. -// Clients first call the presentation definition endpoint to get a presentation definition for the desired scope, -// then create a presentation submission given the definition which is posted to the token endpoint as vp_token. -// The AS then returns an access token with the requested scope. -// Requires: -// - GET /presentation_definition?scope=... (returns a presentation definition) -// - POST /token (with vp_token grant) -type serviceToService struct { -} - -func (s serviceToService) Routes(router core.EchoRouter) { - router.Add("GET", "/public/oauth2/:did/presentation_definition", func(echoCtx echo.Context) error { - // TODO: Read scope, map to presentation definition, return - return echoCtx.JSON(http.StatusOK, map[string]string{}) - }) -} - -func (s serviceToService) validateVPToken(params map[string]string) (string, error) { - submission := params["presentation_submission"] - scope := params["scope"] - vp_token := params["vp_token"] - if submission == "" || scope == "" || vp_token == "" { - // TODO: right error response - return "", errors.New("missing required parameters") - } - // TODO: https://github.com/nuts-foundation/nuts-node/issues/2418 - // TODO: verify parameters - return scope, nil -} - -func (s serviceToService) handleAuthzRequest(_ map[string]string, _ *Session) (*authzResponse, error) { - // Protocol does not support authorization code flow - return nil, nil -} - func (r Wrapper) RequestAccessToken(ctx context.Context, request RequestAccessTokenRequestObject) (RequestAccessTokenResponseObject, error) { if request.Body == nil { // why did oapi-codegen generate a pointer for the body?? diff --git a/auth/api/iam/types.go b/auth/api/iam/types.go index a0f9e48897..f03026d371 100644 --- a/auth/api/iam/types.go +++ b/auth/api/iam/types.go @@ -24,6 +24,7 @@ import ( "github.com/nuts-foundation/nuts-node/auth/oauth" "github.com/nuts-foundation/nuts-node/vcr/pe" "github.com/nuts-foundation/nuts-node/vdr/resolver" + "time" ) // DIDDocument is an alias @@ -47,6 +48,10 @@ type TokenResponse = oauth.TokenResponse // OAuthAuthorizationServerMetadata is an alias type OAuthAuthorizationServerMetadata = oauth.AuthorizationServerMetadata +const ( + sessionExpiry = 5 * time.Minute +) + const ( // responseTypeParam is the name of the response_type parameter. // Specified by https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.1 @@ -69,6 +74,7 @@ const ( // responseTypeVPIDToken is defined in the OpenID4VP flow that combines its vp_token with SIOPv2's id_token // https://openid.bitbucket.io/connect/openid-4-verifiable-presentations-1_0.html#appendix-B responseTypeVPIDToken = "vp_token id_token" + nonceParam = "nonce" ) var responseTypesSupported = []string{responseTypeCode, responseTypeVPToken, responseTypeVPIDToken} diff --git a/auth/services/oauth/interface.go b/auth/services/oauth/interface.go index f393a2c4f2..70cfe707be 100644 --- a/auth/services/oauth/interface.go +++ b/auth/services/oauth/interface.go @@ -28,6 +28,9 @@ import ( // RelyingParty implements the OAuth2 relying party role. type RelyingParty interface { + // AuthorizationServerMetadata returns the metadata of the remote authorization server. + AuthorizationServerMetadata(ctx context.Context, webdid did.DID) (*oauth.AuthorizationServerMetadata, error) + CreateJwtGrant(ctx context.Context, request services.CreateJwtGrantRequest) (*services.JwtBearerTokenResult, error) // RequestRFC003AccessToken is called by the local EHR node to request an access token from a remote Nuts node using Nuts RFC003. diff --git a/auth/services/oauth/mock.go b/auth/services/oauth/mock.go index 8db473762a..1463612dfa 100644 --- a/auth/services/oauth/mock.go +++ b/auth/services/oauth/mock.go @@ -42,6 +42,21 @@ func (m *MockRelyingParty) EXPECT() *MockRelyingPartyMockRecorder { return m.recorder } +// AuthorizationServerMetadata mocks base method. +func (m *MockRelyingParty) AuthorizationServerMetadata(ctx context.Context, webdid did.DID) (*oauth.AuthorizationServerMetadata, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AuthorizationServerMetadata", ctx, webdid) + ret0, _ := ret[0].(*oauth.AuthorizationServerMetadata) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AuthorizationServerMetadata indicates an expected call of AuthorizationServerMetadata. +func (mr *MockRelyingPartyMockRecorder) AuthorizationServerMetadata(ctx, webdid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationServerMetadata", reflect.TypeOf((*MockRelyingParty)(nil).AuthorizationServerMetadata), ctx, webdid) +} + // CreateJwtGrant mocks base method. func (m *MockRelyingParty) CreateJwtGrant(ctx context.Context, request services.CreateJwtGrantRequest) (*services.JwtBearerTokenResult, error) { m.ctrl.T.Helper() diff --git a/auth/services/oauth/relying_party.go b/auth/services/oauth/relying_party.go index 38689e407d..13999529b4 100644 --- a/auth/services/oauth/relying_party.go +++ b/auth/services/oauth/relying_party.go @@ -133,9 +133,9 @@ func (s *relyingParty) RequestRFC003AccessToken(ctx context.Context, jwtGrantTok func (s *relyingParty) RequestRFC021AccessToken(ctx context.Context, requester did.DID, verifier did.DID, scopes string) (*oauth.TokenResponse, error) { iamClient := iam.NewHTTPClient(s.strictMode, s.httpClientTimeout, s.httpClientTLS) - metadata, err := iamClient.OAuthAuthorizationServerMetadata(ctx, verifier) + metadata, err := s.AuthorizationServerMetadata(ctx, verifier) if err != nil { - return nil, fmt.Errorf("failed to retrieve remote OAuth Authorization Server metadata: %w", err) + return nil, err } // get the presentation definition from the verifier @@ -193,6 +193,15 @@ func (s *relyingParty) RequestRFC021AccessToken(ctx context.Context, requester d }, nil } +func (s *relyingParty) AuthorizationServerMetadata(ctx context.Context, webdid did.DID) (*oauth.AuthorizationServerMetadata, error) { + iamClient := iam.NewHTTPClient(s.strictMode, s.httpClientTimeout, s.httpClientTLS) + metadata, err := iamClient.OAuthAuthorizationServerMetadata(ctx, webdid) + if err != nil { + return nil, fmt.Errorf("failed to retrieve remote OAuth Authorization Server metadata: %w", err) + } + return metadata, nil +} + func determineFormat(formats map[string]map[string][]string) (string, error) { for format := range formats { switch format { diff --git a/auth/services/oauth/relying_party_test.go b/auth/services/oauth/relying_party_test.go index 7b57d2635e..50a6aaa7f4 100644 --- a/auth/services/oauth/relying_party_test.go +++ b/auth/services/oauth/relying_party_test.go @@ -371,6 +371,27 @@ func TestService_CreateJwtBearerToken(t *testing.T) { }) } +func TestRelyingParty_AuthorizationServerMetadata(t *testing.T) { + t.Run("ok", func(t *testing.T) { + ctx := createOAuthRPContext(t) + + metadata, err := ctx.relyingParty.AuthorizationServerMetadata(context.Background(), ctx.verifierDID) + + require.NoError(t, err) + require.NotNil(t, metadata) + assert.Equal(t, ctx.authzServerMetadata, *metadata) + }) + t.Run("error - failed to get metadata", func(t *testing.T) { + ctx := createOAuthRPContext(t) + ctx.metadata = nil + + _, err := ctx.relyingParty.AuthorizationServerMetadata(context.Background(), ctx.verifierDID) + + require.Error(t, err) + assert.EqualError(t, err, "failed to retrieve remote OAuth Authorization Server metadata: server returned HTTP 404 (expected: 200)") + }) +} + type rpTestContext struct { ctrl *gomock.Controller keyStore *crypto.MockKeyStore diff --git a/e2e-tests/oauth-flow/rfc021/docker-compose.yml b/e2e-tests/oauth-flow/rfc021/docker-compose.yml index 57dae33a7d..246d310028 100644 --- a/e2e-tests/oauth-flow/rfc021/docker-compose.yml +++ b/e2e-tests/oauth-flow/rfc021/docker-compose.yml @@ -11,6 +11,7 @@ services: - "./node-A/data:/opt/nuts/data:rw" - "../../tls-certs/nodeA-backend-certificate.pem:/opt/nuts/certificate-and-key.pem:ro" - "../../tls-certs/truststore.pem:/opt/nuts/truststore.pem:ro" + - "../../tls-certs/truststore.pem:/etc/ssl/certs/truststore.pem:ro" - "./node-A/presentationexchangemapping.json:/opt/nuts/presentationexchangemapping.json:ro" healthcheck: interval: 1s # Make test run quicker by checking health status more often diff --git a/golden_hammer/module_test.go b/golden_hammer/module_test.go index 45c8a975f3..e6033532c0 100644 --- a/golden_hammer/module_test.go +++ b/golden_hammer/module_test.go @@ -30,7 +30,6 @@ import ( "github.com/nuts-foundation/nuts-node/test/pki" "github.com/nuts-foundation/nuts-node/vcr/openid4vci" "github.com/nuts-foundation/nuts-node/vdr" - "github.com/nuts-foundation/nuts-node/vdr/didnuts/didstore" "github.com/nuts-foundation/nuts-node/vdr/resolver" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -108,59 +107,51 @@ func TestGoldenHammer_Fix(t *testing.T) { t.Run("nothing to fix", func(t *testing.T) { // vendor and care organization DIDs already have the required service, so there's nothing to fix - ctrl := gomock.NewController(t) - didResolver := resolver.NewMockDIDResolver(ctrl) - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil).MinTimes(1) - didResolver.EXPECT().Resolve(clientADID, gomock.Any()).Return(&clientDocumentWithBaseURL, nil, nil).MinTimes(1) - didResolver.EXPECT().Resolve(clientBDID, gomock.Any()).Return(&clientDocumentWithBaseURL, nil, nil).MinTimes(1) - vdr := vdr.NewMockVDR(ctrl) - vdr.EXPECT().Resolver().Return(didResolver).MinTimes(1) - vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID, clientADID, clientBDID}, nil) - service := New(vdr, nil) + ctx := newMockContext(t) + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil).MinTimes(1) + ctx.didResolver.EXPECT().Resolve(clientADID, gomock.Any()).Return(&clientDocumentWithBaseURL, nil, nil).MinTimes(1) + ctx.didResolver.EXPECT().Resolve(clientBDID, gomock.Any()).Return(&clientDocumentWithBaseURL, nil, nil).MinTimes(1) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID, clientADID, clientBDID}, nil) - err := service.registerServiceBaseURLs() + err := ctx.hammer.registerServiceBaseURLs() assert.NoError(t, err) t.Run("second time list of fixed DIDs is cached (no DID resolving)", func(t *testing.T) { - vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID, clientADID, clientBDID}, nil) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID, clientADID, clientBDID}, nil) - err := service.registerServiceBaseURLs() + err := ctx.hammer.registerServiceBaseURLs() assert.NoError(t, err) }) }) t.Run("to be registered on vendor DID and client DIDs", func(t *testing.T) { - ctrl := gomock.NewController(t) - didResolver := didstore.NewMockStore(ctrl) + ctx := newMockContext(t) docClientA := clientDocumentWithoutBaseURL docClientA.ID = clientADID docClientB := clientDocumentWithoutBaseURL docClientB.ID = clientBDID - vdr := vdr.NewMockVDR(ctrl) - vdr.EXPECT().Resolver().Return(didResolver).MinTimes(1) // Order DIDs such that care organization DID is first, to test ordering - vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{clientADID, vendorDID, clientBDID}, nil) - didmanAPI := didman.NewMockDidman(ctrl) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{clientADID, vendorDID, clientBDID}, nil) gomock.InOrder( // DID documents are listed first to check if they should be fixed - didResolver.EXPECT().Resolve(clientADID, gomock.Any()).Return(&docClientA, nil, nil), - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithoutBaseURL, nil, nil), - didResolver.EXPECT().Resolve(clientBDID, gomock.Any()).Return(&docClientB, nil, nil), + ctx.didResolver.EXPECT().Resolve(clientADID, gomock.Any()).Return(&docClientA, nil, nil), + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithoutBaseURL, nil, nil), + ctx.didResolver.EXPECT().Resolve(clientBDID, gomock.Any()).Return(&docClientB, nil, nil), // Vendor document is fixed first - didmanAPI.EXPECT().AddEndpoint(gomock.Any(), vendorDID, resolver.BaseURLServiceType, *expectedBaseURL).Return(nil, nil), + ctx.didmanAPI.EXPECT().AddEndpoint(gomock.Any(), vendorDID, resolver.BaseURLServiceType, *expectedBaseURL).Return(nil, nil), // Then client A - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil), - didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientADID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil), + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil), + ctx.didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientADID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil), // Then client B - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil), - didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientBDID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil), + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil), + ctx.didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientBDID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil), ) - service := New(vdr, didmanAPI) + service := ctx.hammer service.tlsConfig = tlsServer.TLS service.tlsConfig.InsecureSkipVerify = true service.tlsConfig.Certificates = []tls.Certificate{pki.Certificate()} @@ -170,13 +161,10 @@ func TestGoldenHammer_Fix(t *testing.T) { assert.NoError(t, err) }) t.Run("vendor identifier can't be resolved from TLS", func(t *testing.T) { - ctrl := gomock.NewController(t) - didResolver := didstore.NewMockStore(ctrl) - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithoutBaseURL, nil, nil).MinTimes(1) - vdr := vdr.NewMockVDR(ctrl) - vdr.EXPECT().Resolver().Return(didResolver).MinTimes(1) - vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID}, nil) - service := New(vdr, nil) + ctx := newMockContext(t) + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithoutBaseURL, nil, nil).MinTimes(1) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID}, nil) + service := ctx.hammer service.tlsConfig = &tls.Config{ Certificates: []tls.Certificate{pki.Certificate()}, } @@ -188,26 +176,22 @@ func TestGoldenHammer_Fix(t *testing.T) { t.Run("to be registered on client DIDs", func(t *testing.T) { // vendor DID document already contains the service, but its care organization DID documents not yet, // so they need to be registered. - ctrl := gomock.NewController(t) - didResolver := didstore.NewMockStore(ctrl) - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil).MinTimes(1) + ctx := newMockContext(t) + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(&vendorDocumentWithBaseURL, nil, nil).MinTimes(1) docClientA := clientDocumentWithoutBaseURL docClientA.ID = clientADID docClientB := clientDocumentWithoutBaseURL docClientB.ID = clientBDID - didResolver.EXPECT().Resolve(clientADID, gomock.Any()).Return(&docClientA, nil, nil).MinTimes(1) - didResolver.EXPECT().Resolve(clientBDID, gomock.Any()).Return(&docClientB, nil, nil).MinTimes(1) + ctx.didResolver.EXPECT().Resolve(clientADID, gomock.Any()).Return(&docClientA, nil, nil).MinTimes(1) + ctx.didResolver.EXPECT().Resolve(clientBDID, gomock.Any()).Return(&docClientB, nil, nil).MinTimes(1) // Client C is owned, but not linked to the vendor (via NutsComm service), so do not register the service on that one - didResolver.EXPECT().Resolve(clientCDID, gomock.Any()).Return(&did.Document{ID: clientCDID}, nil, nil).MinTimes(1) - vdr := vdr.NewMockVDR(ctrl) - vdr.EXPECT().Resolver().Return(didResolver).MinTimes(1) - vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID, clientADID, clientBDID, clientCDID}, nil) - didmanAPI := didman.NewMockDidman(ctrl) + ctx.didResolver.EXPECT().Resolve(clientCDID, gomock.Any()).Return(&did.Document{ID: clientCDID}, nil, nil).MinTimes(1) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID, clientADID, clientBDID, clientCDID}, nil) // AddEndpoint is not called for vendor DID (URL already present), but for client DIDs. // Not for clientC, since it's not linked to the vendor (doesn't have a NutsComm endpoint). - didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientADID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil) - didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientBDID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil) - service := New(vdr, didmanAPI) + ctx.didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientADID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil) + ctx.didmanAPI.EXPECT().AddEndpoint(gomock.Any(), clientBDID, resolver.BaseURLServiceType, *serviceRef).Return(nil, nil) + service := ctx.hammer service.tlsConfig = tlsServer.TLS service.tlsConfig.InsecureSkipVerify = true service.tlsConfig.Certificates = []tls.Certificate{pki.Certificate()} @@ -217,13 +201,10 @@ func TestGoldenHammer_Fix(t *testing.T) { assert.NoError(t, err) }) t.Run("resolve error", func(t *testing.T) { - ctrl := gomock.NewController(t) - didResolver := resolver.NewMockDIDResolver(ctrl) - didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(nil, nil, fmt.Errorf("resolve error")) - vdr := vdr.NewMockVDR(ctrl) - vdr.EXPECT().Resolver().Return(didResolver) - vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID}, nil) - service := New(vdr, nil) + ctx := newMockContext(t) + ctx.didResolver.EXPECT().Resolve(vendorDID, gomock.Any()).Return(nil, nil, fmt.Errorf("resolve error")) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).Return([]did.DID{vendorDID}, nil) + service := ctx.hammer service.tlsConfig = &tls.Config{ Certificates: []tls.Certificate{pki.Certificate()}, } @@ -240,13 +221,12 @@ func TestGoldenHammer_Lifecycle(t *testing.T) { defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) fixCalled := &atomic.Int64{} - ctrl := gomock.NewController(t) - vdr := vdr.NewMockVDR(ctrl) - vdr.EXPECT().ListOwned(gomock.Any()).DoAndReturn(func(_ context.Context) ([]did.DID, error) { + ctx := newMockContext(t) + ctx.vdr.EXPECT().ListOwned(gomock.Any()).DoAndReturn(func(_ context.Context) ([]did.DID, error) { fixCalled.Add(1) return []did.DID{}, nil }).MinTimes(1) - service := New(vdr, nil) + service := ctx.hammer service.config.Interval = time.Millisecond service.config.Enabled = true @@ -271,6 +251,29 @@ func TestGoldenHammer_Lifecycle(t *testing.T) { }) } +type mockContext struct { + ctrl *gomock.Controller + didmanAPI *didman.MockDidman + didResolver *resolver.MockDIDResolver + hammer *GoldenHammer + vdr *vdr.MockVDR +} + +func newMockContext(t *testing.T) mockContext { + ctrl := gomock.NewController(t) + mockVdr := vdr.NewMockVDR(ctrl) + mockDidmanAPI := didman.NewMockDidman(ctrl) + didResolver := resolver.NewMockDIDResolver(ctrl) + mockVdr.EXPECT().Resolver().Return(didResolver).AnyTimes() + + return mockContext{ + ctrl: ctrl, + didmanAPI: mockDidmanAPI, + didResolver: didResolver, + hammer: New(mockVdr, mockDidmanAPI), + vdr: mockVdr, + } +} func TestGoldenHammer_Name(t *testing.T) { service := New(nil, nil) diff --git a/vdr/management/management_mock.go b/vdr/management/management_mock.go index 6ae0dbd41f..f3d5f10cc2 100644 --- a/vdr/management/management_mock.go +++ b/vdr/management/management_mock.go @@ -14,138 +14,9 @@ import ( did "github.com/nuts-foundation/go-did/did" crypto "github.com/nuts-foundation/nuts-node/crypto" - resolver "github.com/nuts-foundation/nuts-node/vdr/resolver" gomock "go.uber.org/mock/gomock" ) -// MockManager is a mock of Manager interface. -type MockManager struct { - ctrl *gomock.Controller - recorder *MockManagerMockRecorder -} - -// MockManagerMockRecorder is the mock recorder for MockManager. -type MockManagerMockRecorder struct { - mock *MockManager -} - -// NewMockManager creates a new mock instance. -func NewMockManager(ctrl *gomock.Controller) *MockManager { - mock := &MockManager{ctrl: ctrl} - mock.recorder = &MockManagerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockManager) EXPECT() *MockManagerMockRecorder { - return m.recorder -} - -// AddVerificationMethod mocks base method. -func (m *MockManager) AddVerificationMethod(ctx context.Context, id did.DID, keyUsage DIDKeyFlags) (*did.VerificationMethod, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddVerificationMethod", ctx, id, keyUsage) - ret0, _ := ret[0].(*did.VerificationMethod) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// AddVerificationMethod indicates an expected call of AddVerificationMethod. -func (mr *MockManagerMockRecorder) AddVerificationMethod(ctx, id, keyUsage any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddVerificationMethod", reflect.TypeOf((*MockManager)(nil).AddVerificationMethod), ctx, id, keyUsage) -} - -// Create mocks base method. -func (m *MockManager) Create(ctx context.Context, method string, options DIDCreationOptions) (*did.Document, crypto.Key, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, method, options) - ret0, _ := ret[0].(*did.Document) - ret1, _ := ret[1].(crypto.Key) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// Create indicates an expected call of Create. -func (mr *MockManagerMockRecorder) Create(ctx, method, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), ctx, method, options) -} - -// Deactivate mocks base method. -func (m *MockManager) Deactivate(ctx context.Context, id did.DID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Deactivate", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// Deactivate indicates an expected call of Deactivate. -func (mr *MockManagerMockRecorder) Deactivate(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deactivate", reflect.TypeOf((*MockManager)(nil).Deactivate), ctx, id) -} - -// IsOwner mocks base method. -func (m *MockManager) IsOwner(arg0 context.Context, arg1 did.DID) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsOwner", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsOwner indicates an expected call of IsOwner. -func (mr *MockManagerMockRecorder) IsOwner(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsOwner", reflect.TypeOf((*MockManager)(nil).IsOwner), arg0, arg1) -} - -// ListOwned mocks base method. -func (m *MockManager) ListOwned(ctx context.Context) ([]did.DID, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListOwned", ctx) - ret0, _ := ret[0].([]did.DID) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListOwned indicates an expected call of ListOwned. -func (mr *MockManagerMockRecorder) ListOwned(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOwned", reflect.TypeOf((*MockManager)(nil).ListOwned), ctx) -} - -// RemoveVerificationMethod mocks base method. -func (m *MockManager) RemoveVerificationMethod(ctx context.Context, id did.DID, keyID did.DIDURL) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveVerificationMethod", ctx, id, keyID) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveVerificationMethod indicates an expected call of RemoveVerificationMethod. -func (mr *MockManagerMockRecorder) RemoveVerificationMethod(ctx, id, keyID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveVerificationMethod", reflect.TypeOf((*MockManager)(nil).RemoveVerificationMethod), ctx, id, keyID) -} - -// Resolve mocks base method. -func (m *MockManager) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Resolve", id, metadata) - ret0, _ := ret[0].(*did.Document) - ret1, _ := ret[1].(*resolver.DocumentMetadata) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 -} - -// Resolve indicates an expected call of Resolve. -func (mr *MockManagerMockRecorder) Resolve(id, metadata any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Resolve", reflect.TypeOf((*MockManager)(nil).Resolve), id, metadata) -} - // MockDocCreator is a mock of DocCreator interface. type MockDocCreator struct { ctrl *gomock.Controller @@ -170,9 +41,9 @@ func (m *MockDocCreator) EXPECT() *MockDocCreatorMockRecorder { } // Create mocks base method. -func (m *MockDocCreator) Create(ctx context.Context, method string, options DIDCreationOptions) (*did.Document, crypto.Key, error) { +func (m *MockDocCreator) Create(ctx context.Context, options DIDCreationOptions) (*did.Document, crypto.Key, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", ctx, method, options) + ret := m.ctrl.Call(m, "Create", ctx, options) ret0, _ := ret[0].(*did.Document) ret1, _ := ret[1].(crypto.Key) ret2, _ := ret[2].(error) @@ -180,9 +51,9 @@ func (m *MockDocCreator) Create(ctx context.Context, method string, options DIDC } // Create indicates an expected call of Create. -func (mr *MockDocCreatorMockRecorder) Create(ctx, method, options any) *gomock.Call { +func (mr *MockDocCreatorMockRecorder) Create(ctx, options any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDocCreator)(nil).Create), ctx, method, options) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockDocCreator)(nil).Create), ctx, options) } // MockDocUpdater is a mock of DocUpdater interface.