Skip to content

Commit

Permalink
feat(sdk): OpenID4VP error response parsing
Browse files Browse the repository at this point in the history
* Added parsing for OpenID4VP error responses per the spec + new error codes.
*  For consistency, replaced one of the existing OpenID4VP error codes with the new "invalid SDK usage" error code type introduced in the OpenID4CI error handling commit.
* Previously, when errors without error type info were consumed in an app using the gomobile bindings, the default error code and category would differ depending on which layer the error processing was done (would be either GNR2-000/GENERAL_ERROR or UKN2-000/UNEXPECTED_ERROR). I've updated it so that we use the same error code and category in both places. I chose "OTHER_ERROR" for the category to try to be more descriptive/accurate of what the category is.

Signed-off-by: Derek Trider <[email protected]>
  • Loading branch information
Derek Trider committed Jul 24, 2023
1 parent 1f6dc9f commit 0613552
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ func (i *IssuerInitiatedInteraction) CreateAuthorizationURL(clientID, redirectUR
goAPIOpts = append(goAPIOpts, openid4cigoapi.WithIssuerState(*opts.issuerState))
}

return i.goAPIInteraction.CreateAuthorizationURL(clientID, redirectURI, goAPIOpts...)
authorizationURL, err := i.goAPIInteraction.CreateAuthorizationURL(clientID, redirectURI, goAPIOpts...)
if err != nil {
return "", wrapper.ToMobileErrorWithTrace(err, i.oTel)
}

return authorizationURL, nil
}

// RequestCredentialWithPreAuth requests credential(s) from the issuer. This method can only be used for the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ func TestIssuerInitiatedInteraction_CreateAuthorizationURL(t *testing.T) {
nil, false)

authorizationLink, err := interaction.CreateAuthorizationURL("clientID", "redirectURI", nil)
require.EqualError(t, err, "issuer does not support the authorization code grant type")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "issuer does not support the authorization code grant type")
require.Empty(t, authorizationLink)
})
t.Run("Conflicting issuer state", func(t *testing.T) {
Expand All @@ -203,7 +204,8 @@ func TestIssuerInitiatedInteraction_CreateAuthorizationURL(t *testing.T) {

authorizationLink, err := interaction.CreateAuthorizationURL("clientID", "redirectURI",
createAuthorizationURLOpts)
require.EqualError(t, err, "INVALID_SDK_USAGE(OCI3-0000):the credential offer already specifies "+
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "the credential offer already specifies "+
"an issuer state, and a conflicting issuer state value was provided. An issuer state should only be "+
"provided if required by the issuer and the credential offer does not specify one already")
require.Empty(t, authorizationLink)
Expand Down Expand Up @@ -472,7 +474,8 @@ func TestIssuerInitiatedInteractionAlias(t *testing.T) {
// IssuerInitiatedInteraction) See TestIssuerInitiatedInteraction_RequestCredential or the integration tests for
// better examples.
authURL, err := interaction.CreateAuthorizationURL("", "", nil)
require.EqualError(t, err, "issuer does not support the authorization code grant type")
requireErrorContains(t, err, "INVALID_SDK_USAGE")
requireErrorContains(t, err, "issuer does not support the authorization code grant type")
require.Empty(t, authURL)

credentials, err = interaction.RequestCredentialWithPreAuth(nil, nil)
Expand Down
4 changes: 2 additions & 2 deletions cmd/wallet-sdk-gomobile/walleterror/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func Parse(message string) *Error {
err := json.Unmarshal([]byte(message), walletErr)
if err != nil {
return &Error{
Code: "GNR2-000",
Category: "GENERAL_ERROR",
Code: "UKN2-000",
Category: "OTHER_ERROR",
Details: message,
}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/wallet-sdk-gomobile/wrapper/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func convertToGomobileError(err error, trace *otel.Trace) *walleterror.Error {
// gomobile wallet error using a generic code.
return &walleterror.Error{
Code: "UKN2-000",
Category: "UNEXPECTED_ERROR",
Category: "OTHER_ERROR",
Details: err.Error(),
TraceID: traceID,
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/wallet-sdk-gomobile/wrapper/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestToMobileError(t *testing.T) {
parsedErr := walleterror.Parse(err.Error())

require.Equal(t, "UKN2-000", parsedErr.Code)
require.Equal(t, "UNEXPECTED_ERROR", parsedErr.Category)
require.Equal(t, "OTHER_ERROR", parsedErr.Category)
require.Equal(t, "regular Go error", parsedErr.Details)
})
t.Run("Non-goapiwalleterror.Error wrapped with another Non-goapiwalleterror.Error passed in", func(t *testing.T) {
Expand All @@ -60,7 +60,7 @@ func TestToMobileError(t *testing.T) {
parsedErr := walleterror.Parse(err.Error())

require.Equal(t, "UKN2-000", parsedErr.Code)
require.Equal(t, "UNEXPECTED_ERROR", parsedErr.Category)
require.Equal(t, "OTHER_ERROR", parsedErr.Category)
require.Equal(t, "higher-level error: regular Go error", parsedErr.Details)
})
t.Run("goapiwalleterror.Error wrapped by one higher-level non-goapiwalleterror.Error is passed in",
Expand Down
3 changes: 2 additions & 1 deletion pkg/openid4ci/issuerinitiatedinteraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ func (i *IssuerInitiatedInteraction) CreateAuthorizationURL(clientID, redirectUR
opts ...CreateAuthorizationURLOpt,
) (string, error) {
if !i.AuthorizationCodeGrantTypeSupported() {
return "", errors.New("issuer does not support the authorization code grant type")
return "", walleterror.NewInvalidSDKUsageError(ErrorModule,
errors.New("issuer does not support the authorization code grant type"))
}

processedOpts := processCreateAuthorizationURLOpts(opts)
Expand Down
38 changes: 26 additions & 12 deletions pkg/openid4vp/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,34 @@ package openid4vp
// Constants' names and reasons are obvious so they do not require additional comments.
// nolint:golint,nolintlint
const (
module = "OVP"
RequestObjectFetchFailedError = "REQUEST_OBJECT_FETCH_FAILED"
VerifyAuthorizationRequestFailedError = "VERIFY_AUTHORIZATION_REQUEST_FAILED"
CreateAuthorizedResponseFailedError = "CREATE_AUTHORIZED_RESPONSE"
SendAuthorizedResponseFailedError = "SEND_AUTHORIZED_RESPONSE"
NotInitializedProperlyError = "NOT_INITIALIZED_PROPERLY"
ErrorModule = "OVP"
RequestObjectFetchFailedError = "REQUEST_OBJECT_FETCH_FAILED"
VerifyAuthorizationRequestFailedError = "VERIFY_AUTHORIZATION_REQUEST_FAILED"
CreateAuthorizedResponseFailedError = "CREATE_AUTHORIZED_RESPONSE_FAILED"
InvalidScopeError = "INVALID_SCOPE"
InvalidRequestError = "INVALID_REQUEST"
InvalidClientError = "INVALID_CLIENT"
VPFormatsNotSupportedError = "VP_FORMATS_NOT_SUPPORTED"
InvalidPresentationDefinitionURIError = "INVALID_PRESENTATION_DEFINITION_URI"
InvalidPresentationDefinitionReferenceError = "INVALID_PRESENTATION_DEFINITION_REFERENCE"
OtherAuthorizationResponseError = "OTHER_AUTHORIZATION_RESPONSE_ERROR"
)

// Constants' names and reasons are obvious so they do not require additional comments.
// Constants' names and reasons are obvious, so they do not require additional comments.
// nolint:golint,nolintlint
const (
RequestObjectFetchFailedCode = iota
VerifyAuthorizationRequestFailedCode
CreateAuthorizedResponseFailedCode
SendAuthorizedResponseFailedCode
NotInitializedProperlyErrorCode
RequestObjectFetchFailedCode = 0
VerifyAuthorizationRequestFailedCode = 1
CreateAuthorizedResponseFailedCode = 2
InvalidScopeErrorCode = 3
InvalidRequestErrorCode = 4
InvalidClientErrorCode = 5
VPFormatsNotSupportedErrorCode = 6
InvalidPresentationDefinitionURIErrorCode = 7
InvalidPresentationDefinitionReferenceErrorCode = 8
OtherAuthorizationResponseErrorCode = 9
)

type errorResponse struct {
Error string `json:"error,omitempty"`
}
85 changes: 64 additions & 21 deletions pkg/openid4vp/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (o *Interaction) GetQuery() (*presexch.PresentationDefinition, error) {
rawRequestObject, err := o.fetchRequestObject()
if err != nil {
return nil, walleterror.NewExecutionError(
module,
ErrorModule,
RequestObjectFetchFailedCode,
RequestObjectFetchFailedError,
fmt.Errorf("fetch request object: %w", err))
Expand All @@ -115,7 +115,7 @@ func (o *Interaction) GetQuery() (*presexch.PresentationDefinition, error) {
requestObject, err := verifyAuthorizationRequestAndDecodeClaims(rawRequestObject, o.signatureVerifier)
if err != nil {
return nil, walleterror.NewExecutionError(
module,
ErrorModule,
VerifyAuthorizationRequestFailedCode,
VerifyAuthorizationRequestFailedError,
fmt.Errorf("verify authorization request: %w", err))
Expand All @@ -133,10 +133,8 @@ func (o *Interaction) GetQuery() (*presexch.PresentationDefinition, error) {
// VerifierDisplayData returns display information about verifier.
func (o *Interaction) VerifierDisplayData() (*VerifierDisplayData, error) {
if o.requestObject == nil {
return nil, walleterror.NewExecutionError(
module,
NotInitializedProperlyErrorCode,
NotInitializedProperlyError,
return nil, walleterror.NewInvalidSDKUsageError(
ErrorModule,
fmt.Errorf("call GetQuery first"))
}

Expand Down Expand Up @@ -176,10 +174,8 @@ func (o *Interaction) presentCredentials(credentials []*verifiable.Credential, o
timeStartPresentCredential := time.Now()

if o.requestObject == nil {
return walleterror.NewExecutionError(
module,
NotInitializedProperlyErrorCode,
NotInitializedProperlyError,
return walleterror.NewInvalidSDKUsageError(
ErrorModule,
fmt.Errorf("call GetQuery first"))
}

Expand All @@ -193,7 +189,7 @@ func (o *Interaction) presentCredentials(credentials []*verifiable.Credential, o
)
if err != nil {
return walleterror.NewExecutionError(
module,
ErrorModule,
CreateAuthorizedResponseFailedCode,
CreateAuthorizedResponseFailedError,
fmt.Errorf("create authorized response failed: %w", err))
Expand All @@ -206,7 +202,7 @@ func (o *Interaction) presentCredentials(credentials []*verifiable.Credential, o

err = o.sendAuthorizedResponse(data.Encode())
if err != nil {
return err
return fmt.Errorf("send authorized response failed: %w", err)
}

err = o.metricsLogger.Log(&api.MetricsEvent{
Expand Down Expand Up @@ -250,16 +246,9 @@ func (o *Interaction) sendAuthorizedResponse(responseBody string) error {
o.requestObject.RedirectURI, "application/x-www-form-urlencoded",
bytes.NewBuffer([]byte(responseBody)),
fmt.Sprintf(sendAuthorizedResponseEventText, o.requestObject.RedirectURI),
presentCredentialEventText, nil)
if err != nil {
return walleterror.NewExecutionError(
module,
SendAuthorizedResponseFailedCode,
SendAuthorizedResponseFailedError,
fmt.Errorf("send authorized response failed: %w", err))
}
presentCredentialEventText, processAuthorizationErrorResponse)

return nil
return err
}

func verifyAuthorizationRequestAndDecodeClaims(
Expand Down Expand Up @@ -568,3 +557,57 @@ func (r *resolverAdapter) Resolve(did string, opts ...vdrspi.DIDMethodOption) (*
func wrapResolver(didResolver api.DIDResolver) *resolverAdapter {
return &resolverAdapter{didResolver: didResolver}
}

func processAuthorizationErrorResponse(statusCode int, respBytes []byte) error {
detailedErr := fmt.Errorf(
"received status code [%d] with body [%s] in response to the authorization request",
statusCode, string(respBytes))

var errResponse errorResponse

err := json.Unmarshal(respBytes, &errResponse)
if err != nil {
return walleterror.NewExecutionError(ErrorModule,
OtherAuthorizationResponseErrorCode,
OtherAuthorizationResponseError,
detailedErr)
}

switch errResponse.Error {
case "invalid_scope":
return walleterror.NewExecutionError(ErrorModule,
InvalidScopeErrorCode,
InvalidScopeError,
detailedErr)
case "invalid_request":
return walleterror.NewExecutionError(ErrorModule,
InvalidRequestErrorCode,
InvalidRequestError,
detailedErr)
case "invalid_client":
return walleterror.NewExecutionError(ErrorModule,
InvalidClientErrorCode,
InvalidClientError,
detailedErr)
case "vp_formats_not_supported":
return walleterror.NewExecutionError(ErrorModule,
VPFormatsNotSupportedErrorCode,
VPFormatsNotSupportedError,
detailedErr)
case "invalid_presentation_definition_uri":
return walleterror.NewExecutionError(ErrorModule,
InvalidPresentationDefinitionURIErrorCode,
InvalidPresentationDefinitionURIError,
detailedErr)
case "invalid_presentation_definition_reference":
return walleterror.NewExecutionError(ErrorModule,
InvalidPresentationDefinitionReferenceErrorCode,
InvalidPresentationDefinitionReferenceError,
detailedErr)
default:
return walleterror.NewExecutionError(ErrorModule,
OtherAuthorizationResponseErrorCode,
OtherAuthorizationResponseError,
detailedErr)
}
}
Loading

0 comments on commit 0613552

Please sign in to comment.