Skip to content

Commit

Permalink
feat: Enumerate error codes (#1493)
Browse files Browse the repository at this point in the history
Enumerate the errors and assign error codes. Also, add error code and error component to failure events.

Signed-off-by: Bob Stasyszyn <Bob.Stasyszyn@securekey.com>
  • Loading branch information
bstasyszyn authored Oct 24, 2023
1 parent 9095c2c commit e5dda9a
Show file tree
Hide file tree
Showing 36 changed files with 596 additions and 250 deletions.
26 changes: 13 additions & 13 deletions pkg/event/spi/spi.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,27 @@ type EventType string

const (
// VerifierOIDCInteractionInitiated verifier oidc event.
VerifierOIDCInteractionInitiated = "verifier.oidc-interaction-initiated.v1"
VerifierOIDCInteractionInitiated EventType = "verifier.oidc-interaction-initiated.v1"
// VerifierOIDCInteractionQRScanned verifier oidc event.
VerifierOIDCInteractionQRScanned = "verifier.oidc-interaction-qr-scanned.v1"
VerifierOIDCInteractionQRScanned EventType = "verifier.oidc-interaction-qr-scanned.v1"
// VerifierOIDCInteractionSucceeded verifier oidc event.
VerifierOIDCInteractionSucceeded = "verifier.oidc-interaction-succeeded.v1"
VerifierOIDCInteractionSucceeded EventType = "verifier.oidc-interaction-succeeded.v1"
// VerifierOIDCInteractionFailed verifier oidc event.
VerifierOIDCInteractionFailed = "verifier.oidc-interaction-failed.v1"
VerifierOIDCInteractionClaimsRetrieved = "verifier.oidc-interaction-claims-retrieved.v1"
VerifierOIDCInteractionFailed EventType = "verifier.oidc-interaction-failed.v1"
VerifierOIDCInteractionClaimsRetrieved EventType = "verifier.oidc-interaction-claims-retrieved.v1"

// IssuerOIDCInteractionInitiated Issuer oidc event.
IssuerOIDCInteractionInitiated = EventType("issuer.oidc-interaction-initiated.v1")
IssuerOIDCInteractionInitiated EventType = "issuer.oidc-interaction-initiated.v1"
// IssuerOIDCInteractionQRScanned Issuer oidc event.
IssuerOIDCInteractionQRScanned = EventType("issuer.oidc-interaction-qr-scanned.v1")
IssuerOIDCInteractionQRScanned EventType = "issuer.oidc-interaction-qr-scanned.v1"
// IssuerOIDCInteractionSucceeded Issuer oidc event.
IssuerOIDCInteractionSucceeded = EventType("issuer.oidc-interaction-succeeded.v1")
IssuerOIDCInteractionAuthorizationRequestPrepared = EventType("issuer.oidc-interaction-authorization-request-prepared.v1") //nolint
IssuerOIDCInteractionAuthorizationCodeStored = EventType("issuer.oidc-interaction-authorization-code-stored.v1") //nolint
IssuerOIDCInteractionAuthorizationCodeExchanged = EventType("issuer.oidc-interaction-authorization-code-exchanged.v1") //nolint
IssuerOIDCInteractionFailed = EventType("issuer.oidc-interaction-failed.v1")
IssuerOIDCInteractionSucceeded EventType = "issuer.oidc-interaction-succeeded.v1"
IssuerOIDCInteractionAuthorizationRequestPrepared EventType = "issuer.oidc-interaction-authorization-request-prepared.v1" //nolint
IssuerOIDCInteractionAuthorizationCodeStored EventType = "issuer.oidc-interaction-authorization-code-stored.v1" //nolint
IssuerOIDCInteractionAuthorizationCodeExchanged EventType = "issuer.oidc-interaction-authorization-code-exchanged.v1" //nolint
IssuerOIDCInteractionFailed EventType = "issuer.oidc-interaction-failed.v1"

CredentialStatusStatusUpdated = EventType("issuer.credential-status-updated.v1")
CredentialStatusStatusUpdated EventType = "issuer.credential-status-updated.v1" //nolint:gosec
)

// Payload defines payload.
Expand Down
107 changes: 102 additions & 5 deletions pkg/restapi/resterr/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ SPDX-License-Identifier: Apache-2.0
package resterr

import (
"errors"
"fmt"
"net/http"
)

type ErrorCode string

//nolint:gosec
const (
SystemError ErrorCode = "system-error"
Unauthorized ErrorCode = "unauthorized"
Expand All @@ -26,9 +28,75 @@ const (
OIDCPreAuthorizeExpectPin ErrorCode = "oidc-pre-authorize-expect-pin"
OIDCPreAuthorizeInvalidPin ErrorCode = "oidc-pre-authorize-invalid-pin"
OIDCPreAuthorizeInvalidClientID ErrorCode = "oidc-pre-authorize-invalid-client-id"
OIDCCredentialFormatNotSupported ErrorCode = "oidc-credential-format-not-supported" //nolint:gosec
OIDCCredentialTypeNotSupported ErrorCode = "oidc-credential-type-not-supported" //nolint:gosec
OIDCCredentialFormatNotSupported ErrorCode = "oidc-credential-format-not-supported"
OIDCCredentialTypeNotSupported ErrorCode = "oidc-credential-type-not-supported"
InvalidOrMissingProofOIDCErr ErrorCode = "invalid_or_missing_proof"

ProfileNotFound ErrorCode = "profile-not-found"
ProfileInactive ErrorCode = "profile-inactive"
TransactionNotFound ErrorCode = "transaction-not-found"
CredentialTemplateNotFound ErrorCode = "credential-template-not-found"
PresentationVerificationFailed ErrorCode = "presentation-verification-failed"
DuplicatePresentationID ErrorCode = "duplicate-presentation-id"
PresentationDefinitionMismatch ErrorCode = "presentation-definition-mismatch"
ClaimsNotReceived ErrorCode = "claims-not-received"
ClaimsNotFound ErrorCode = "claims-not-found"
DataNotFound ErrorCode = "data-not-found"
OpStateKeyDuplication ErrorCode = "op-state-key duplication"
CredentialTemplateNotConfigured ErrorCode = "credential-template-not-configured"
CredentialTemplateIDRequired ErrorCode = "credential-template-id-required"
AuthorizedCodeFlowNotSupported ErrorCode = "authorized-code-flow-not-supported"
ResponseTypeMismatch ErrorCode = "response-type-mismatch"
InvalidScope ErrorCode = "invalid-scope"
CredentialTypeNotSupported ErrorCode = "credential-type-not-supported"
CredentialFormatNotSupported ErrorCode = "credential-format-not-supported"
VCOptionsNotConfigured ErrorCode = "vc-options-not-configured"
InvalidIssuerURL ErrorCode = "invalid-issuer-url"
)

type Component = string

//nolint:gosec
const (
IssuerSvcComponent Component = "issuer.service"
IssuerProfileSvcComponent Component = "issuer.profile-service"
IssueCredentialSvcComponent Component = "issuer.issue-credential-service"
IssuerOIDC4ciSvcComponent Component = "issuer.oidc4ci-service"

VerifierVerifyCredentialSvcComponent Component = "verifier.verify-credential-service"
VerifierOIDC4vpSvcComponent Component = "verifier.oidc4vp-service"
VerifierProfileSvcComponent Component = "verifier.profile-service"
VerifierTxnMgrComponent Component = "verifier.txn-mgr"
VerifierVCSignerComponent Component = "verifier.vc-signer"
VerifierKMSRegistryComponent Component = "verifier.kms-registry"
VerifierPresentationVerifierComponent Component = "verifier.presentation-verifier"
VerifierDataIntegrityVerifier Component = "verifier.data-integrity-verifier"

ClientIDSchemeSvcComponent Component = "client-id-scheme-service"
ClientManagerComponent Component = "client-manager"
WellKnownSvcComponent Component = "well-known-service"
DataProtectorComponent Component = "data-protector"
ClaimDataStoreComponent Component = "claim-data-store"
TransactionStoreComponent Component = "transaction-store"
CryptoJWTSignerComponent Component = "crypto-jwt-signer"
CredentialOfferReferenceStoreComponent Component = "credential-offer-reference-store"
RedisComponent Component = "redis-service"
)

var (
ErrDataNotFound = NewCustomError(DataNotFound, errors.New("data not found"))
ErrOpStateKeyDuplication = NewCustomError(OpStateKeyDuplication, errors.New("op state key duplication"))
ErrProfileInactive = NewCustomError(ProfileInactive, errors.New("profile not active"))
ErrCredentialTemplateNotFound = NewCustomError(CredentialTemplateNotFound, errors.New("credential template not found")) //nolint:lll
ErrCredentialTemplateNotConfigured = NewCustomError(CredentialTemplateNotConfigured, errors.New("credential template not configured")) //nolint:lll
ErrCredentialTemplateIDRequired = NewCustomError(CredentialTemplateIDRequired, errors.New("credential template ID is required")) //nolint:lll
ErrAuthorizedCodeFlowNotSupported = NewCustomError(AuthorizedCodeFlowNotSupported, errors.New("authorized code flow not supported")) //nolint:lll
ErrResponseTypeMismatch = NewCustomError(ResponseTypeMismatch, errors.New("response type mismatch"))
ErrInvalidScope = NewCustomError(InvalidScope, errors.New("invalid scope"))
ErrCredentialTypeNotSupported = NewCustomError(CredentialTypeNotSupported, errors.New("credential type not supported")) //nolint:lll
ErrCredentialFormatNotSupported = NewCustomError(CredentialFormatNotSupported, errors.New("credential format not supported")) //nolint:lll
ErrVCOptionsNotConfigured = NewCustomError(VCOptionsNotConfigured, errors.New("vc options not configured"))
ErrInvalidIssuerURL = NewCustomError(InvalidIssuerURL, errors.New("invalid issuer url"))
)

func (c ErrorCode) Name() string {
Expand All @@ -39,11 +107,11 @@ type CustomError struct {
Code ErrorCode
IncorrectValue string
FailedOperation string
Component string
Component Component
Err error
}

func NewSystemError(component, failedOperation string, err error) *CustomError {
func NewSystemError(component Component, failedOperation string, err error) *CustomError {
return &CustomError{
Code: SystemError,
FailedOperation: failedOperation,
Expand Down Expand Up @@ -86,11 +154,20 @@ func (e *CustomError) Error() string {
if e.Code == SystemError {
return fmt.Sprintf("%s[%s, %s]: %v", SystemError, e.Component, e.FailedOperation, e.Err)
}

if e.Code == Unauthorized {
return fmt.Sprintf("%s: %v", e.Code, e.Err)
}

return fmt.Sprintf("%s[%s]: %v", e.Code, e.IncorrectValue, e.Err)
if e.IncorrectValue != "" {
return fmt.Sprintf("%s[%s]: %v", e.Code, e.IncorrectValue, e.Err)
}

return fmt.Sprintf("%s: %v", e.Code, e.Err)
}

func (e *CustomError) Unwrap() error {
return e.Err
}

func (e *CustomError) HTTPCodeMsg() (int, interface{}) {
Expand All @@ -104,16 +181,25 @@ func (e *CustomError) HTTPCodeMsg() (int, interface{}) {
"operation": e.FailedOperation,
"message": e.Err.Error(),
}

case Unauthorized:
return http.StatusUnauthorized, map[string]interface{}{
"code": Unauthorized.Name(),
"message": e.Err.Error(),
}

case ProfileNotFound:
return http.StatusNotFound, map[string]interface{}{
"code": ProfileNotFound.Name(),
"message": e.Err.Error(),
}

case OIDCError:
return http.StatusBadRequest, map[string]interface{}{
"error": e.Component,
"_raw": e.Err.Error(),
}

case AlreadyExist:
code = http.StatusConflict

Expand Down Expand Up @@ -148,3 +234,14 @@ type RegistrationError struct {
func (e *RegistrationError) Error() string {
return e.Err.Error()
}

// GetErrorDetails extracts the error message, error code and component from the given error. If the error
// is not a CustomError implementation then the error code and component will be empty.
func GetErrorDetails(err error) (string, string, Component) {
var ce *CustomError
if ok := errors.As(err, &ce); ok {
return ce.Err.Error(), string(ce.Code), ce.Component
}

return err.Error(), "", ""
}
35 changes: 35 additions & 0 deletions pkg/restapi/resterr/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package resterr

import (
"errors"
"fmt"
"net/http"
"testing"

Expand Down Expand Up @@ -80,4 +81,38 @@ func TestNewValidationError(t *testing.T) {
requireCode(t, resp, ConditionNotMet.Name())
requireMessage(t, resp, "some error")
})

t.Run("profile not found error", func(t *testing.T) {
err := NewCustomError(ProfileNotFound, errors.New("some error"))
require.Equal(t, "profile-not-found: some error", err.Error())

httpCode, resp := err.HTTPCodeMsg()

require.Equal(t, http.StatusNotFound, httpCode)
requireCode(t, resp, ProfileNotFound.Name())
requireMessage(t, resp, "some error")
})
}

func TestGetErrorDetails(t *testing.T) {
t.Run("custom error", func(t *testing.T) {
e := errors.New("some error")

err := fmt.Errorf("got error: %w",
NewSystemError(TransactionStoreComponent, "getData", e))

errMsg, errCode, errComponent := GetErrorDetails(err)
require.Equal(t, e.Error(), errMsg)
require.Equal(t, string(SystemError), errCode)
require.Equal(t, TransactionStoreComponent, errComponent)
})

t.Run("other error", func(t *testing.T) {
err := errors.New("some error")

errMsg, errCode, errComponent := GetErrorDetails(err)
require.Equal(t, err.Error(), errMsg)
require.Empty(t, errCode)
require.Empty(t, errComponent)
})
}
44 changes: 20 additions & 24 deletions pkg/restapi/v1/issuer/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ import (
var logger = log.New("restapi-issuer")

const (
issuerProfileSvcComponent = "issuer.ProfileService"
oidc4ciSvcComponent = "OIDC4CIService"

defaultCtx = "https://www.w3.org/2018/credentials/v1"
)

Expand Down Expand Up @@ -320,7 +317,7 @@ func (c *Controller) signCredential(
) (*verifiable.Credential, error) {
signedVC, err := c.issueCredentialService.IssueCredential(ctx, credential, profile, opts...)
if err != nil {
return nil, resterr.NewSystemError("IssueCredentialService", "IssueCredential", err)
return nil, resterr.NewSystemError(resterr.IssueCredentialSvcComponent, "IssueCredential", err)
}

return signedVC, nil
Expand Down Expand Up @@ -503,16 +500,14 @@ func (c *Controller) initiateIssuance(

resp, err := c.oidc4ciService.InitiateIssuance(ctx, issuanceReq, profile)
if err != nil {
if errors.Is(err, oidc4ci.ErrCredentialTemplateNotFound) ||
errors.Is(err, oidc4ci.ErrCredentialTemplateIDRequired) {
e := resterr.NewValidationError(resterr.InvalidValue, "credential_template_id", err)

c.sendFailedEvent(ctx, profile.OrganizationID, profile.ID, profile.Version, e)
if errors.Is(err, resterr.ErrCredentialTemplateNotFound) ||
errors.Is(err, resterr.ErrCredentialTemplateIDRequired) {
c.sendFailedEvent(ctx, profile.OrganizationID, profile.ID, profile.Version, err)

return nil, "", e
return nil, "", err
}

e := resterr.NewSystemError(oidc4ciSvcComponent, "InitiateIssuance", err)
e := resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "InitiateIssuance", err)

c.sendFailedEvent(ctx, profile.OrganizationID, profile.ID, profile.Version, e)

Expand Down Expand Up @@ -541,15 +536,15 @@ func (c *Controller) PushAuthorizationDetails(ctx echo.Context) error {
}

if err = c.oidc4ciService.PushAuthorizationDetails(ctx.Request().Context(), body.OpState, ad); err != nil {
if errors.Is(err, oidc4ci.ErrCredentialTypeNotSupported) {
if errors.Is(err, resterr.ErrCredentialTypeNotSupported) {
return resterr.NewValidationError(resterr.InvalidValue, "authorization_details.type", err)
}

if errors.Is(err, oidc4ci.ErrCredentialFormatNotSupported) {
if errors.Is(err, resterr.ErrCredentialFormatNotSupported) {
return resterr.NewValidationError(resterr.InvalidValue, "authorization_details.format", err)
}

return resterr.NewSystemError(oidc4ciSvcComponent, "PushAuthorizationRequest", err)
return resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "PushAuthorizationRequest", err)
}

return ctx.NoContent(http.StatusOK)
Expand Down Expand Up @@ -585,12 +580,12 @@ func (c *Controller) prepareClaimDataAuthorizationRequest(
},
)
if err != nil {
return nil, resterr.NewSystemError(oidc4ciSvcComponent, "PrepareClaimDataAuthorizationRequest", err)
return nil, resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "PrepareClaimDataAuthorizationRequest", err)
}

profile, err := c.profileSvc.GetProfile(resp.ProfileID, resp.ProfileVersion)
if err != nil {
return nil, resterr.NewSystemError(oidc4ciSvcComponent, "PrepareClaimDataAuthorizationRequest", err)
return nil, resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "PrepareClaimDataAuthorizationRequest", err)
}

return &PrepareClaimDataAuthorizationResponse{
Expand All @@ -611,15 +606,15 @@ func (c *Controller) accessProfile(profileID, profileVersion string) (*profileap
profile, err := c.profileSvc.GetProfile(profileID, profileVersion)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return nil, resterr.NewValidationError(resterr.DoesntExist, "profile",
return nil, resterr.NewCustomError(resterr.ProfileNotFound,
fmt.Errorf("profile with given id %s_%s, doesn't exist", profileID, profileVersion))
}

return nil, resterr.NewSystemError(issuerProfileSvcComponent, "GetProfile", err)
return nil, resterr.NewSystemError(resterr.IssuerProfileSvcComponent, "GetProfile", err)
}

if profile == nil {
return nil, resterr.NewValidationError(resterr.DoesntExist, "profile",
return nil, resterr.NewCustomError(resterr.ProfileNotFound,
fmt.Errorf("profile with given id %s_%s, doesn't exist", profileID, profileVersion))
}

Expand All @@ -634,7 +629,7 @@ func (c *Controller) accessOIDCProfile(profileID, profileVersion, tenantID strin

// Profiles of other organization is not visible.
if profile.OrganizationID != tenantID {
return nil, resterr.NewValidationError(resterr.DoesntExist, "profile",
return nil, resterr.NewCustomError(resterr.ProfileNotFound,
fmt.Errorf("profile with given id %s_%s, doesn't exist", profileID, profileVersion))
}

Expand Down Expand Up @@ -727,7 +722,7 @@ func (c *Controller) PrepareCredential(e echo.Context) error {
return custom
}

return resterr.NewSystemError(oidc4ciSvcComponent, "PrepareCredential", err)
return resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "PrepareCredential", err)
}

profile, err := c.accessProfile(result.ProfileID, result.ProfileVersion)
Expand All @@ -736,7 +731,7 @@ func (c *Controller) PrepareCredential(e echo.Context) error {
}

if result.Credential == nil {
return resterr.NewSystemError(oidc4ciSvcComponent, "PrepareCredential",
return resterr.NewSystemError(resterr.IssuerOIDC4ciSvcComponent, "PrepareCredential",
errors.New("credentials should not be nil"))
}

Expand Down Expand Up @@ -925,17 +920,18 @@ func (c *Controller) sendFailedEvent(ctx context.Context, orgID, profileID, prof
OrgID: orgID,
ProfileID: profileID,
ProfileVersion: profileVersion,
Error: e.Error(),
}

ep.Error, ep.ErrorCode, ep.ErrorComponent = resterr.GetErrorDetails(e)

payload, err := c.marshal(ep)
if err != nil {
logger.Errorc(ctx, "Error sending event due to marshalling error", log.WithError(err))

return
}

evt := spi.NewEventWithPayload(uuid.NewString(), "source://vcs/issuer", spi.VerifierOIDCInteractionFailed, payload)
evt := spi.NewEventWithPayload(uuid.NewString(), "source://vcs/issuer", spi.IssuerOIDCInteractionFailed, payload)

err = c.eventSvc.Publish(ctx, c.eventTopic, evt)
if err != nil {
Expand Down
Loading

0 comments on commit e5dda9a

Please sign in to comment.