From 71c652504cc86b53705951ba8dd286dac9f2e4b9 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw Date: Thu, 22 Apr 2021 13:45:48 +0200 Subject: [PATCH 1/3] standardize API returns --- auth/api/v1/api.go | 102 +++++++++++++++++++++++++++----------- auth/api/v1/api_test.go | 87 ++++++++++++++++---------------- auth/api/v1/generated.go | 59 +++------------------- crypto/api/v1/api.go | 3 +- docs/_static/auth/v1.yaml | 95 +++++++++++++++++++++++------------ go.sum | 4 -- 6 files changed, 187 insertions(+), 163 deletions(-) diff --git a/auth/api/v1/api.go b/auth/api/v1/api.go index 4a03ec9f10..7447c13c38 100644 --- a/auth/api/v1/api.go +++ b/auth/api/v1/api.go @@ -22,9 +22,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/nuts-foundation/go-did/did" - "github.com/nuts-foundation/nuts-node/auth/logging" - "github.com/nuts-foundation/nuts-node/core" "net/http" "regexp" "strings" @@ -32,17 +29,29 @@ import ( "github.com/labstack/echo/v4" + "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/nuts-node/auth" "github.com/nuts-foundation/nuts-node/auth/contract" + "github.com/nuts-foundation/nuts-node/auth/logging" "github.com/nuts-foundation/nuts-node/auth/services" + "github.com/nuts-foundation/nuts-node/core" ) var _ ServerInterface = (*Wrapper)(nil) -const errOauthInvalidRequest = "invalid_request" -const errOauthInvalidGrant = "invalid_grant" -const errOauthUnsupportedGrant = "unsupported_grant_type" -const bearerTokenHeaderPrefix = "bearer " +const ( + errOauthInvalidRequest = "invalid_request" + errOauthInvalidGrant = "invalid_grant" + errOauthUnsupportedGrant = "unsupported_grant_type" + bearerTokenHeaderPrefix = "bearer " + + problemTitleVerifySignature = "signature verification failed" + problemTitleCreateSignSession = "sign session creation failed" + problemTitleCreateJwtBearerToken = "token creation failed" + problemTitleDrawUpContract = "contract creation failed" + problemTitleGetContract = "could not get contract" + problemTitleSignSessionStatus = "could not get session status" +) // Wrapper bridges the generated api types and http logic to the internal types and logic. // It checks required parameters and message body. It converts data from api to internal types. @@ -62,23 +71,31 @@ func (w *Wrapper) Routes(router core.EchoRouter) { func (w Wrapper) VerifySignature(ctx echo.Context) error { requestParams := new(SignatureVerificationRequest) if err := ctx.Bind(requestParams); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not parse request body: %s", err.Error())) + err = fmt.Errorf("could not parse request body: %w", err) + logging.Log().WithError(err).Warn() + return core.NewProblem(problemTitleVerifySignature, http.StatusBadRequest, err.Error()) } rawVP, err := json.Marshal(requestParams.VerifiablePresentation) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("unable to convert the verifiable presentation: %s", err.Error())) + err = fmt.Errorf("unable to convert the verifiable presentation: %w", err) + logging.Log().WithError(err).Error(problemTitleVerifySignature) + return core.NewProblem(problemTitleVerifySignature, http.StatusInternalServerError, err.Error()) } checkTime := time.Now() if requestParams.CheckTime != nil { checkTime, err = time.Parse(time.RFC3339, *requestParams.CheckTime) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not parse checkTime: %s", err.Error())) + err = fmt.Errorf("could not parse checkTime: %w", err) + logging.Log().WithError(err).Warn(problemTitleVerifySignature) + return core.NewProblem(problemTitleVerifySignature, http.StatusBadRequest, err.Error()) } } validationResult, err := w.Auth.ContractClient().VerifyVP(rawVP, &checkTime) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("unable to verify the verifiable presentation: %s", err.Error())) + err = fmt.Errorf("unable to verify the verifiable presentation: %w", err) + logging.Log().WithError(err).Warn(problemTitleVerifySignature) + return core.NewProblem(problemTitleVerifySignature, http.StatusBadRequest, err.Error()) } // Convert internal validationResult to api SignatureVerificationResponse response := SignatureVerificationResponse{} @@ -109,7 +126,9 @@ func (w Wrapper) VerifySignature(ctx echo.Context) error { func (w Wrapper) CreateSignSession(ctx echo.Context) error { requestParams := new(CreateSignSessionRequest) if err := ctx.Bind(requestParams); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not parse request body: %s", err.Error())) + err = fmt.Errorf("could not parse request body: %w", err) + logging.Log().WithError(err).Warn(problemTitleCreateSignSession) + return core.NewProblem(problemTitleCreateSignSession, http.StatusBadRequest, err.Error()) } createSessionRequest := services.CreateSessionRequest{ SigningMeans: contract.SigningMeans(requestParams.Means), @@ -117,13 +136,17 @@ func (w Wrapper) CreateSignSession(ctx echo.Context) error { } sessionPtr, err := w.Auth.ContractClient().CreateSigningSession(createSessionRequest) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("unable to create sign challenge: %s", err.Error())) + err = fmt.Errorf("unable to create sign challenge: %w", err) + logging.Log().WithError(err).Warn(problemTitleCreateSignSession) + return core.NewProblem(problemTitleCreateSignSession, http.StatusBadRequest, err.Error()) } var keyValPointer map[string]interface{} err = convertToMap(sessionPtr, &keyValPointer) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("unable to build sessionPointer: %s", err.Error())) + err = fmt.Errorf("unable to build sessionPointer: %w", err) + logging.Log().WithError(err).Warn(problemTitleCreateSignSession) + return core.NewProblem(problemTitleCreateSignSession, http.StatusBadRequest, err.Error()) } response := CreateSignSessionResponse{ @@ -138,21 +161,27 @@ func (w Wrapper) CreateSignSession(ctx echo.Context) error { func (w Wrapper) GetSignSessionStatus(ctx echo.Context, sessionID string) error { sessionStatus, err := w.Auth.ContractClient().SigningSessionStatus(sessionID) if err != nil { + err = fmt.Errorf("failed to get session status for %s, reason: %w", sessionID, err) + logging.Log().WithError(err).Error(problemTitleSignSessionStatus) if errors.Is(err, services.ErrSessionNotFound) { - return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("no active signing session for sessionID: '%s' found", sessionID)) + return core.NewProblem(problemTitleSignSessionStatus, http.StatusNotFound, err.Error()) } - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("unable to retrieve a session status: %s", err.Error())) + return core.NewProblem(problemTitleSignSessionStatus, http.StatusInternalServerError, err.Error()) } vp, err := sessionStatus.VerifiablePresentation() if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("error while building verifiable presentation: %s", err.Error())) + err = fmt.Errorf("error while building verifiable presentation: %w", err) + logging.Log().WithError(err).Error(problemTitleSignSessionStatus) + return core.NewProblem(problemTitleSignSessionStatus, http.StatusInternalServerError, err.Error()) } var apiVp *VerifiablePresentation if vp != nil { apiVp = &VerifiablePresentation{} err = convertToMap(vp, apiVp) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("unable to convert verifiable presentation: %s", err.Error())) + err = fmt.Errorf("unable to convert verifiable presentation: %w", err) + logging.Log().WithError(err).Error(problemTitleSignSessionStatus) + return core.NewProblem(problemTitleSignSessionStatus, http.StatusInternalServerError, err.Error()) } } response := GetSignSessionStatusResponse{Status: sessionStatus.Status(), VerifiablePresentation: apiVp} @@ -177,7 +206,9 @@ func (w Wrapper) GetContractByType(ctx echo.Context, contractType string, params // get contract authContract := contract.StandardContractTemplates.Get(contract.Type(contractType), contractLanguage, contractVersion) if authContract == nil { - return echo.NewHTTPError(http.StatusNotFound, "could not found contract template") + err := errors.New("could not find contract template") + logging.Log().WithError(err).Error(problemTitleGetContract) + return core.NewProblem(problemTitleGetContract, http.StatusNotFound, err.Error()) } // convert internal data types to generated api types @@ -196,7 +227,9 @@ func (w Wrapper) GetContractByType(ctx echo.Context, contractType string, params func (w Wrapper) DrawUpContract(ctx echo.Context) error { params := new(DrawUpContractRequest) if err := ctx.Bind(params); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not parse request body: %s", err.Error())) + err = fmt.Errorf("could not parse request body: %w", err) + logging.Log().WithError(err).Error(problemTitleDrawUpContract) + return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } var ( @@ -207,7 +240,9 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { if params.ValidFrom != nil { vf, err = time.Parse("2006-01-02T15:04:05-07:00", *params.ValidFrom) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not parse validFrom: %s", err.Error())) + err = fmt.Errorf("could not parse validFrom: %w", err) + logging.Log().WithError(err).Error(problemTitleDrawUpContract) + return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } } else { vf = time.Now() @@ -216,22 +251,29 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { if params.ValidDuration != nil { validDuration, err = time.ParseDuration(*params.ValidDuration) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("could not parse validDuration: %s", err.Error())) + err = fmt.Errorf("could not parse validDuration: %w", err) + logging.Log().WithError(err).Error(problemTitleDrawUpContract) + return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } } template := contract.StandardContractTemplates.Get(contract.Type(params.Type), contract.Language(params.Language), contract.Version(params.Version)) if template == nil { - return echo.NewHTTPError(http.StatusNotFound, "no contract found for given combination of type, version and language") + err = errors.New("no contract found for given combination of type, version, and language") + logging.Log().WithError(err).Error(problemTitleDrawUpContract) + return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) // todo: should this be 404 as in GetContractByType() } orgID, err := did.ParseDID(string(params.LegalEntity)) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid value for param legalEntity: '%s'", params.LegalEntity)) + err = fmt.Errorf("invalid value '%s' for param legalEntity: %w", params.LegalEntity, err) + logging.Log().WithError(err).Error(problemTitleDrawUpContract) + return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } drawnUpContract, err := w.Auth.ContractNotary().DrawUpContract(*template, *orgID, vf, validDuration) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("error while drawing up the contract: %s", err.Error())) + logging.Log().WithError(err).Error(problemTitleDrawUpContract) + return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } response := ContractResponse{ @@ -247,7 +289,9 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { func (w Wrapper) CreateJwtBearerToken(ctx echo.Context) error { requestBody := &CreateJwtBearerTokenRequest{} if err := ctx.Bind(requestBody); err != nil { - return err + err = fmt.Errorf("could not parse request body: %w", err) + logging.Log().WithError(err).Error(problemTitleCreateJwtBearerToken) + return core.NewProblem(problemTitleCreateJwtBearerToken, http.StatusBadRequest, err.Error()) } request := services.CreateJwtBearerTokenRequest{ @@ -259,7 +303,8 @@ func (w Wrapper) CreateJwtBearerToken(ctx echo.Context) error { } response, err := w.Auth.OAuthClient().CreateJwtBearerToken(request) if err != nil { - return ctx.JSON(http.StatusBadRequest, err.Error()) + logging.Log().WithError(err).Warn(problemTitleCreateJwtBearerToken) + return core.NewProblem(problemTitleCreateJwtBearerToken, http.StatusBadRequest, err.Error()) } return ctx.JSON(http.StatusOK, JwtBearerTokenResponse{BearerToken: response.BearerToken}) @@ -268,7 +313,8 @@ func (w Wrapper) CreateJwtBearerToken(ctx echo.Context) error { // CreateAccessToken handles the http request (from a remote vendor's Nuts node) for creating an access token for accessing // resources of the local vendor's EPD/XIS. It consumes a JWT Bearer token. // It consumes and checks the JWT and returns a smaller sessionToken -func (w Wrapper) CreateAccessToken(ctx echo.Context, params CreateAccessTokenParams) (err error) { +// The errors returns for this API do not follow RFC7807 but follow the oauth framework error response: RFC6749 (https://tools.ietf.org/html/rfc6749#page-45) +func (w Wrapper) CreateAccessToken(ctx echo.Context) (err error) { // Can't use echo.Bind() here since it requires extra tags on generated code request := new(CreateAccessTokenRequest) request.Assertion = ctx.FormValue("assertion") diff --git a/auth/api/v1/api_test.go b/auth/api/v1/api_test.go index c6175888c3..dda834acc6 100644 --- a/auth/api/v1/api_test.go +++ b/auth/api/v1/api_test.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + test2 "github.com/nuts-foundation/nuts-node/test" "net/http" "reflect" "testing" @@ -32,7 +33,6 @@ import ( "github.com/sirupsen/logrus" "github.com/golang/mock/gomock" - "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" pkg2 "github.com/nuts-foundation/nuts-node/auth" @@ -153,10 +153,10 @@ func TestWrapper_GetSignSessionStatus(t *testing.T) { ctx.contractClientMock.EXPECT().SigningSessionStatus(signingSessionID).Return(nil, services.ErrSessionNotFound) err := ctx.wrapper.GetSignSessionStatus(ctx.echoMock, signingSessionID) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusNotFound, httpError.Code) - assert.Equal(t, "no active signing session for sessionID: '123' found", httpError.Message) + + test2.AssertErrProblemTitle(t, problemTitleSignSessionStatus, err) + test2.AssertErrProblemStatusCode(t, http.StatusNotFound, err) + test2.AssertErrProblemDetail(t, "failed to get session status for 123, reason: session not found", err) }) t.Run("nok - unable to build a VP", func(t *testing.T) { @@ -166,15 +166,15 @@ func TestWrapper_GetSignSessionStatus(t *testing.T) { signingSessionID := "123" signingSessionResult := contract.NewMockSigningSessionResult(ctx.ctrl) - signingSessionResult.EXPECT().VerifiablePresentation().Return(nil, errors.New("could not build VP")) + signingSessionResult.EXPECT().VerifiablePresentation().Return(nil, errors.New("missing key")) ctx.contractClientMock.EXPECT().SigningSessionStatus(signingSessionID).Return(signingSessionResult, nil) err := ctx.wrapper.GetSignSessionStatus(ctx.echoMock, signingSessionID) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusInternalServerError, httpError.Code) - assert.Equal(t, "error while building verifiable presentation: could not build VP", httpError.Message) + + test2.AssertErrProblemTitle(t, problemTitleSignSessionStatus, err) + test2.AssertErrProblemStatusCode(t, http.StatusInternalServerError, err) + test2.AssertErrProblemDetail(t, "error while building verifiable presentation: missing key", err) }) } @@ -218,10 +218,9 @@ func TestWrapper_GetContractByType(t *testing.T) { wrapper := Wrapper{Auth: ctx.authMock} err := wrapper.GetContractByType(ctx.echoMock, cType, params) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusNotFound, httpError.Code) - + test2.AssertErrProblemTitle(t, problemTitleGetContract, err) + test2.AssertErrProblemStatusCode(t, http.StatusNotFound, err) + test2.AssertErrProblemDetail(t, "could not find contract template", err) }) } @@ -269,7 +268,7 @@ func TestWrapper_DrawUpContract(t *testing.T) { ctx := createContext(t) defer ctx.ctrl.Finish() - validFrom := "invalid date" + validFrom := "02 Jan 2010" params := DrawUpContractRequest{ ValidFrom: &validFrom, @@ -278,10 +277,9 @@ func TestWrapper_DrawUpContract(t *testing.T) { err := ctx.wrapper.DrawUpContract(ctx.echoMock) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, httpError.Code) - assert.Equal(t, "could not parse validFrom: parsing time \"invalid date\" as \"2006-01-02T15:04:05-07:00\": cannot parse \"invalid date\" as \"2006\"", httpError.Message) + test2.AssertErrProblemTitle(t, problemTitleDrawUpContract, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "could not parse validFrom: parsing time \"02 Jan 2010\" as \"2006-01-02T15:04:05-07:00\": cannot parse \"an 2010\" as \"2006\"", err) }) t.Run("invalid formatted duration", func(t *testing.T) { @@ -297,10 +295,9 @@ func TestWrapper_DrawUpContract(t *testing.T) { err := ctx.wrapper.DrawUpContract(ctx.echoMock) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, httpError.Code) - assert.Equal(t, "could not parse validDuration: time: unknown unit \" minutes\" in duration \"15 minutes\"", httpError.Message) + test2.AssertErrProblemTitle(t, problemTitleDrawUpContract, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "could not parse validDuration: time: unknown unit \" minutes\" in duration \"15 minutes\"", err) }) t.Run("unknown contract", func(t *testing.T) { @@ -316,10 +313,9 @@ func TestWrapper_DrawUpContract(t *testing.T) { err := ctx.wrapper.DrawUpContract(ctx.echoMock) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusNotFound, httpError.Code) - assert.Equal(t, "no contract found for given combination of type, version and language", httpError.Message) + test2.AssertErrProblemTitle(t, problemTitleDrawUpContract, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "no contract found for given combination of type, version, and language", err) }) t.Run("malformed orgID", func(t *testing.T) { @@ -336,10 +332,9 @@ func TestWrapper_DrawUpContract(t *testing.T) { err := ctx.wrapper.DrawUpContract(ctx.echoMock) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, httpError.Code) - assert.Equal(t, "invalid value for param legalEntity: 'ZorgId:15'", httpError.Message) + test2.AssertErrProblemTitle(t, problemTitleDrawUpContract, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "invalid value 'ZorgId:15' for param legalEntity: input does not begin with 'did:' prefix", err) }) }) @@ -360,10 +355,9 @@ func TestWrapper_DrawUpContract(t *testing.T) { err := ctx.wrapper.DrawUpContract(ctx.echoMock) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, httpError.Code) - assert.Equal(t, "error while drawing up the contract: unknown error while drawing up the contract", httpError.Message) + test2.AssertErrProblemTitle(t, problemTitleDrawUpContract, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "unknown error while drawing up the contract", err) }) } @@ -439,7 +433,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) { errorResponse := AccessTokenRequestFailedResponse{ErrorDescription: errorDescription, Error: errOauthUnsupportedGrant} expectError(ctx, errorResponse) - err := ctx.wrapper.CreateAccessToken(ctx.echoMock, CreateAccessTokenParams{}) + err := ctx.wrapper.CreateAccessToken(ctx.echoMock) assert.Nil(t, err) }) @@ -455,7 +449,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) { errorResponse := AccessTokenRequestFailedResponse{ErrorDescription: errorDescription, Error: errOauthInvalidGrant} expectError(ctx, errorResponse) - err := ctx.wrapper.CreateAccessToken(ctx.echoMock, CreateAccessTokenParams{}) + err := ctx.wrapper.CreateAccessToken(ctx.echoMock) assert.Nil(t, err) }) @@ -472,7 +466,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) { expectError(ctx, errorResponse) ctx.oauthClientMock.EXPECT().CreateAccessToken(services.CreateAccessTokenRequest{RawJwtBearerToken: validJwt}).Return(nil, fmt.Errorf("oh boy")) - err := ctx.wrapper.CreateAccessToken(ctx.echoMock, CreateAccessTokenParams{XSslClientCert: "cert"}) + err := ctx.wrapper.CreateAccessToken(ctx.echoMock) assert.Nil(t, err) }) @@ -490,7 +484,7 @@ func TestWrapper_CreateAccessToken(t *testing.T) { apiResponse := AccessTokenResponse{AccessToken: pkgResponse.AccessToken} expectStatusOK(ctx, apiResponse) - err := ctx.wrapper.CreateAccessToken(ctx.echoMock, CreateAccessTokenParams{}) + err := ctx.wrapper.CreateAccessToken(ctx.echoMock) assert.Nil(t, err) @@ -702,10 +696,9 @@ func TestWrapper_CreateSignSession(t *testing.T) { err := ctx.wrapper.CreateSignSession(ctx.echoMock) - assert.IsType(t, &echo.HTTPError{}, err) - httpError := err.(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, httpError.Code) - assert.Equal(t, "unable to create sign challenge: some error", httpError.Message) + test2.AssertErrProblemTitle(t, problemTitleCreateSignSession, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "unable to create sign challenge: some error", err) }) } @@ -837,7 +830,9 @@ func TestWrapper_VerifySignature(t *testing.T) { if !assert.Error(t, err) { return } - assert.Equal(t, "code=400, message=could not parse checkTime: parsing time \"invalid formatted timestamp\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"invalid formatted timestamp\" as \"2006\"", err.Error()) + test2.AssertErrProblemTitle(t, problemTitleVerifySignature, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "could not parse checkTime: parsing time \"invalid formatted timestamp\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"invalid formatted timestamp\" as \"2006\"", err) }) t.Run("nok - verification returns an error", func(t *testing.T) { @@ -856,6 +851,8 @@ func TestWrapper_VerifySignature(t *testing.T) { if !assert.Error(t, err) { return } - assert.Equal(t, `code=400, message=unable to verify the verifiable presentation: verification error`, err.Error()) + test2.AssertErrProblemTitle(t, problemTitleVerifySignature, err) + test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemDetail(t, "unable to verify the verifiable presentation: verification error", err) }) } diff --git a/auth/api/v1/generated.go b/auth/api/v1/generated.go index 18a388423d..5c5e745567 100644 --- a/auth/api/v1/generated.go +++ b/auth/api/v1/generated.go @@ -266,12 +266,6 @@ type VerifySignatureJSONBody SignatureVerificationRequest // CreateAccessTokenJSONBody defines parameters for CreateAccessToken. type CreateAccessTokenJSONBody CreateAccessTokenRequest -// CreateAccessTokenParams defines parameters for CreateAccessToken. -type CreateAccessTokenParams struct { - XSslClientCert string `json:"X-Ssl-Client-Cert"` - XNutsLegalEntity *string `json:"X-Nuts-LegalEntity,omitempty"` -} - // GetContractByTypeParams defines parameters for GetContractByType. type GetContractByTypeParams struct { @@ -300,11 +294,10 @@ type ServerInterface interface { // Introspection endpoint to retrieve information from an Access Token as described by RFC7662 // (POST /internal/auth/v1/accesstoken/introspect) IntrospectAccessToken(ctx echo.Context) error - // Verifies the access token given in the Authorization header (as bearer token). If it's a valid access token issued by this server, it'll return a 200 status code. - // If it cannot be verified it'll return 403. Note that it'll not return the contents of the access token. The introspection API is for that. + // Verifies the provided access token // (HEAD /internal/auth/v1/accesstoken/verify) VerifyAccessToken(ctx echo.Context, params VerifyAccessTokenParams) error - // Create a JWT Bearer Token which can be used in the createAccessToken request in the assertion field + // Create a JWT Bearer Token // (POST /internal/auth/v1/bearertoken) CreateJwtBearerToken(ctx echo.Context) error // Draw up a contract using a specified contract template, language and version @@ -320,11 +313,8 @@ type ServerInterface interface { // (PUT /internal/auth/v1/signature/verify) VerifySignature(ctx echo.Context) error // Create an access token based on the OAuth JWT Bearer flow. - // This endpoint must be available to the outside world for other applications to request access tokens. - // It requires a two-way TLS connection. The client certificate must be a sibling of the signing certificate of the given JWT. - // The client certificate must be passed using a X-Ssl-Client-Cert header, PEM encoded and urlescaped. - // (POST /public/auth/v1/accesstoken) - CreateAccessToken(ctx echo.Context, params CreateAccessTokenParams) error + // (POST /n2n/auth/v1/accesstoken) + CreateAccessToken(ctx echo.Context) error // Get a contract by type and version // (GET /public/auth/v1/contract/{contractType}) GetContractByType(ctx echo.Context, contractType string, params GetContractByTypeParams) error @@ -431,45 +421,8 @@ func (w *ServerInterfaceWrapper) VerifySignature(ctx echo.Context) error { func (w *ServerInterfaceWrapper) CreateAccessToken(ctx echo.Context) error { var err error - // Parameter object where we will unmarshal all parameters from the context - var params CreateAccessTokenParams - - headers := ctx.Request().Header - // ------------- Required header parameter "X-Ssl-Client-Cert" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("X-Ssl-Client-Cert")]; found { - var XSslClientCert string - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for X-Ssl-Client-Cert, got %d", n)) - } - - err = runtime.BindStyledParameter("simple", false, "X-Ssl-Client-Cert", valueList[0], &XSslClientCert) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter X-Ssl-Client-Cert: %s", err)) - } - - params.XSslClientCert = XSslClientCert - } else { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Header parameter X-Ssl-Client-Cert is required, but not found")) - } - // ------------- Optional header parameter "X-Nuts-LegalEntity" ------------- - if valueList, found := headers[http.CanonicalHeaderKey("X-Nuts-LegalEntity")]; found { - var XNutsLegalEntity string - n := len(valueList) - if n != 1 { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Expected one value for X-Nuts-LegalEntity, got %d", n)) - } - - err = runtime.BindStyledParameter("simple", false, "X-Nuts-LegalEntity", valueList[0], &XNutsLegalEntity) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter X-Nuts-LegalEntity: %s", err)) - } - - params.XNutsLegalEntity = &XNutsLegalEntity - } - // Invoke the callback with all the unmarshalled arguments - err = w.Handler.CreateAccessToken(ctx, params) + err = w.Handler.CreateAccessToken(ctx) return err } @@ -534,7 +487,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.Add(http.MethodPost, baseURL+"/internal/auth/v1/signature/session", wrapper.CreateSignSession) router.Add(http.MethodGet, baseURL+"/internal/auth/v1/signature/session/:sessionID", wrapper.GetSignSessionStatus) router.Add(http.MethodPut, baseURL+"/internal/auth/v1/signature/verify", wrapper.VerifySignature) - router.Add(http.MethodPost, baseURL+"/public/auth/v1/accesstoken", wrapper.CreateAccessToken) + router.Add(http.MethodPost, baseURL+"/n2n/auth/v1/accesstoken", wrapper.CreateAccessToken) router.Add(http.MethodGet, baseURL+"/public/auth/v1/contract/:contractType", wrapper.GetContractByType) } diff --git a/crypto/api/v1/api.go b/crypto/api/v1/api.go index 6ad3c104d4..d434253135 100644 --- a/crypto/api/v1/api.go +++ b/crypto/api/v1/api.go @@ -51,8 +51,7 @@ func (signRequest SignJwtRequest) validate() error { } const ( - problemTitleSignJwt = "SignJWT failed" - problemTitlePublicKey = "Failed to get PublicKey" + problemTitleSignJwt = "SignJWT failed" ) // SignJwt handles api calls for signing a Jwt diff --git a/docs/_static/auth/v1.yaml b/docs/_static/auth/v1.yaml index 6226eb9667..959d73d49a 100644 --- a/docs/_static/auth/v1.yaml +++ b/docs/_static/auth/v1.yaml @@ -8,6 +8,11 @@ paths: post: operationId: createSignSession summary: Create a signing session for a supported means. + description: | + Create a signing session for a supported means. + + error returns: + * 400 - ?? requestBody: required: true content: @@ -21,10 +26,19 @@ paths: application/json: schema: $ref: "#/components/schemas/CreateSignSessionResponse" + default: + $ref: '../common/error_response.yaml' + /internal/auth/v1/signature/session/{sessionID}: get: operationId: getSignSessionStatus summary: Get the current status of a signing session + description: | + Get the current status of a signing session + + error returns: + * 404 - session could not be found + * 500 - internal server error parameters: - name: sessionID in: path @@ -38,12 +52,19 @@ paths: application/json: schema: $ref: "#/components/schemas/GetSignSessionStatusResponse" - 404: - description: When the session could not be found. + default: + $ref: '../common/error_response.yaml' + /internal/auth/v1/signature/verify: put: operationId: verifySignature summary: Verify a signature in the form of a verifiable presentation + description: | + Verify a signature in the form of a verifiable presentation + + error returns: + * 400 - one of the parameters has the wrong format + * 500 - internal server error requestBody: required: true content: @@ -57,11 +78,18 @@ paths: application/json: schema: $ref: "#/components/schemas/SignatureVerificationResponse" + default: + $ref: '../common/error_response.yaml' /public/auth/v1/contract/{contractType}: get: operationId: getContractByType summary: Get a contract by type and version + description: | + Get contract by type and version + + error returns: + * 404 - contract does not exists tags: - auth parameters: @@ -83,22 +111,25 @@ paths: type: string default: nl responses: - '404': - description: if no contract exists - content: - text/plain: - schema: - type: string '200': description: Returns the contract of this type, version and language content: application/json: schema: $ref: "#/components/schemas/Contract" + default: + $ref: '../common/error_response.yaml' + /internal/auth/v1/contract/drawup: put: operationId: drawUpContract summary: Draw up a contract using a specified contract template, language and version + description: | + Draw up a contract using a specified contract template, language and version + + error returns: + * 400 - one of the parameters has the wrong format + * 404 - ?? Invalid template ?? requestBody: required: true content: @@ -112,13 +143,18 @@ paths: application/json: schema: $ref: "#/components/schemas/ContractResponse" - 404: - description: When the combination of template, language and version was not found. + default: + $ref: '../common/error_response.yaml' /internal/auth/v1/bearertoken: post: operationId: createJwtBearerToken - summary: Create a JWT Bearer Token which can be used in the createAccessToken request in the assertion field + summary: Create a JWT Bearer Token + description: | + Create a JWT Bearer Token which can be used in the createAccessToken request in the assertion field + + error returns: + * 400 - ?? tags: - auth requestBody: @@ -134,30 +170,22 @@ paths: application/json: schema: $ref: "#/components/schemas/JwtBearerTokenResponse" + default: + $ref: '../common/error_response.yaml' - - /public/auth/v1/accesstoken: + /n2n/auth/v1/accesstoken: post: operationId: createAccessToken - summary: | + summary: Create an access token based on the OAuth JWT Bearer flow. + description: | Create an access token based on the OAuth JWT Bearer flow. - This endpoint must be available to the outside world for other applications to request access tokens. - It requires a two-way TLS connection. The client certificate must be a sibling of the signing certificate of the given JWT. - The client certificate must be passed using a X-Ssl-Client-Cert header, PEM encoded and urlescaped. + This endpoint must be available to other nodes for other applications to request access tokens. + It requires a two-way TLS connection according to the network agreement. + + error returns: + * Follows the oauth framework error response: RFC6749 tags: - auth - parameters: - - name: X-Ssl-Client-Cert - in: header - required: true - schema: - type: string - - name: X-Nuts-LegalEntity - in: header - required: false - deprecated: true - schema: - type: string requestBody: required: true content: @@ -180,12 +208,16 @@ paths: application/json: schema: $ref: "#/components/schemas/AccessTokenRequestFailedResponse" + /internal/auth/v1/accesstoken/verify: head: operationId: verifyAccessToken - summary: | + summary: Verifies the provided access token + description: | Verifies the access token given in the Authorization header (as bearer token). If it's a valid access token issued by this server, it'll return a 200 status code. - If it cannot be verified it'll return 403. Note that it'll not return the contents of the access token. The introspection API is for that. + + error returns: + * 403 - Token cannot be verified. Note that the contents of the access token are not returned. The introspection API is for that. tags: - auth parameters: @@ -199,6 +231,7 @@ paths: description: The access token is valid. It has been signed by this server. '403': description: The given access token is invalid or couldn't be verified. + /internal/auth/v1/accesstoken/introspect: post: operationId: introspectAccessToken diff --git a/go.sum b/go.sum index 45a66e33cb..c82075983a 100644 --- a/go.sum +++ b/go.sum @@ -448,12 +448,8 @@ github.com/nightlyone/lockfile v0.0.0-20180618180623-0ad87eef1443 h1:+2OJrU8cmOs github.com/nightlyone/lockfile v0.0.0-20180618180623-0ad87eef1443/go.mod h1:JbxfV1Iifij2yhRjXai0oFrbpxszXHRx1E5RuM26o4Y= github.com/nishanths/exhaustive v0.0.0-20200525081945-8e46705b6132 h1:NjznefjSrral0MiR4KlB41io/d3OklvhcgQUdfZTqJE= github.com/nishanths/exhaustive v0.0.0-20200525081945-8e46705b6132/go.mod h1:wBEpHwM2OdmeNpdCvRPUlkEbBuaFmcK4Wv8Q7FuGW3c= -github.com/nuts-foundation/go-did v0.0.0-20210331072910-c246e14ed80f h1:WB7Mx6Jyxr/uLEb3mGVuxXntjzHnHiNl+2pCPuHqOiY= -github.com/nuts-foundation/go-did v0.0.0-20210331072910-c246e14ed80f/go.mod h1:Xqmd6ILZj41nNovodxnb4F+JgcKmIz+XGpLw6m6fZnU= github.com/nuts-foundation/go-did v0.0.0-20210406065435-ed072ef7b317 h1:CCG0I1W4WP7idYGHHjCNtpHLBQA3BWGMN/qIIJvheYU= github.com/nuts-foundation/go-did v0.0.0-20210406065435-ed072ef7b317/go.mod h1:Xqmd6ILZj41nNovodxnb4F+JgcKmIz+XGpLw6m6fZnU= -github.com/nuts-foundation/go-leia v0.4.0 h1:BdjZ3k2Wi3d25TCJBWZlD0Il8ox3uHVWP1ECvSdd0BM= -github.com/nuts-foundation/go-leia v0.4.0/go.mod h1:VMmOAg9I6kRUGY9eRNgeQqyj5vyx+oAteKWfVXzcTQs= github.com/nuts-foundation/go-leia v0.4.1 h1:BKmWhTkc9RzDGvJ1UeR99igiSx2JXVNaEYcNPO7bOyQ= github.com/nuts-foundation/go-leia v0.4.1/go.mod h1:VMmOAg9I6kRUGY9eRNgeQqyj5vyx+oAteKWfVXzcTQs= github.com/ockam-network/did v0.1.3 h1:qJGdccOV4bLfsS/eFM+Aj+CdCRJKNMxbmJevQclw44k= From 53c29b9c5273514ecf399b105ec71bd54af021c2 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw Date: Thu, 22 Apr 2021 14:01:51 +0200 Subject: [PATCH 2/3] fix status code --- auth/api/v1/api.go | 2 +- auth/api/v1/api_test.go | 2 +- docs/_static/auth/v1.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/auth/api/v1/api.go b/auth/api/v1/api.go index 7447c13c38..6309afe00f 100644 --- a/auth/api/v1/api.go +++ b/auth/api/v1/api.go @@ -261,7 +261,7 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { if template == nil { err = errors.New("no contract found for given combination of type, version, and language") logging.Log().WithError(err).Error(problemTitleDrawUpContract) - return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) // todo: should this be 404 as in GetContractByType() + return core.NewProblem(problemTitleDrawUpContract, http.StatusNotFound, err.Error()) } orgID, err := did.ParseDID(string(params.LegalEntity)) if err != nil { diff --git a/auth/api/v1/api_test.go b/auth/api/v1/api_test.go index dda834acc6..3ddc4280ae 100644 --- a/auth/api/v1/api_test.go +++ b/auth/api/v1/api_test.go @@ -314,7 +314,7 @@ func TestWrapper_DrawUpContract(t *testing.T) { err := ctx.wrapper.DrawUpContract(ctx.echoMock) test2.AssertErrProblemTitle(t, problemTitleDrawUpContract, err) - test2.AssertErrProblemStatusCode(t, http.StatusBadRequest, err) + test2.AssertErrProblemStatusCode(t, http.StatusNotFound, err) test2.AssertErrProblemDetail(t, "no contract found for given combination of type, version, and language", err) }) diff --git a/docs/_static/auth/v1.yaml b/docs/_static/auth/v1.yaml index 959d73d49a..1f6a56b3fc 100644 --- a/docs/_static/auth/v1.yaml +++ b/docs/_static/auth/v1.yaml @@ -112,7 +112,7 @@ paths: default: nl responses: '200': - description: Returns the contract of this type, version and language + description: Returns the contract of this type, version, and language content: application/json: schema: @@ -129,7 +129,7 @@ paths: error returns: * 400 - one of the parameters has the wrong format - * 404 - ?? Invalid template ?? + * 404 - combinetaion of template, language, and version not found requestBody: required: true content: From 8455024a8607bfb0d527419083757b357247b603 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw Date: Fri, 23 Apr 2021 09:24:45 +0200 Subject: [PATCH 3/3] update logging + oapi path desciptions --- auth/api/v1/api.go | 21 +++++++++++---------- docs/_static/auth/v1.yaml | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/auth/api/v1/api.go b/auth/api/v1/api.go index 6309afe00f..f7875e1af8 100644 --- a/auth/api/v1/api.go +++ b/auth/api/v1/api.go @@ -72,7 +72,7 @@ func (w Wrapper) VerifySignature(ctx echo.Context) error { requestParams := new(SignatureVerificationRequest) if err := ctx.Bind(requestParams); err != nil { err = fmt.Errorf("could not parse request body: %w", err) - logging.Log().WithError(err).Warn() + logging.Log().WithError(err).Warn(problemTitleVerifySignature) return core.NewProblem(problemTitleVerifySignature, http.StatusBadRequest, err.Error()) } rawVP, err := json.Marshal(requestParams.VerifiablePresentation) @@ -162,10 +162,11 @@ func (w Wrapper) GetSignSessionStatus(ctx echo.Context, sessionID string) error sessionStatus, err := w.Auth.ContractClient().SigningSessionStatus(sessionID) if err != nil { err = fmt.Errorf("failed to get session status for %s, reason: %w", sessionID, err) - logging.Log().WithError(err).Error(problemTitleSignSessionStatus) if errors.Is(err, services.ErrSessionNotFound) { + logging.Log().WithError(err).Warn(problemTitleSignSessionStatus) return core.NewProblem(problemTitleSignSessionStatus, http.StatusNotFound, err.Error()) } + logging.Log().WithError(err).Error(problemTitleSignSessionStatus) return core.NewProblem(problemTitleSignSessionStatus, http.StatusInternalServerError, err.Error()) } vp, err := sessionStatus.VerifiablePresentation() @@ -207,7 +208,7 @@ func (w Wrapper) GetContractByType(ctx echo.Context, contractType string, params authContract := contract.StandardContractTemplates.Get(contract.Type(contractType), contractLanguage, contractVersion) if authContract == nil { err := errors.New("could not find contract template") - logging.Log().WithError(err).Error(problemTitleGetContract) + logging.Log().WithError(err).Info(problemTitleGetContract) return core.NewProblem(problemTitleGetContract, http.StatusNotFound, err.Error()) } @@ -228,7 +229,7 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { params := new(DrawUpContractRequest) if err := ctx.Bind(params); err != nil { err = fmt.Errorf("could not parse request body: %w", err) - logging.Log().WithError(err).Error(problemTitleDrawUpContract) + logging.Log().WithError(err).Warn(problemTitleDrawUpContract) return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } @@ -241,7 +242,7 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { vf, err = time.Parse("2006-01-02T15:04:05-07:00", *params.ValidFrom) if err != nil { err = fmt.Errorf("could not parse validFrom: %w", err) - logging.Log().WithError(err).Error(problemTitleDrawUpContract) + logging.Log().WithError(err).Warn(problemTitleDrawUpContract) return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } } else { @@ -252,7 +253,7 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { validDuration, err = time.ParseDuration(*params.ValidDuration) if err != nil { err = fmt.Errorf("could not parse validDuration: %w", err) - logging.Log().WithError(err).Error(problemTitleDrawUpContract) + logging.Log().WithError(err).Warn(problemTitleDrawUpContract) return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } } @@ -260,19 +261,19 @@ func (w Wrapper) DrawUpContract(ctx echo.Context) error { template := contract.StandardContractTemplates.Get(contract.Type(params.Type), contract.Language(params.Language), contract.Version(params.Version)) if template == nil { err = errors.New("no contract found for given combination of type, version, and language") - logging.Log().WithError(err).Error(problemTitleDrawUpContract) + logging.Log().WithError(err).Warn(problemTitleDrawUpContract) return core.NewProblem(problemTitleDrawUpContract, http.StatusNotFound, err.Error()) } orgID, err := did.ParseDID(string(params.LegalEntity)) if err != nil { err = fmt.Errorf("invalid value '%s' for param legalEntity: %w", params.LegalEntity, err) - logging.Log().WithError(err).Error(problemTitleDrawUpContract) + logging.Log().WithError(err).Warn(problemTitleDrawUpContract) return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } drawnUpContract, err := w.Auth.ContractNotary().DrawUpContract(*template, *orgID, vf, validDuration) if err != nil { - logging.Log().WithError(err).Error(problemTitleDrawUpContract) + logging.Log().WithError(err).Warn(problemTitleDrawUpContract) return core.NewProblem(problemTitleDrawUpContract, http.StatusBadRequest, err.Error()) } @@ -290,7 +291,7 @@ func (w Wrapper) CreateJwtBearerToken(ctx echo.Context) error { requestBody := &CreateJwtBearerTokenRequest{} if err := ctx.Bind(requestBody); err != nil { err = fmt.Errorf("could not parse request body: %w", err) - logging.Log().WithError(err).Error(problemTitleCreateJwtBearerToken) + logging.Log().WithError(err).Warn(problemTitleCreateJwtBearerToken) return core.NewProblem(problemTitleCreateJwtBearerToken, http.StatusBadRequest, err.Error()) } diff --git a/docs/_static/auth/v1.yaml b/docs/_static/auth/v1.yaml index 1f6a56b3fc..dc36ee1f84 100644 --- a/docs/_static/auth/v1.yaml +++ b/docs/_static/auth/v1.yaml @@ -12,7 +12,7 @@ paths: Create a signing session for a supported means. error returns: - * 400 - ?? + * 400 - one of the parameters has the wrong format requestBody: required: true content: @@ -154,7 +154,7 @@ paths: Create a JWT Bearer Token which can be used in the createAccessToken request in the assertion field error returns: - * 400 - ?? + * 400 - one of the parameters has the wrong format tags: - auth requestBody: