From 363d0e3aab1d91bda0bb476b19883efd1085dee4 Mon Sep 17 00:00:00 2001 From: Mykhailo Sizov Date: Mon, 29 Jan 2024 17:05:29 +0200 Subject: [PATCH] feat: 5.1.1. Request Issuance of a Certain Credential Type using authorization_details Parameter Signed-off-by: Mykhailo Sizov --- api/spec/openapi.gen.go | 352 +++++++------- component/wallet-cli/cmd/oidc4vci_cmd.go | 13 +- component/wallet-cli/pkg/oidc4vci/models.go | 6 +- .../wallet-cli/pkg/oidc4vci/oidc4vci_flow.go | 80 +++- docs/v1/common.yaml | 35 +- docs/v1/openapi.yaml | 40 +- pkg/profile/api.go | 8 +- pkg/restapi/resterr/error.go | 72 +-- pkg/restapi/v1/common/openapi.gen.go | 64 ++- pkg/restapi/v1/issuer/controller.go | 4 +- pkg/restapi/v1/issuer/controller_test.go | 132 +++++- pkg/restapi/v1/issuer/openapi.gen.go | 24 +- pkg/restapi/v1/oidc4ci/controller.go | 61 +-- .../v1/oidc4ci/controller_e2e_flows_test.go | 1 + pkg/restapi/v1/oidc4ci/controller_test.go | 207 +++++++-- pkg/restapi/v1/oidc4ci/openapi.gen.go | 2 +- pkg/restapi/v1/util/validate.go | 45 +- pkg/restapi/v1/util/validate_test.go | 242 +++++++--- pkg/service/oidc4ci/api.go | 23 +- pkg/service/oidc4ci/oidc4ci_service.go | 76 +++- pkg/service/oidc4ci/oidc4ci_service_test.go | 428 +++++++++++++++++- .../wellknown/provider/wellknown_service.go | 9 +- .../oidc4cinoncestore/oidc4vc_store_test.go | 7 +- .../oidc4cinoncestore/oidc4vc_store_test.go | 7 +- test/bdd/pkg/v1/oidc4vc/oidc4vci.go | 20 +- test/stress/pkg/stress/stress_test_case.go | 11 +- 26 files changed, 1448 insertions(+), 521 deletions(-) diff --git a/api/spec/openapi.gen.go b/api/spec/openapi.gen.go index 55623ab17..96db26ffe 100644 --- a/api/spec/openapi.gen.go +++ b/api/spec/openapi.gen.go @@ -19,183 +19,181 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AAD//5zSy+Vx+gAA", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/component/wallet-cli/cmd/oidc4vci_cmd.go b/component/wallet-cli/cmd/oidc4vci_cmd.go index 32b371260..356340974 100644 --- a/component/wallet-cli/cmd/oidc4vci_cmd.go +++ b/component/wallet-cli/cmd/oidc4vci_cmd.go @@ -25,6 +25,7 @@ import ( "github.com/trustbloc/vcs/component/wallet-cli/pkg/oidc4vci" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wallet" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wellknown" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" ) const ( @@ -40,7 +41,7 @@ type oidc4vciCommandFlags struct { demoIssuerURL string vcFormat string credentialType string - credentialFormat string + oidcCredentialFormat vcsverifiable.OIDCFormat walletDIDIndex int clientID string scopes []string @@ -86,7 +87,7 @@ func NewOIDC4VCICommand() *cobra.Command { return fmt.Errorf("--credential-type not set") } - if flags.credentialFormat == "" { + if flags.oidcCredentialFormat == "" { return fmt.Errorf("--credential-format not set") } @@ -171,7 +172,7 @@ func NewOIDC4VCICommand() *cobra.Command { opts := []oidc4vci.Opt{ oidc4vci.WithCredentialType(flags.credentialType), - oidc4vci.WithCredentialFormat(flags.credentialFormat), + oidc4vci.WithOIDCCredentialFormat(flags.oidcCredentialFormat), oidc4vci.WithClientID(flags.clientID), oidc4vci.WithTrustRegistryURL(flags.trustRegistryURL), } @@ -251,6 +252,8 @@ func NewOIDC4VCICommand() *cobra.Command { }, } + var oidcCredentialFormat string + cmd.Flags().StringVar(&flags.serviceFlags.levelDBPath, "leveldb-path", "", "leveldb path") cmd.Flags().StringVar(&flags.serviceFlags.mongoDBConnectionString, "mongodb-connection-string", "", "mongodb connection string") @@ -258,7 +261,7 @@ func NewOIDC4VCICommand() *cobra.Command { cmd.Flags().StringVar(&flags.qrCodePath, "qr-code-path", "", "path to file with qr code") cmd.Flags().StringVar(&flags.credentialOffer, "credential-offer", "", "openid credential offer") cmd.Flags().StringVar(&flags.demoIssuerURL, "demo-issuer-url", "", "demo issuer url for downloading qr code automatically") - cmd.Flags().StringVar(&flags.credentialFormat, "credential-format", "ldp_vc", "supported credential formats: ldp_vc,jwt_vc_json-ld") + cmd.Flags().StringVar(&oidcCredentialFormat, "credential-format", "ldp_vc", "supported credential formats: ldp_vc,jwt_vc_json-ld") cmd.Flags().StringVar(&flags.credentialType, "credential-type", "", "credential type") cmd.Flags().IntVar(&flags.walletDIDIndex, "wallet-did-index", -1, "index of wallet did, if not set the most recently created DID is used") cmd.Flags().StringVar(&flags.clientID, "client-id", "", "vcs oauth2 client") @@ -274,6 +277,8 @@ func NewOIDC4VCICommand() *cobra.Command { cmd.Flags().BoolVar(&flags.enableTracing, "enable-tracing", false, "enables http tracing") cmd.Flags().StringVar(&flags.proxyURL, "proxy-url", "", "proxy url for http client") + flags.oidcCredentialFormat = vcsverifiable.OIDCFormat(oidcCredentialFormat) + return cmd } diff --git a/component/wallet-cli/pkg/oidc4vci/models.go b/component/wallet-cli/pkg/oidc4vci/models.go index 4bd34cb34..ab9f756ac 100644 --- a/component/wallet-cli/pkg/oidc4vci/models.go +++ b/component/wallet-cli/pkg/oidc4vci/models.go @@ -20,9 +20,9 @@ type JWTProofClaims struct { } type CredentialRequest struct { - Format string `json:"format,omitempty"` - Types []string `json:"types"` - Proof JWTProof `json:"proof,omitempty"` + Format verifiable.OIDCFormat `json:"format,omitempty"` + Types []string `json:"types"` + Proof JWTProof `json:"proof,omitempty"` } type JWTProof struct { diff --git a/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go b/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go index 10d115cd5..013553869 100644 --- a/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go +++ b/component/wallet-cli/pkg/oidc4vci/oidc4vci_flow.go @@ -11,6 +11,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "log/slog" @@ -41,6 +42,7 @@ import ( "github.com/trustbloc/vcs/component/wallet-cli/pkg/trustregistry" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wallet" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wellknown" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" kmssigner "github.com/trustbloc/vcs/pkg/kms/signer" "github.com/trustbloc/vcs/pkg/restapi/v1/common" issuerv1 "github.com/trustbloc/vcs/pkg/restapi/v1/issuer" @@ -76,7 +78,8 @@ type Flow struct { flowType FlowType credentialOffer string credentialType string - credentialFormat string + oidcCredentialFormat vcsverifiable.OIDCFormat + credentialConfigurationID string clientID string scopes []string redirectURI string @@ -201,7 +204,8 @@ func NewFlow(p provider, opts ...Opt) (*Flow, error) { flowType: o.flowType, credentialOffer: o.credentialOffer, credentialType: o.credentialType, - credentialFormat: o.credentialFormat, + oidcCredentialFormat: o.oidcCredentialFormat, + credentialConfigurationID: o.credentialConfigurationID, clientID: o.clientID, scopes: o.scopes, redirectURI: o.redirectURI, @@ -220,7 +224,7 @@ func (f *Flow) Run(ctx context.Context) (*verifiable.Credential, error) { "flow_type", f.flowType, "credential_offer_uri", f.credentialOffer, "credential_type", f.credentialType, - "credential_format", f.credentialFormat, + "credential_format", f.oidcCredentialFormat, ) var ( @@ -465,23 +469,17 @@ func (f *Flow) getAuthorizationCode(oauthClient *oauth2.Config, issuerState stri oauthClient.RedirectURL = redirectURI.String() - b, err := json.Marshal(&common.AuthorizationDetails{ - Type: "openid_credential", - Types: []string{ - "VerifiableCredential", - f.credentialType, - }, - Format: lo.ToPtr(f.credentialFormat), - }) + authorizationDetailsRequestBody, err := f.getAuthorizationDetailsRequestBody( + f.credentialType, f.credentialConfigurationID, f.oidcCredentialFormat) if err != nil { - return "", fmt.Errorf("marshal authorization details: %w", err) + return "", fmt.Errorf("getAuthorizationDetailsRequestBody: %w", err) } authCodeOptions := []oauth2.AuthCodeOption{ oauth2.SetAuthURLParam("issuer_state", issuerState), oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"), oauth2.SetAuthURLParam("code_challenge_method", "S256"), - oauth2.SetAuthURLParam("authorization_details", string(b)), + oauth2.SetAuthURLParam("authorization_details", string(authorizationDetailsRequestBody)), } if f.enableDiscoverableClientID { @@ -716,7 +714,7 @@ func (f *Flow) receiveVC( // TODO: take configuration from wellKnown.CredentialsConfigurationSupported[credentialType] b, err := json.Marshal(CredentialRequest{ - Format: f.credentialFormat, + Format: f.oidcCredentialFormat, Types: []string{"VerifiableCredential", f.credentialType}, Proof: JWTProof{ ProofType: "jwt", //TODO: take the value from wellKnown.CredentialsConfigurationSupported[credentialType].ProofTypesSupported @@ -879,6 +877,46 @@ func (f *Flow) handleIssuanceAck( return nil } +// getAuthorizationDetailsRequestBody returns authorization details request body +// either with credential_configuration_id or format params. +// +// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.1 +func (f *Flow) getAuthorizationDetailsRequestBody( + credentialType, credentialConfigurationID string, oidcCredentialFormat vcsverifiable.OIDCFormat, +) ([]byte, error) { + res := make([]common.AuthorizationDetails, 1) // We do not support multiple authorization details for now. + + switch { + case credentialConfigurationID != "": // Priority 1. Based on credentialConfigurationID. + res[0] = common.AuthorizationDetails{ + CredentialConfigurationId: &credentialConfigurationID, + CredentialDefinition: nil, + Format: nil, + Locations: nil, // Not supported for now. + Type: "openid_credential", + } + case oidcCredentialFormat != "": // Priority 2. Based on credentialFormat. + res[0] = common.AuthorizationDetails{ + CredentialConfigurationId: nil, + CredentialDefinition: &common.CredentialDefinition{ + Context: nil, // Not supported for now. + CredentialSubject: nil, // Not supported for now. + Type: []string{ + "VerifiableCredential", + credentialType, + }, + }, + Format: lo.ToPtr(string(oidcCredentialFormat)), + Locations: nil, // Not supported for now. + Type: "openid_credential", + } + default: + return nil, errors.New("neither credentialFormat nor credentialConfigurationID supplied") + } + + return json.Marshal(res) +} + func (f *Flow) PerfInfo() *PerfInfo { return f.perfInfo } @@ -927,7 +965,8 @@ type options struct { proofBuilder JWTProofBuilder credentialOffer string credentialType string - credentialFormat string + oidcCredentialFormat vcsverifiable.OIDCFormat + credentialConfigurationID string clientID string scopes []string redirectURI string @@ -966,9 +1005,9 @@ func WithCredentialType(credentialType string) Opt { } } -func WithCredentialFormat(credentialFormat string) Opt { +func WithOIDCCredentialFormat(oidcCredentialFormat vcsverifiable.OIDCFormat) Opt { return func(opts *options) { - opts.credentialFormat = credentialFormat + opts.oidcCredentialFormat = oidcCredentialFormat } } @@ -1031,3 +1070,10 @@ func WithWalletDIDIndex(idx int) Opt { opts.walletDIDIndex = idx } } + +// WithCredentialConfigurationID adds credentialConfigurationID to authorization request. +func WithCredentialConfigurationID(credentialConfigurationID string) Opt { + return func(opts *options) { + opts.credentialConfigurationID = credentialConfigurationID + } +} diff --git a/docs/v1/common.yaml b/docs/v1/common.yaml index cee64cdb4..2dac53f2d 100644 --- a/docs/v1/common.yaml +++ b/docs/v1/common.yaml @@ -102,14 +102,14 @@ components: type: type: string description: String that determines the authorization details type. MUST be set to "openid_credential" for OIDC4VC. - types: - type: array - items: - type: string - description: String array denoting the types of the requested Credential. format: type: string - description: String representing a format in which the Credential is requested to be issued. Valid values defined by OIDC4VC are jwt_vc_json-ld and ldp_vc. Issuer can refuse the authorization request if the given credential type and format combo is not supported. + description: REQUIRED when CredentialConfigurationId parameter is not present. String identifying the format of the Credential the Wallet needs. This Credential format identifier determines further claims in the authorization details object needed to identify the Credential type in the requested format. It MUST NOT be present if credential_configuration_id parameter is present. + credential_configuration_id: + type: string + description: REQUIRED when Format parameter is not present. String specifying a unique identifier of the Credential being described in the credential_configurations_supported map in the Credential Issuer Metadata. The referenced object in the credential_configurations_supported map conveys the details, such as the format, for issuance of the requested Credential. It MUST NOT be present if format parameter is present. + credential_definition: + $ref: '#/components/schemas/CredentialDefinition' locations: description: An array of strings that allows a client to specify the location of the resource server(s) allowing the Authorization Server to mint audience restricted access tokens. type: array @@ -117,4 +117,25 @@ components: type: string required: - type - - types + CredentialDefinition: + title: CredentialDefinition object definition. + x-tags: + - issuer + type: object + description: Object containing the detailed description of the credential type. + properties: + '@context': + type: array + items: + type: string + description: 'For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts.' + type: + type: array + items: + type: string + description: Array designating the types a certain credential type supports + credentialSubject: + type: object + description: 'An object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. The value can be another such object (nested data structures), or an array of such objects.' + required: + - type \ No newline at end of file diff --git a/docs/v1/openapi.yaml b/docs/v1/openapi.yaml index c42432582..2a5178a23 100644 --- a/docs/v1/openapi.yaml +++ b/docs/v1/openapi.yaml @@ -579,7 +579,7 @@ paths: description: An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery. authorization_details: type: string - description: The authorization_details conveys the details about the credentials the wallet wants to obtain. Multiple authorization_details can be used with type openid_credential to request authorization in case of multiple credentials. + description: Encoded array of authorization_details conveys the details about the credentials the wallet wants to obtain. Multiple authorization_details can be used with type openid_credential to request authorization in case of multiple credentials. wallet_issuer: type: string description: Wallet's OpenID Connect Issuer URL. The Issuer will use the discovery process to determine the wallet's capabilities and endpoints. RECOMMENDED in Dynamic Credential Request. @@ -659,7 +659,7 @@ paths: type: string in: query name: authorization_details - description: The authorization_details conveys the details about the credentials the wallet wants to obtain. Multiple authorization_details can be used with type openid_credential to request authorization in case of multiple credentials. + description: Encoded array of the authorization_details conveys the details about the credentials the wallet wants to obtain. Multiple authorization_details can be used with type openid_credential to request authorization in case of multiple credentials. - schema: type: string in: query @@ -1309,7 +1309,9 @@ components: items: type: string authorization_details: - $ref: ./common.yaml#/components/schemas/AuthorizationDetails + type: array + items: + $ref: './common.yaml#/components/schemas/AuthorizationDetails' op_state: type: string required: @@ -1358,7 +1360,7 @@ components: code: type: string wallet_initiated_flow: - $ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData + $ref: './common.yaml#/components/schemas/WalletInitiatedFlowData' nullable: true required: - op_state @@ -1437,7 +1439,7 @@ components: type: string description: Transaction ID to correlate upcoming authorization response. wallet_initiated_flow: - $ref: ./common.yaml#/components/schemas/WalletInitiatedFlowData + $ref: './common.yaml#/components/schemas/WalletInitiatedFlowData' required: - authorization_request - authorization_endpoint @@ -1767,7 +1769,9 @@ components: op_state: type: string authorization_details: - $ref: ./common.yaml#/components/schemas/AuthorizationDetails + type: array + items: + $ref: './common.yaml#/components/schemas/AuthorizationDetails' required: - op_state - authorization_details @@ -2002,7 +2006,7 @@ components: type: string description: Array of case sensitive strings that identify the cryptographic suites that are supported for the cryptographic_binding_methods_supported. credential_definition: - $ref: '#/components/schemas/CredentialConfigurationsSupportedDefinition' + $ref: './common.yaml#/components/schemas/CredentialDefinition' order: type: array items: @@ -2029,28 +2033,6 @@ components: $ref: '#/components/schemas/CredentialDisplay' required: - format - CredentialConfigurationsSupportedDefinition: - title: CredentialConfigurationsSupportedDefinition object definition. - x-tags: - - issuer - type: object - description: Object containing the detailed description of the credential type. - properties: - '@context': - type: array - items: - type: string - description: 'For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts.' - type: - type: array - items: - type: string - description: Array designating the types a certain credential type supports - credentialSubject: - type: object - description: 'An object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. The value can be another such object (nested data structures), or an array of such objects.' - required: - - type CredentialResponseEncryptionSupported: title: CredentialResponseEncryption object definition. x-tags: diff --git a/pkg/profile/api.go b/pkg/profile/api.go index 8f5558554..d11de8323 100644 --- a/pkg/profile/api.go +++ b/pkg/profile/api.go @@ -66,7 +66,7 @@ type CredentialsConfigurationSupported struct { Claims map[string]interface{} `json:"claims"` // Object containing the detailed description of the credential type. - CredentialDefinition *CredentialConfigurationsSupportedDefinition `json:"credential_definition"` + CredentialDefinition *CredentialDefinition `json:"credential_definition"` // An array of objects, where each object contains the display properties // of the supported credential for a certain language. @@ -76,7 +76,7 @@ type CredentialsConfigurationSupported struct { Doctype string `json:"doctype"` // A JSON string identifying the format of this credential, i.e., jwt_vc_json or ldp_vc. - Format string `json:"format"` + Format vcsverifiable.OIDCFormat `json:"format"` // Array of the claim name values that lists them in the order they should be displayed by the Wallet. Order []string `json:"order"` @@ -89,8 +89,8 @@ type CredentialsConfigurationSupported struct { Vct string `json:"vct"` } -// CredentialConfigurationsSupportedDefinition containing the detailed description of the credential type. -type CredentialConfigurationsSupportedDefinition struct { +// CredentialDefinition containing the detailed description of the credential type. +type CredentialDefinition struct { // For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts. Context []string `json:"@context"` diff --git a/pkg/restapi/resterr/error.go b/pkg/restapi/resterr/error.go index 24e4a5e69..d84d64c91 100644 --- a/pkg/restapi/resterr/error.go +++ b/pkg/restapi/resterr/error.go @@ -33,28 +33,29 @@ const ( OIDCClientAuthenticationFailed ErrorCode = "oidc-client-authentication-failed" 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" - ClaimsValidationErr ErrorCode = "invalid-claims" - 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" - InvalidStateTransition ErrorCode = "invalid-state-transition" + 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" + ClaimsValidationErr ErrorCode = "invalid-claims" + 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" + InvalidCredentialConfigurationID ErrorCode = "invalid-credential-configuration-id" + CredentialTypeNotSupported ErrorCode = "credential-type-not-supported" + CredentialFormatNotSupported ErrorCode = "credential-format-not-supported" + VCOptionsNotConfigured ErrorCode = "vc-options-not-configured" + InvalidIssuerURL ErrorCode = "invalid-issuer-url" + InvalidStateTransition ErrorCode = "invalid-state-transition" ) type Component = string @@ -87,19 +88,20 @@ const ( ) 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")) + 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 + ErrInvalidCredentialConfigurationID = NewCustomError(InvalidCredentialConfigurationID, errors.New("invalid credential configuration ID")) //nolint:lll + ErrVCOptionsNotConfigured = NewCustomError(VCOptionsNotConfigured, errors.New("vc options not configured")) + ErrInvalidIssuerURL = NewCustomError(InvalidIssuerURL, errors.New("invalid issuer url")) ) func (c ErrorCode) Name() string { diff --git a/pkg/restapi/v1/common/openapi.gen.go b/pkg/restapi/v1/common/openapi.gen.go index aab95ad09..bb0bede38 100644 --- a/pkg/restapi/v1/common/openapi.gen.go +++ b/pkg/restapi/v1/common/openapi.gen.go @@ -44,7 +44,13 @@ const ( // Model to convey the details about the Credentials the Client wants to obtain. type AuthorizationDetails struct { - // String representing a format in which the Credential is requested to be issued. Valid values defined by OIDC4VC are jwt_vc_json-ld and ldp_vc. Issuer can refuse the authorization request if the given credential type and format combo is not supported. + // REQUIRED when Format parameter is not present. String specifying a unique identifier of the Credential being described in the credential_configurations_supported map in the Credential Issuer Metadata. The referenced object in the credential_configurations_supported map conveys the details, such as the format, for issuance of the requested Credential. It MUST NOT be present if format parameter is present. + CredentialConfigurationId *string `json:"credential_configuration_id,omitempty"` + + // Object containing the detailed description of the credential type. + CredentialDefinition *CredentialDefinition `json:"credential_definition,omitempty"` + + // REQUIRED when CredentialConfigurationId parameter is not present. String identifying the format of the Credential the Wallet needs. This Credential format identifier determines further claims in the authorization details object needed to identify the Credential type in the requested format. It MUST NOT be present if credential_configuration_id parameter is present. Format *string `json:"format,omitempty"` // An array of strings that allows a client to specify the location of the resource server(s) allowing the Authorization Server to mint audience restricted access tokens. @@ -52,9 +58,18 @@ type AuthorizationDetails struct { // String that determines the authorization details type. MUST be set to "openid_credential" for OIDC4VC. Type string `json:"type"` +} + +// Object containing the detailed description of the credential type. +type CredentialDefinition struct { + // For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts. + Context *[]string `json:"@context,omitempty"` + + // An object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. The value can be another such object (nested data structures), or an array of such objects. + CredentialSubject *map[string]interface{} `json:"credentialSubject,omitempty"` - // String array denoting the types of the requested Credential. - Types []string `json:"types"` + // Array designating the types a certain credential type supports + Type []string `json:"type"` } // DID method of the DID to be used for signing. @@ -103,25 +118,30 @@ type WalletInitiatedFlowData struct { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/5xWQW/bOBP9KwOeWkBxiq/fybes3AJGkq1RN+5hWxgUObIYU6SWHNnxFvnvi6Gs2LHl", - "ot2LYZGcN29mHmf4QyhfN96hoyjGP0RUFdYy/b1pqfLB/CPJeDdBksamdY1RBdPwqhiLe6/RAnlQ3m1w", - "B1Qh6O4wyMK3lFbygBodGWlj920NOoKtdBTZ2BckjRuJTDTBNxjIYPJV+lBLOvc6p2DcCgI2ASMDuxVI", - "6E6DcbCtjKpOPIOJEPDvFiOhZqcFgomxRT2ChbRGw0baFiNoLI1DDcUOPk0n+f8XOciA8Lil5UYtH6N3", - "V1aDdBqsbpYbNYIpwwRQ0kHAso2YXMvjBPauwZRpc2U26EAd2NGuwQS6j0L5uvDM2XmC2DaND4SaU8Qn", - "xVjElAPxnAnrVfIxUJ4bBzIEuQNfQmfABZAE0lq/jSBBdaUgD7FBZcquhD0k2/F3wOjboBAihg2GN/Ft", - "h8CJ5/1XYoF5OsSYtXEEstUGnUooFIzi/EulMHLt1+giR2UI6xTAWXj7hRTH4fuCJFJwGglDbRzGgUL0", - "6mSYEdw/zL+wEiKmHHwTvkFn9PJQmW+CS9JLYbAAvBAvUuoKoNF56tOVDA657UV5EOvvZOQ5EwxhAmox", - "/qvb7Dl9zwQZsnx68D6/YPniERUx+GQ6uUeqvD4PaDKdQJ32eu680l2lNmLSLkSzcsatOAJ0bc2UfChE", - "JrbIv2vcJVanMd3ez3PvSrO61GMY+/Z+zo2mNKs2pDjOW4YuZgFL83QO060zcy1JFjLuSRe7JHcL6zoO", - "llcXXwYlx6v/Ce7h89052sPnO07lb4Kh0403bqBHcq763UHTiCog3Xm1vsXdTFI1kDJJVWoN6ShTWf8i", - "L/ppxtZ17HB4cASU1LW+SD50mlrjLh4rKDl70ZDcxgENDd2DI/0fBHYq+kw8XZFcRbZKEyGI78+ZWOQf", - "L42fvh3DIt/361dsX48KkR0viEx0Y+OY24urgUwuZr9AY3aRRtM7bF45nF12+FVaizR1howk1B+t304k", - "SfbvWmtlwQgUWjy9espKUy+PFXmGfWirS8K6sZJwafTgUd8sI0nCwc0m+NLYi7b99gZDTLkaOBOV33ft", - "y432NN6fNt4jTucMXvxlp2m6mJSjFBxV7lJ1zjo50zOu9AO3MLSR/rBewSKf92+L47cIXwLJA5sv5QaD", - "Kc3+OdBGHmFf3+ewyK9uZlOQ1rsVbA1V8KlBN53wc6kJnrzyXc/ubtR1gsEAxhEGqRJaMusCYt1ao9DF", - "VHAn6zSzGqkqvPrf6J3IRBusGIuKqInj6+vtdjuSaXvkw+p6bxuv76b5hz/nH9hmRE9povWpy31de7cf", - "vkxtkULjAh+/E/n1YhTCm0U+fysy8SIi8W7ETJI20cnGiLF4P3qXyDWSqijGLJjnfwMAAP//r/R+MlUL", - "AAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/restapi/v1/issuer/controller.go b/pkg/restapi/v1/issuer/controller.go index 1ff470075..c47bd1c64 100644 --- a/pkg/restapi/v1/issuer/controller.go +++ b/pkg/restapi/v1/issuer/controller.go @@ -536,7 +536,7 @@ func (c *Controller) PushAuthorizationDetails(ctx echo.Context) error { return err } - ad, err := util.ValidateAuthorizationDetails(&body.AuthorizationDetails) + ad, err := util.ValidateAuthorizationDetails(body.AuthorizationDetails) if err != nil { return err } @@ -572,7 +572,7 @@ func (c *Controller) prepareClaimDataAuthorizationRequest( ctx context.Context, body *PrepareClaimDataAuthorizationRequest, ) (*PrepareClaimDataAuthorizationResponse, error) { - ad, err := util.ValidateAuthorizationDetails(body.AuthorizationDetails) + ad, err := util.ValidateAuthorizationDetails(*body.AuthorizationDetails) if err != nil { return nil, err } diff --git a/pkg/restapi/v1/issuer/controller_test.go b/pkg/restapi/v1/issuer/controller_test.go index f3c51054b..790ddf8fe 100644 --- a/pkg/restapi/v1/issuer/controller_test.go +++ b/pkg/restapi/v1/issuer/controller_test.go @@ -975,18 +975,51 @@ func TestController_InitiateCredentialIssuance(t *testing.T) { func TestController_PushAuthorizationDetails(t *testing.T) { var ( - mockOIDC4CISvc = NewMockOIDC4CIService(gomock.NewController(t)) - req string + mockOIDC4CISvc = NewMockOIDC4CIService(gomock.NewController(t)) + req string + authorizationDetailsFormatBased = `[{ + "type": "openid_credential", + "format": "ldp_vc", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + } + }]` + authorizationDetailsCredentialConfigurationIDBased = `[{ + "type": "openid_credential", + "credential_configuration_id": "UniversityDegreeCredential" + }]` ) - t.Run("Success", func(t *testing.T) { + t.Run("Success: AuthorizationDetails contains Format field", func(t *testing.T) { mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Return(nil) controller := NewController(&Config{ OIDC4CIService: mockOIDC4CISvc, }) - req = `{"op_state":"opState","authorization_details":{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}}` //nolint:lll + req = fmt.Sprintf(`{"op_state":"opState","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll + c := echoContext(withRequestBody([]byte(req))) + + err := controller.PushAuthorizationDetails(c) + require.NoError(t, err) + }) + + t.Run("Success: AuthorizationDetails contains CredentialConfigurationID field", func(t *testing.T) { + mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Return(nil) + + controller := NewController(&Config{ + OIDC4CIService: mockOIDC4CISvc, + }) + + req = fmt.Sprintf(`{"op_state":"opState","authorization_details":%s}`, authorizationDetailsCredentialConfigurationIDBased) //nolint:lll c := echoContext(withRequestBody([]byte(req))) err := controller.PushAuthorizationDetails(c) @@ -1004,7 +1037,7 @@ func TestController_PushAuthorizationDetails(t *testing.T) { setup: func() { mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Times(0) - req = `{"op_state":"opState","authorization_details":{"type":"invalid","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}}` //nolint:lll + req = `{"op_state":"opState","authorization_details":[{"type":"invalid","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}]}` //nolint:lll }, check: func(t *testing.T, err error) { require.ErrorContains(t, err, "type should be 'openid_credential'") @@ -1016,7 +1049,7 @@ func TestController_PushAuthorizationDetails(t *testing.T) { mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Return( resterr.ErrCredentialTypeNotSupported) - req = `{"op_state":"opState","authorization_details":{"type":"openid_credential"}}` + req = fmt.Sprintf(`{"op_state":"opState","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll }, check: func(t *testing.T, err error) { require.ErrorContains(t, err, "credential type not supported") @@ -1028,7 +1061,7 @@ func TestController_PushAuthorizationDetails(t *testing.T) { mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Return( resterr.ErrCredentialFormatNotSupported) - req = `{"op_state":"opState","authorization_details":{"type":"openid_credential"}}` + req = fmt.Sprintf(`{"op_state":"opState","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll }, check: func(t *testing.T, err error) { require.ErrorContains(t, err, "credential format not supported") @@ -1040,7 +1073,7 @@ func TestController_PushAuthorizationDetails(t *testing.T) { mockOIDC4CISvc.EXPECT().PushAuthorizationDetails(gomock.Any(), "opState", gomock.Any()).Return( errors.New("service error")) - req = `{"op_state":"opState","authorization_details":{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}}` //nolint:lll + req = fmt.Sprintf(`{"op_state":"opState","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll }, check: func(t *testing.T, err error) { require.ErrorContains(t, err, "service error") @@ -1065,7 +1098,29 @@ func TestController_PushAuthorizationDetails(t *testing.T) { } func TestController_PrepareAuthorizationRequest(t *testing.T) { - t.Run("success", func(t *testing.T) { + var ( + authorizationDetailsFormatBased = `[{ + "type": "openid_credential", + "format": "ldp_vc", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + } + }]` + authorizationDetailsCredentialConfigurationIDBased = `[{ + "type": "openid_credential", + "credential_configuration_id": "UniversityDegreeCredential" + }]` + ) + + t.Run("success format based", func(t *testing.T) { mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) mockOIDC4CIService.EXPECT().PrepareClaimDataAuthorizationRequest(gomock.Any(), gomock.Any()).DoAndReturn( func( @@ -1074,6 +1129,55 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) { ) (*oidc4ci.PrepareClaimDataAuthorizationResponse, error) { assert.Equal(t, "123", req.OpState) + ad := req.AuthorizationDetails + assert.NotEmpty(t, ad) + assert.Equal(t, ad.Type, "openid_credential") + assert.Equal(t, ad.Format, vcsverifiable.Ldp) + assert.Nil(t, ad.Locations) + assert.Empty(t, ad.CredentialConfigurationID) + assert.Nil(t, ad.CredentialDefinition.Context) + assert.NotNil(t, ad.CredentialDefinition.CredentialSubject) + assert.Equal(t, ad.CredentialDefinition.Type, []string{"VerifiableCredential", "UniversityDegreeCredential"}) + + return &oidc4ci.PrepareClaimDataAuthorizationResponse{ + ProfileID: profileID, + ProfileVersion: profileVersion, + }, nil + }, + ) + + mockProfileService := NewMockProfileService(gomock.NewController(t)) + mockProfileService.EXPECT().GetProfile(profileID, profileVersion).Return(&profileapi.Issuer{ + OIDCConfig: &profileapi.OIDCConfig{}, + }, nil) + + c := &Controller{ + oidc4ciService: mockOIDC4CIService, + profileSvc: mockProfileService, + } + + req := fmt.Sprintf(`{"response_type":"code","op_state":"123","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll + ctx := echoContext(withRequestBody([]byte(req))) + assert.NoError(t, c.PrepareAuthorizationRequest(ctx)) + }) + + t.Run("success credentialConfigurationID based", func(t *testing.T) { + mockOIDC4CIService := NewMockOIDC4CIService(gomock.NewController(t)) + mockOIDC4CIService.EXPECT().PrepareClaimDataAuthorizationRequest(gomock.Any(), gomock.Any()).DoAndReturn( + func( + ctx context.Context, + req *oidc4ci.PrepareClaimDataAuthorizationRequest, + ) (*oidc4ci.PrepareClaimDataAuthorizationResponse, error) { + assert.Equal(t, "123", req.OpState) + + ad := req.AuthorizationDetails + assert.NotEmpty(t, ad) + assert.Equal(t, ad.Type, "openid_credential") + assert.Empty(t, ad.Format) + assert.Nil(t, ad.Locations) + assert.Equal(t, "UniversityDegreeCredential", ad.CredentialConfigurationID) + assert.Nil(t, ad.CredentialDefinition) + return &oidc4ci.PrepareClaimDataAuthorizationResponse{ ProfileID: profileID, ProfileVersion: profileVersion, @@ -1091,7 +1195,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) { profileSvc: mockProfileService, } - req := `{"response_type":"code","op_state":"123","authorization_details":{"type":"openid_credential","credential_type":"https://did.example.org/healthCard","format":"ldp_vc","locations":[]}}` //nolint:lll + req := fmt.Sprintf(`{"response_type":"code","op_state":"123","authorization_details":%s}`, authorizationDetailsCredentialConfigurationIDBased) //nolint:lll ctx := echoContext(withRequestBody([]byte(req))) assert.NoError(t, c.PrepareAuthorizationRequest(ctx)) }) @@ -1104,7 +1208,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) { oidc4ciService: mockOIDC4CIService, } - req := `{"response_type":"code","op_state":"123","authorization_details":{"type":"invalid","credential_type":"https://did.example.org/healthCard","format":"ldp_vc","locations":[]}}` //nolint:lll + req := `{"response_type":"code","op_state":"123","authorization_details":[{"type":"invalid","credential_type":"https://did.example.org/healthCard","format":"ldp_vc","locations":[]}]}` //nolint:lll ctx := echoContext(withRequestBody([]byte(req))) assert.ErrorContains(t, c.PrepareAuthorizationRequest(ctx), "authorization_details.type") }) @@ -1117,7 +1221,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) { oidc4ciService: mockOIDC4CIService, } - req := `{"response_type":"code","op_state":"123","authorization_details":{"type":"openid_credential","credential_type":"https://did.example.org/healthCard","format":"invalid","locations":[]}}` //nolint:lll + req := `{"response_type":"code","op_state":"123","authorization_details":[{"type":"openid_credential","credential_type":"https://did.example.org/healthCard","format":"invalid","locations":[]}]}` //nolint:lll ctx := echoContext(withRequestBody([]byte(req))) assert.ErrorContains(t, c.PrepareAuthorizationRequest(ctx), "authorization_details.format") }) @@ -1131,7 +1235,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) { oidc4ciService: mockOIDC4CIService, } - req := `{"response_type":"code","op_state":"123","authorization_details":{"type":"openid_credential","credential_type":"https://did.example.org/healthCard","format":"ldp_vc","locations":[]}}` //nolint:lll + req := fmt.Sprintf(`{"response_type":"code","op_state":"123","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll ctx := echoContext(withRequestBody([]byte(req))) assert.ErrorContains(t, c.PrepareAuthorizationRequest(ctx), "service error") }) @@ -1160,7 +1264,7 @@ func TestController_PrepareAuthorizationRequest(t *testing.T) { profileSvc: mockProfileService, } - req := `{"response_type":"code","op_state":"123","authorization_details":{"type":"openid_credential","credential_type":"https://did.example.org/healthCard","format":"ldp_vc","locations":[]}}` //nolint:lll + req := fmt.Sprintf(`{"response_type":"code","op_state":"123","authorization_details":%s}`, authorizationDetailsFormatBased) //nolint:lll ctx := echoContext(withRequestBody([]byte(req))) assert.ErrorContains(t, c.PrepareAuthorizationRequest(ctx), "get profile error") }) diff --git a/pkg/restapi/v1/issuer/openapi.gen.go b/pkg/restapi/v1/issuer/openapi.gen.go index 155b8cd9b..eda8b19d2 100644 --- a/pkg/restapi/v1/issuer/openapi.gen.go +++ b/pkg/restapi/v1/issuer/openapi.gen.go @@ -26,7 +26,7 @@ type CredentialConfigurationsSupported struct { Claims *map[string]interface{} `json:"claims,omitempty"` // Object containing the detailed description of the credential type. - CredentialDefinition *CredentialConfigurationsSupportedDefinition `json:"credential_definition,omitempty"` + CredentialDefinition *externalRef0.CredentialDefinition `json:"credential_definition,omitempty"` // Array of case sensitive strings that identify how the Credential is bound to the identifier of the End-User who possesses the Credential. CryptographicBindingMethodsSupported *[]string `json:"cryptographic_binding_methods_supported,omitempty"` @@ -56,18 +56,6 @@ type CredentialConfigurationsSupported struct { Vct *string `json:"vct,omitempty"` } -// Object containing the detailed description of the credential type. -type CredentialConfigurationsSupportedDefinition struct { - // For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts. - Context *[]string `json:"@context,omitempty"` - - // An object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. The value can be another such object (nested data structures), or an array of such objects. - CredentialSubject *map[string]interface{} `json:"credentialSubject,omitempty"` - - // Array designating the types a certain credential type supports - Type []string `json:"type"` -} - // CredentialDisplay defines model for CredentialDisplay. type CredentialDisplay struct { BackgroundColor *string `json:"background_color,omitempty"` @@ -257,9 +245,8 @@ type OAuthParameters struct { // Model for Prepare Claim Data Authorization Request. type PrepareClaimDataAuthorizationRequest struct { - // Model to convey the details about the Credentials the Client wants to obtain. - AuthorizationDetails *externalRef0.AuthorizationDetails `json:"authorization_details,omitempty"` - OpState string `json:"op_state"` + AuthorizationDetails *[]externalRef0.AuthorizationDetails `json:"authorization_details,omitempty"` + OpState string `json:"op_state"` // Value MUST be set to "code". ResponseType string `json:"response_type"` @@ -324,9 +311,8 @@ type PrepareCredentialResult struct { // Model for Push Authorization Details request. type PushAuthorizationDetailsRequest struct { - // Model to convey the details about the Credentials the Client wants to obtain. - AuthorizationDetails externalRef0.AuthorizationDetails `json:"authorization_details"` - OpState string `json:"op_state"` + AuthorizationDetails []externalRef0.AuthorizationDetails `json:"authorization_details"` + OpState string `json:"op_state"` } // Object containing requested information for encrypting the Credential Response. diff --git a/pkg/restapi/v1/oidc4ci/controller.go b/pkg/restapi/v1/oidc4ci/controller.go index d56540396..65fa9087b 100644 --- a/pkg/restapi/v1/oidc4ci/controller.go +++ b/pkg/restapi/v1/oidc4ci/controller.go @@ -180,26 +180,21 @@ func (c *Controller) OidcPushedAuthorizationRequest(e echo.Context) error { return resterr.NewFositeError(resterr.FositePARError, e, c.oauth2Provider, err).WithAuthorizeRequester(ar) } - var ad common.AuthorizationDetails + var ad []common.AuthorizationDetails if err = json.Unmarshal([]byte(par.AuthorizationDetails), &ad); err != nil { return resterr.NewValidationError(resterr.InvalidValue, "authorization_details", err) } - authorizationDetails, err := apiUtil.ValidateAuthorizationDetails(&ad) + _, err = apiUtil.ValidateAuthorizationDetails(ad) if err != nil { return err } r, err := c.issuerInteractionClient.PushAuthorizationDetails(ctx, issuer.PushAuthorizationDetailsJSONRequestBody{ - AuthorizationDetails: common.AuthorizationDetails{ - Types: authorizationDetails.Types, - Format: lo.ToPtr(string(authorizationDetails.Format)), - Locations: lo.ToPtr(authorizationDetails.Locations), - Type: authorizationDetails.Type, - }, - OpState: par.OpState, + AuthorizationDetails: ad, + OpState: par.OpState, }, ) if err != nil { @@ -250,42 +245,48 @@ func (c *Controller) OidcAuthorize(e echo.Context, params OidcAuthorizeParams) e }, } - var ( - credentialType []string - vcFormat *string - ) - scope := []string(ar.GetRequestedScopes()) - if params.AuthorizationDetails != nil { - var authorizationDetails common.AuthorizationDetails + var prepareAuthRequestAuthorizationDetails common.AuthorizationDetails + if params.AuthorizationDetails != nil { + var authorizationDetails []common.AuthorizationDetails if err = json.Unmarshal([]byte(*params.AuthorizationDetails), &authorizationDetails); err != nil { return resterr.NewValidationError(resterr.InvalidValue, "authorization_details", err) } - if _, err = apiUtil.ValidateAuthorizationDetails(&authorizationDetails); err != nil { + if _, err = apiUtil.ValidateAuthorizationDetails(authorizationDetails); err != nil { return err } - credentialType = authorizationDetails.Types - vcFormat = authorizationDetails.Format + // only single authorization_details supported for now. + prepareAuthRequestAuthorizationDetails = authorizationDetails[0] } else { - // using scope parameter to request credential type - // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-using-scope-parameter-to-re - credentialType = scope + // TODO: implement using scope parameter to request credential type + // https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.2 + + // prepareAuthRequestAuthorizationDetails = common.AuthorizationDetails{ + // CredentialConfigurationId: nil, + // CredentialDefinition: &common.CredentialDefinition{ + // Context: nil, // Not supported for now. + // CredentialSubject: nil, // Not supported for now. + // Type: scope, + // }, + // Format: nil, + // Locations: nil, // Not supported for now. + // Type: "openid_credential", + // } + + return resterr.NewValidationError(resterr.InvalidValue, "authorization_details", + errors.New("not supplied")) } r, err := c.issuerInteractionClient.PrepareAuthorizationRequest(ctx, issuer.PrepareAuthorizationRequestJSONRequestBody{ - AuthorizationDetails: &common.AuthorizationDetails{ - Type: "openid_credential", - Types: credentialType, - Format: vcFormat, - }, - OpState: lo.FromPtr(params.IssuerState), - ResponseType: params.ResponseType, - Scope: lo.ToPtr(scope), + AuthorizationDetails: lo.ToPtr([]common.AuthorizationDetails{prepareAuthRequestAuthorizationDetails}), + OpState: lo.FromPtr(params.IssuerState), + ResponseType: params.ResponseType, + Scope: lo.ToPtr(scope), }, ) if err != nil { diff --git a/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go b/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go index c44269c2f..a10acdb89 100644 --- a/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_e2e_flows_test.go @@ -136,6 +136,7 @@ func TestAuthorizeCodeGrantFlow(t *testing.T) { oauth2.SetAuthURLParam("code_challenge_method", "S256"), oauth2.SetAuthURLParam("code_challenge", "MLSjJIlPzeRQoN9YiIsSzziqEuBSmS4kDgI3NDjbfF8"), oauth2.SetAuthURLParam("issuer_state", opState), + oauth2.SetAuthURLParam("authorization_details", authorizationDetailsCredentialConfigurationIDBased), } authCodeURL := oauthClient.AuthCodeURL(opState, params...) diff --git a/pkg/restapi/v1/oidc4ci/controller_test.go b/pkg/restapi/v1/oidc4ci/controller_test.go index 66c0f82a6..0cbd73739 100644 --- a/pkg/restapi/v1/oidc4ci/controller_test.go +++ b/pkg/restapi/v1/oidc4ci/controller_test.go @@ -56,6 +56,30 @@ const ( profileVersion = "v1.0" ) +var ( + //nolint:gochecknoglobals + authorizationDetailsFormatBased = `[{ + "type": "openid_credential", + "format": "ldp_vc", + "credential_definition": { + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ], + "credentialSubject": { + "given_name": {}, + "family_name": {}, + "degree": {} + } + } + }]` + //nolint:gochecknoglobals + authorizationDetailsCredentialConfigurationIDBased = `[{ + "type": "openid_credential", + "credential_configuration_id": "UniversityDegreeCredential" + }]` +) + func TestController_OidcPushedAuthorizationRequest(t *testing.T) { var ( mockOAuthProvider = NewMockOAuth2Provider(gomock.NewController(t)) @@ -69,7 +93,29 @@ func TestController_OidcPushedAuthorizationRequest(t *testing.T) { check func(t *testing.T, rec *httptest.ResponseRecorder, err error) }{ { - name: "success", + name: "success: AuthorizationDetails contains Format field", + setup: func() { + mockOAuthProvider.EXPECT().NewPushedAuthorizeRequest(gomock.Any(), gomock.Any()).Return(&fosite.AuthorizeRequest{}, nil) + mockOAuthProvider.EXPECT().NewPushedAuthorizeResponse(gomock.Any(), gomock.Any(), gomock.Any()).Return(&fosite.PushedAuthorizeResponse{}, nil) + mockOAuthProvider.EXPECT().WritePushedAuthorizeResponse(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) + + mockInteractionClient.EXPECT().PushAuthorizationDetails(gomock.Any(), gomock.Any()).Return( + &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(nil)), + }, nil) + + q = url.Values{} + q.Add("op_state", "opState") + q.Add("authorization_details", authorizationDetailsFormatBased) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.NoError(t, err) + require.Equal(t, http.StatusOK, rec.Code) + }, + }, + { + name: "success: AuthorizationDetails contains CredentialConfigurationID field", setup: func() { mockOAuthProvider.EXPECT().NewPushedAuthorizeRequest(gomock.Any(), gomock.Any()).Return(&fosite.AuthorizeRequest{}, nil) mockOAuthProvider.EXPECT().NewPushedAuthorizeResponse(gomock.Any(), gomock.Any(), gomock.Any()).Return(&fosite.PushedAuthorizeResponse{}, nil) @@ -83,7 +129,7 @@ func TestController_OidcPushedAuthorizationRequest(t *testing.T) { q = url.Values{} q.Add("op_state", "opState") - q.Add("authorization_details", `{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`) + q.Add("authorization_details", authorizationDetailsCredentialConfigurationIDBased) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.NoError(t, err) @@ -120,7 +166,7 @@ func TestController_OidcPushedAuthorizationRequest(t *testing.T) { q = url.Values{} q.Add("op_state", "opState") - q.Add("authorization_details", `{"type":"invalid","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`) + q.Add("authorization_details", `[{"type":"invalid","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}]`) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.ErrorContains(t, err, "type should be 'openid_credential'") @@ -134,7 +180,7 @@ func TestController_OidcPushedAuthorizationRequest(t *testing.T) { q = url.Values{} q.Add("op_state", "opState") - q.Add("authorization_details", `{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`) + q.Add("authorization_details", authorizationDetailsFormatBased) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.ErrorContains(t, err, "push authorization details error") @@ -153,7 +199,7 @@ func TestController_OidcPushedAuthorizationRequest(t *testing.T) { q = url.Values{} q.Add("op_state", "opState") - q.Add("authorization_details", `{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`) + q.Add("authorization_details", authorizationDetailsFormatBased) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.ErrorContains(t, err, "push authorization details: status code") @@ -173,7 +219,7 @@ func TestController_OidcPushedAuthorizationRequest(t *testing.T) { q = url.Values{} q.Add("op_state", "opState") - q.Add("authorization_details", `{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`) + q.Add("authorization_details", authorizationDetailsFormatBased) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { require.ErrorContains(t, err, "new pushed authorize response error") @@ -217,7 +263,7 @@ func TestController_OidcAuthorize(t *testing.T) { check func(t *testing.T, rec *httptest.ResponseRecorder, err error) }{ { - name: "success", + name: "success format based", setup: func() { state := "state" @@ -225,7 +271,7 @@ func TestController_OidcAuthorize(t *testing.T) { ResponseType: "code", State: &state, IssuerState: lo.ToPtr("opState"), - AuthorizationDetails: lo.ToPtr(`{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -257,6 +303,74 @@ func TestController_OidcAuthorize(t *testing.T) { req issuer.PrepareAuthorizationRequestJSONRequestBody, reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { + var authorizationDetails *[]common.AuthorizationDetails + err = json.Unmarshal([]byte(authorizationDetailsFormatBased), &authorizationDetails) + assert.NoError(t, err) + assert.Equal(t, authorizationDetails, req.AuthorizationDetails) + assert.Equal(t, params.ResponseType, req.ResponseType) + assert.Equal(t, *params.IssuerState, req.OpState) + assert.Equal(t, lo.ToPtr(scope), req.Scope) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBuffer(b)), + }, nil + }) + + mockStateStore.EXPECT().SaveAuthorizeState(gomock.Any(), *params.IssuerState, gomock.Any()). + Return(nil) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.NoError(t, err) + require.Equal(t, http.StatusSeeOther, rec.Code) + require.NotEmpty(t, rec.Header().Get("Location")) + }, + }, + { + name: "success CredentialConfigurationId based", + setup: func() { + state := "state" + + params = oidc4ci.OidcAuthorizeParams{ + ResponseType: "code", + State: &state, + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsCredentialConfigurationIDBased), + } + + scope := []string{"openid", "profile"} + + mockOAuthProvider.EXPECT().NewAuthorizeRequest(gomock.Any(), gomock.Any()).Return(&fosite.AuthorizeRequest{ + Request: fosite.Request{RequestedScope: scope}, + }, nil) + + mockOAuthProvider.EXPECT().NewAuthorizeResponse(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func( + ctx context.Context, + ar fosite.AuthorizeRequester, + session fosite.Session, + ) (fosite.AuthorizeResponder, error) { + assert.Equal(t, *params.State, ar.(*fosite.AuthorizeRequest).State) + + return &fosite.AuthorizeResponse{}, nil + }, + ) + + b, err := json.Marshal(&issuer.PrepareClaimDataAuthorizationResponse{ + AuthorizationRequest: issuer.OAuthParameters{}, + }) + require.NoError(t, err) + + mockInteractionClient.EXPECT().PrepareAuthorizationRequest(gomock.Any(), gomock.Any()). + DoAndReturn(func( + ctx context.Context, + req issuer.PrepareAuthorizationRequestJSONRequestBody, + reqEditors ...issuer.RequestEditorFn, + ) (*http.Response, error) { + var authorizationDetails *[]common.AuthorizationDetails + err = json.Unmarshal([]byte(authorizationDetailsCredentialConfigurationIDBased), &authorizationDetails) + assert.NoError(t, err) + assert.Equal(t, authorizationDetails, req.AuthorizationDetails) assert.Equal(t, params.ResponseType, req.ResponseType) assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) @@ -285,7 +399,7 @@ func TestController_OidcAuthorize(t *testing.T) { ResponseType: "code", State: &state, IssuerState: lo.ToPtr("https://some.issuer"), - AuthorizationDetails: lo.ToPtr(`{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"ldp_vc"}`), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -320,6 +434,10 @@ func TestController_OidcAuthorize(t *testing.T) { req issuer.PrepareAuthorizationRequestJSONRequestBody, reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { + var authorizationDetails *[]common.AuthorizationDetails + err = json.Unmarshal([]byte(authorizationDetailsFormatBased), &authorizationDetails) + assert.NoError(t, err) + assert.Equal(t, authorizationDetails, req.AuthorizationDetails) assert.Equal(t, params.ResponseType, req.ResponseType) assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) @@ -343,8 +461,9 @@ func TestController_OidcAuthorize(t *testing.T) { name: "success with par", setup: func() { params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -380,6 +499,10 @@ func TestController_OidcAuthorize(t *testing.T) { req issuer.PrepareAuthorizationRequestJSONRequestBody, reqEditors ...issuer.RequestEditorFn, ) (*http.Response, error) { + var authorizationDetails *[]common.AuthorizationDetails + err = json.Unmarshal([]byte(authorizationDetailsFormatBased), &authorizationDetails) + assert.NoError(t, err) + assert.Equal(t, authorizationDetails, req.AuthorizationDetails) assert.Equal(t, params.ResponseType, req.ResponseType) assert.Equal(t, *params.IssuerState, req.OpState) assert.Equal(t, lo.ToPtr(scope), req.Scope) @@ -399,12 +522,35 @@ func TestController_OidcAuthorize(t *testing.T) { require.NotEmpty(t, rec.Header().Get("Location")) }, }, + { + name: "error no AuthorizationDetails supplied", + setup: func() { + state := "state" + + params = oidc4ci.OidcAuthorizeParams{ + ResponseType: "code", + State: &state, + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: nil, + } + + scope := []string{"openid", "profile"} + + mockOAuthProvider.EXPECT().NewAuthorizeRequest(gomock.Any(), gomock.Any()).Return(&fosite.AuthorizeRequest{ + Request: fosite.Request{RequestedScope: scope}, + }, nil) + }, + check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { + require.ErrorContains(t, err, "invalid-value[authorization_details]: not supplied") + }, + }, { name: "par error", setup: func() { params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -461,8 +607,9 @@ func TestController_OidcAuthorize(t *testing.T) { name: "invalid authorize request", setup: func() { params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } mockOAuthProvider.EXPECT().NewAuthorizeRequest(gomock.Any(), gomock.Any()).Return(nil, @@ -478,7 +625,7 @@ func TestController_OidcAuthorize(t *testing.T) { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", IssuerState: lo.ToPtr("opState"), - AuthorizationDetails: lo.ToPtr("invalid"), + AuthorizationDetails: lo.ToPtr(`{"key":"value"}`), } scope := []string{"openid", "profile"} @@ -499,7 +646,7 @@ func TestController_OidcAuthorize(t *testing.T) { params = oidc4ci.OidcAuthorizeParams{ ResponseType: "code", IssuerState: lo.ToPtr("opState"), - AuthorizationDetails: lo.ToPtr(`{"type":"openid_credential","credential_type":"UniversityDegreeCredential","format":"invalid"}`), + AuthorizationDetails: lo.ToPtr(`[]`), } scope := []string{"openid", "profile"} @@ -509,15 +656,16 @@ func TestController_OidcAuthorize(t *testing.T) { }, nil) }, check: func(t *testing.T, rec *httptest.ResponseRecorder, err error) { - require.ErrorContains(t, err, "authorization_details.format") + require.ErrorContains(t, err, "only single authorization_details supported") }, }, { name: "prepare claim data authorization", setup: func() { params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -547,8 +695,9 @@ func TestController_OidcAuthorize(t *testing.T) { name: "invalid status code for prepare claim data authorization", setup: func() { params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -583,9 +732,10 @@ func TestController_OidcAuthorize(t *testing.T) { state := "state" params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - State: &state, - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + State: &state, + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} @@ -636,9 +786,10 @@ func TestController_OidcAuthorize(t *testing.T) { state := "state" params = oidc4ci.OidcAuthorizeParams{ - ResponseType: "code", - State: &state, - IssuerState: lo.ToPtr("opState"), + ResponseType: "code", + State: &state, + IssuerState: lo.ToPtr("opState"), + AuthorizationDetails: lo.ToPtr(authorizationDetailsFormatBased), } scope := []string{"openid", "profile"} diff --git a/pkg/restapi/v1/oidc4ci/openapi.gen.go b/pkg/restapi/v1/oidc4ci/openapi.gen.go index c5741da34..791c91161 100644 --- a/pkg/restapi/v1/oidc4ci/openapi.gen.go +++ b/pkg/restapi/v1/oidc4ci/openapi.gen.go @@ -266,7 +266,7 @@ type OidcAuthorizeParams struct { // An opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery. State *string `form:"state,omitempty" json:"state,omitempty"` - // The authorization_details conveys the details about the credentials the wallet wants to obtain. Multiple authorization_details can be used with type openid_credential to request authorization in case of multiple credentials. + // Encoded array of the authorization_details conveys the details about the credentials the wallet wants to obtain. Multiple authorization_details can be used with type openid_credential to request authorization in case of multiple credentials. AuthorizationDetails *string `form:"authorization_details,omitempty" json:"authorization_details,omitempty"` // Wallet's OpenID Connect Issuer URL. The Issuer will use the discovery process to determine the wallet's capabilities and endpoints. RECOMMENDED in Dynamic Credential Request. diff --git a/pkg/restapi/v1/util/validate.go b/pkg/restapi/v1/util/validate.go index f59f172b4..4d3978adb 100644 --- a/pkg/restapi/v1/util/validate.go +++ b/pkg/restapi/v1/util/validate.go @@ -16,25 +16,56 @@ import ( "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) -func ValidateAuthorizationDetails(ad *common.AuthorizationDetails) (*oidc4ci.AuthorizationDetails, error) { +func ValidateAuthorizationDetails( + authorizationDetails []common.AuthorizationDetails) (*oidc4ci.AuthorizationDetails, error) { + if len(authorizationDetails) != 1 { + return nil, resterr.NewOIDCError("invalid_request", + errors.New("only single authorization_details supported")) + } + + ad := authorizationDetails[0] + if ad.Type != "openid_credential" { return nil, resterr.NewValidationError(resterr.InvalidValue, "authorization_details.type", errors.New("type should be 'openid_credential'")) } + oidcCredentialFormat := lo.FromPtr(ad.Format) + credentialConfigurationID := lo.FromPtr(ad.CredentialConfigurationId) + mapped := &oidc4ci.AuthorizationDetails{ - Type: ad.Type, - Types: ad.Types, - Locations: lo.FromPtr(ad.Locations), + Type: ad.Type, + Locations: lo.FromPtr(ad.Locations), + CredentialConfigurationID: "", + Format: "", + CredentialDefinition: nil, } - if ad.Format != nil { - vcFormat, err := common.ValidateVCFormat(common.VCFormat(*ad.Format)) + switch { + case credentialConfigurationID != "": // Priority 1. Based on credentialConfigurationID. + mapped.CredentialConfigurationID = credentialConfigurationID + case oidcCredentialFormat != "": // Priority 2. Based on credentialFormat. + vcsCredentialFormat, err := common.ValidateVCFormat(common.VCFormat(oidcCredentialFormat)) if err != nil { return nil, resterr.NewValidationError(resterr.InvalidValue, "authorization_details.format", err) } - mapped.Format = vcFormat + mapped.Format = vcsCredentialFormat + + if ad.CredentialDefinition == nil { + return nil, resterr.NewValidationError(resterr.InvalidValue, + "authorization_details.credential_definition", errors.New("not supplied")) + } + + mapped.CredentialDefinition = &oidc4ci.CredentialDefinition{ + Context: lo.FromPtr(ad.CredentialDefinition.Context), + CredentialSubject: lo.FromPtr(ad.CredentialDefinition.CredentialSubject), + Type: ad.CredentialDefinition.Type, + } + default: + return nil, resterr.NewValidationError(resterr.InvalidValue, + "authorization_details.credential_configuration_id", + errors.New("neither credentialFormat nor credentialConfigurationID supplied")) } return mapped, nil diff --git a/pkg/restapi/v1/util/validate_test.go b/pkg/restapi/v1/util/validate_test.go index 52497c134..72132e7b4 100644 --- a/pkg/restapi/v1/util/validate_test.go +++ b/pkg/restapi/v1/util/validate_test.go @@ -7,82 +7,196 @@ SPDX-License-Identifier: Apache-2.0 package util_test import ( + "reflect" + "strings" "testing" "github.com/samber/lo" - "github.com/stretchr/testify/require" - vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/pkg/restapi/v1/common" "github.com/trustbloc/vcs/pkg/restapi/v1/util" "github.com/trustbloc/vcs/pkg/service/oidc4ci" ) func TestValidateAuthorizationDetails(t *testing.T) { - t.Run("success", func(t *testing.T) { - tests := []struct { - name string - arg *string - want vcsverifiable.Format - }{ - { - name: "ldp_vc format", - arg: lo.ToPtr(string(common.LdpVc)), - want: vcsverifiable.Ldp, + type args struct { + ad []common.AuthorizationDetails + } + tests := []struct { + name string + args args + want *oidc4ci.AuthorizationDetails + wantErr bool + errorContains string + }{ + { + name: "Success Based on credentialConfigurationID", + args: args{ + ad: []common.AuthorizationDetails{ + { + CredentialConfigurationId: lo.ToPtr("UniversityDegreeCredential"), + Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), + Type: "openid_credential", + CredentialDefinition: nil, + Format: nil, + }, + }, }, - { - name: "jwt_vc format", - arg: lo.ToPtr(string(common.JwtVcJsonLd)), - want: vcsverifiable.Jwt, + want: &oidc4ci.AuthorizationDetails{ + Type: "openid_credential", + Locations: []string{"https://example.com/rs1", "https://example.com/rs2"}, + CredentialConfigurationID: "UniversityDegreeCredential", + Format: "", + CredentialDefinition: nil, }, - { - name: "no format", - arg: nil, - want: "", + wantErr: false, + errorContains: "", + }, + { + name: "Success Based on credentialFormat", + args: args{ + ad: []common.AuthorizationDetails{ + { + CredentialConfigurationId: nil, + Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), + Type: "openid_credential", + CredentialDefinition: &common.CredentialDefinition{ + Context: lo.ToPtr([]string{"https://example.com/context/1", "https://example.com/context/2"}), + CredentialSubject: lo.ToPtr(map[string]interface{}{ + "key": "value", + }), + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, + Format: lo.ToPtr("jwt_vc_json"), + }, + }, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ad := &common.AuthorizationDetails{ - Type: "openid_credential", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: tt.arg, - Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), - } - - got, err := util.ValidateAuthorizationDetails(ad) - require.NoError(t, err) - require.Equal(t, &oidc4ci.AuthorizationDetails{ - Type: "openid_credential", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: tt.want, - Locations: []string{"https://example.com/rs1", "https://example.com/rs2"}, - }, got) - }) - } - }) - - t.Run("invalid format", func(t *testing.T) { - ad := &common.AuthorizationDetails{ - Type: "openid_credential", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: lo.ToPtr("invalid"), - } - - got, err := util.ValidateAuthorizationDetails(ad) - require.ErrorContains(t, err, "unsupported vc format") - require.Nil(t, got) - }) - - t.Run("type should be 'openid_credential'", func(t *testing.T) { - ad := &common.AuthorizationDetails{ - Type: "invalid", - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, - Format: lo.ToPtr("ldp_vc"), - } + want: &oidc4ci.AuthorizationDetails{ + Type: "openid_credential", + Locations: []string{"https://example.com/rs1", "https://example.com/rs2"}, + CredentialConfigurationID: "", + Format: "jwt", + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Context: []string{"https://example.com/context/1", "https://example.com/context/2"}, + CredentialSubject: map[string]interface{}{ + "key": "value", + }, + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, + }, + wantErr: false, + errorContains: "", + }, + { + name: "Error multiple authorization details supplied", + args: args{ + ad: []common.AuthorizationDetails{ + { + Type: "unknown", + }, + { + Type: "unknown", + }, + }, + }, + want: nil, + wantErr: true, + errorContains: "oidc-error: only single authorization_details supported", + }, + { + name: "Error invalid type", + args: args{ + ad: []common.AuthorizationDetails{ + { + Type: "unknown", + }, + }, + }, + want: nil, + wantErr: true, + errorContains: "invalid-value[authorization_details.type]: type should be 'openid_credential'", + }, + { + name: "Error: credentialFormat: invalid format", + args: args{ + ad: []common.AuthorizationDetails{ + { + CredentialConfigurationId: nil, + Locations: nil, + Type: "openid_credential", + CredentialDefinition: &common.CredentialDefinition{ + Context: lo.ToPtr([]string{"https://example.com/context/1", "https://example.com/context/2"}), + CredentialSubject: lo.ToPtr(map[string]interface{}{ + "key": "value", + }), + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, + Format: lo.ToPtr("unknown"), + }, + }, + }, + want: nil, + wantErr: true, + errorContains: "invalid-value[authorization_details.format]: " + + "unsupported vc format unknown, use one of next [jwt_vc_json-ld, ldp_vc]", + }, + { + name: "Error: credentialFormat: empty CredentialDefinition", + args: args{ + ad: []common.AuthorizationDetails{ + { + CredentialConfigurationId: nil, + Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), + Type: "openid_credential", + CredentialDefinition: nil, + Format: lo.ToPtr("jwt_vc_json"), + }, + }, + }, + want: nil, + wantErr: true, + errorContains: "invalid-value[authorization_details.credential_definition]: not supplied", + }, + { + name: "Error: neither credentialFormat nor credentialConfigurationID supplied", + args: args{ + ad: []common.AuthorizationDetails{ + { + CredentialConfigurationId: nil, + Locations: lo.ToPtr([]string{"https://example.com/rs1", "https://example.com/rs2"}), + Type: "openid_credential", + CredentialDefinition: &common.CredentialDefinition{ + Context: lo.ToPtr([]string{"https://example.com/context/1", "https://example.com/context/2"}), + CredentialSubject: lo.ToPtr(map[string]interface{}{ + "key": "value", + }), + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, + Format: nil, + }, + }, + }, + want: nil, + wantErr: true, + errorContains: "invalid-value[authorization_details.credential_configuration_id]: " + + "neither credentialFormat nor credentialConfigurationID supplied", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := util.ValidateAuthorizationDetails(tt.args.ad) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateAuthorizationDetails() error = %v, wantErr %v", err, tt.wantErr) + return + } - got, err := util.ValidateAuthorizationDetails(ad) - require.ErrorContains(t, err, "type should be 'openid_credential'") - require.Nil(t, got) - }) + if tt.wantErr && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("ValidateAuthorizationDetails() error = %v, errorContains %v", err, tt.errorContains) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ValidateAuthorizationDetails() got = %v, want %v", got, tt.want) + } + }) + } } diff --git a/pkg/service/oidc4ci/api.go b/pkg/service/oidc4ci/api.go index e0a794248..388c514fa 100644 --- a/pkg/service/oidc4ci/api.go +++ b/pkg/service/oidc4ci/api.go @@ -66,7 +66,7 @@ type TransactionData struct { ProfileVersion profileapi.Version OrgID string CredentialTemplate *profileapi.CredentialTemplate - CredentialFormat vcsverifiable.Format + CredentialFormat vcsverifiable.Format // Format, that represents issued VC format (JWT, LDP). OIDCCredentialFormat vcsverifiable.OIDCFormat AuthorizationEndpoint string PushedAuthorizationRequestEndpoint string @@ -94,12 +94,23 @@ type TransactionData struct { WalletInitiatedIssuance bool } -// AuthorizationDetails are the VC-related details for VC issuance. +// AuthorizationDetails represents the domain model for Authorization Details request. +// +// Spec: https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-5.1.1 type AuthorizationDetails struct { - Type string - Types []string - Format vcsverifiable.Format - Locations []string + Type string + Format vcsverifiable.Format + Locations []string + CredentialConfigurationID string + CredentialDefinition *CredentialDefinition +} + +// CredentialDefinition contains the detailed description of the credential type. +type CredentialDefinition struct { + // For ldp_vc only. Array as defined in https://www.w3.org/TR/vc-data-model/#contexts. + Context []string + CredentialSubject map[string]interface{} + Type []string } // IssuerIDPOIDCConfiguration represents an Issuer's IDP OIDC configuration diff --git a/pkg/service/oidc4ci/oidc4ci_service.go b/pkg/service/oidc4ci/oidc4ci_service.go index 014db6a13..218d2ec52 100644 --- a/pkg/service/oidc4ci/oidc4ci_service.go +++ b/pkg/service/oidc4ci/oidc4ci_service.go @@ -214,7 +214,7 @@ func (s *Service) PushAuthorizationDetails( return fmt.Errorf("find tx by op state: %w", err) } - return s.updateAuthorizationDetails(ctx, ad, tx) + return s.updateTransactionAuthorizationDetails(ctx, ad, tx) } func (s *Service) checkScopes(reqScopes []string, txScopes []string) error { @@ -283,7 +283,7 @@ func (s *Service) PrepareClaimDataAuthorizationRequest( } if req.AuthorizationDetails != nil { - if err = s.updateAuthorizationDetails(ctx, req.AuthorizationDetails, tx); err != nil { + if err = s.updateTransactionAuthorizationDetails(ctx, req.AuthorizationDetails, tx); err != nil { s.sendFailedTransactionEvent(ctx, tx, err) return nil, err } @@ -376,24 +376,74 @@ func (s *Service) prepareClaimDataAuthorizationRequestWalletInitiated( }, nil } -func (s *Service) updateAuthorizationDetails(ctx context.Context, ad *AuthorizationDetails, tx *Transaction) error { - if tx.CredentialTemplate == nil { - return resterr.ErrCredentialTemplateNotConfigured +func (s *Service) updateTransactionAuthorizationDetails( + ctx context.Context, ad *AuthorizationDetails, tx *Transaction) error { + if err := s.checkTransactionAuthorizationDetails(ad, tx); err != nil { + return err } - targetType := ad.Types[len(ad.Types)-1] - if !strings.EqualFold(targetType, tx.CredentialTemplate.Type) { - return resterr.ErrCredentialTypeNotSupported + tx.AuthorizationDetails = ad + + if err := s.store.Update(ctx, tx); err != nil { + return resterr.NewSystemError(resterr.TransactionStoreComponent, "Update", err) } - if ad.Format != "" && ad.Format != tx.CredentialFormat { - return resterr.ErrCredentialFormatNotSupported + return nil +} + +//nolint:gocognit,nolintlint +func (s *Service) checkTransactionAuthorizationDetails(ad *AuthorizationDetails, tx *Transaction) error { + if tx.CredentialTemplate == nil { + return resterr.ErrCredentialTemplateNotConfigured } - tx.AuthorizationDetails = ad + switch { + case ad.CredentialConfigurationID != "": // AuthorizationDetails contains CredentialConfigurationID. + profile, err := s.profileService.GetProfile(tx.ProfileID, tx.ProfileVersion) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return resterr.NewCustomError(resterr.ProfileNotFound, + fmt.Errorf("update tx auth details: get profile: %w", err)) + } - if err := s.store.Update(ctx, tx); err != nil { - return resterr.NewSystemError(resterr.TransactionStoreComponent, "Update", err) + return resterr.NewSystemError(resterr.IssuerProfileSvcComponent, "GetProfile", + fmt.Errorf("update tx auth details: get profile: %w", err)) + } + + var credentialsConfigurationSupported *profileapi.CredentialsConfigurationSupported + if meta := profile.CredentialMetaData; meta != nil { + credentialsConfigurationSupported = meta.CredentialsConfigurationSupported[ad.CredentialConfigurationID] + } + + if credentialsConfigurationSupported == nil { + return resterr.ErrInvalidCredentialConfigurationID + } + + if credentialsConfigurationSupported.Format != tx.OIDCCredentialFormat { + return resterr.ErrCredentialFormatNotSupported + } + + var targetType string + if cd := credentialsConfigurationSupported.CredentialDefinition; cd != nil { + targetType = cd.Type[len(cd.Type)-1] + } + + if !strings.EqualFold(targetType, tx.CredentialTemplate.Type) { + return resterr.ErrCredentialTypeNotSupported + } + case ad.Format != "": // AuthorizationDetails contains Format. + // Compare AuthorizationDetails.Format with tx.CredentialFormat (Issuer's supported credential format) + if ad.Format != tx.CredentialFormat { + return resterr.ErrCredentialFormatNotSupported + } + + // Check credential type. + targetType := ad.CredentialDefinition.Type[len(ad.CredentialDefinition.Type)-1] + if !strings.EqualFold(targetType, tx.CredentialTemplate.Type) { + return resterr.ErrCredentialTypeNotSupported + } + default: + return errors.New("neither credentialFormat nor credentialConfigurationID supplied") } return nil diff --git a/pkg/service/oidc4ci/oidc4ci_service_test.go b/pkg/service/oidc4ci/oidc4ci_service_test.go index 0c6fa7da1..6d986f454 100644 --- a/pkg/service/oidc4ci/oidc4ci_service_test.go +++ b/pkg/service/oidc4ci/oidc4ci_service_test.go @@ -33,6 +33,7 @@ import ( func TestService_PushAuthorizationDetails(t *testing.T) { var ( mockTransactionStore = NewMockTransactionStore(gomock.NewController(t)) + profileSvc = NewMockProfileService(gomock.NewController(t)) ad *oidc4ci.AuthorizationDetails ) @@ -42,7 +43,7 @@ func TestService_PushAuthorizationDetails(t *testing.T) { check func(t *testing.T, err error) }{ { - name: "Success", + name: "Success AuthorizationDetails contains Format field", setup: func() { mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ ID: "txID", @@ -57,7 +58,9 @@ func TestService_PushAuthorizationDetails(t *testing.T) { mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) ad = &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "universitydegreecredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "universitydegreecredential"}, + }, Format: vcsverifiable.Ldp, } }, @@ -65,6 +68,48 @@ func TestService_PushAuthorizationDetails(t *testing.T) { require.NoError(t, err) }, }, + { + name: "Success AuthorizationDetails contains CredentialConfigurationID field", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + &profileapi.Issuer{ + CredentialMetaData: &profileapi.CredentialMetaData{ + CredentialsConfigurationSupported: map[string]*profileapi.CredentialsConfigurationSupported{ + "UniversityDegreeCredential": { + CredentialDefinition: &profileapi.CredentialDefinition{ + Type: []string{ + "VerifiableCredential", "UniversityDegreeCredential", + }, + }, + Format: vcsverifiable.JwtVCJsonLD, + }, + }, + }, + }, nil) + + mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()).Return(nil) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, { name: "Fail to find transaction by op state", setup: func() { @@ -72,7 +117,9 @@ func TestService_PushAuthorizationDetails(t *testing.T) { nil, errors.New("find tx error")) ad = &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "universitydegreecredential"}, + }, Format: vcsverifiable.Ldp, } }, @@ -91,7 +138,9 @@ func TestService_PushAuthorizationDetails(t *testing.T) { }, nil) ad = &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, } }, @@ -100,7 +149,251 @@ func TestService_PushAuthorizationDetails(t *testing.T) { }, }, { - name: "Credential type not supported", + name: "Error AuthorizationDetails contains CredentialConfigurationID field: get profile not found", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + nil, errors.New("not found")) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + var customErr *resterr.CustomError + is := errors.As(err, &customErr) + require.True(t, is) + + require.Equal(t, resterr.ProfileNotFound, customErr.Code) + require.Empty(t, customErr.FailedOperation) + require.Empty(t, customErr.Component) + require.ErrorContains(t, customErr.Err, "update tx auth details: get profile: not found") + }, + }, + { + name: "Error AuthorizationDetails contains CredentialConfigurationID field: get profile common error", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + nil, errors.New("some error")) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + var customErr *resterr.CustomError + is := errors.As(err, &customErr) + require.True(t, is) + + require.Equal(t, resterr.SystemError, customErr.Code) + require.Equal(t, "GetProfile", customErr.FailedOperation) + require.Equal(t, "issuer.profile-service", customErr.Component) + require.ErrorContains(t, customErr.Err, "update tx auth details: get profile: some error") + }, + }, + { + name: "Error AuthorizationDetails contains CredentialConfigurationID field: empty CredentialMetaData", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + &profileapi.Issuer{ + CredentialMetaData: nil, + }, nil) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + var customErr *resterr.CustomError + is := errors.As(err, &customErr) + require.True(t, is) + + require.Equal(t, resterr.InvalidCredentialConfigurationID, customErr.Code) + require.Empty(t, customErr.FailedOperation) + require.Empty(t, customErr.Component) + require.ErrorContains(t, customErr.Err, "invalid credential configuration ID") + }, + }, + { + name: "Error AuthorizationDetails contains CredentialConfigurationID field: " + + "CredentialMetaData for different VC type", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + &profileapi.Issuer{ + CredentialMetaData: &profileapi.CredentialMetaData{ + CredentialsConfigurationSupported: map[string]*profileapi.CredentialsConfigurationSupported{ + "PermanentResidentCard": { + CredentialDefinition: &profileapi.CredentialDefinition{ + Type: []string{ + "VerifiableCredential", "PermanentResidentCard", + }, + }, + Format: vcsverifiable.JwtVCJsonLD, + }, + }, + }, + }, nil) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + var customErr *resterr.CustomError + is := errors.As(err, &customErr) + require.True(t, is) + + require.Equal(t, resterr.InvalidCredentialConfigurationID, customErr.Code) + require.Empty(t, customErr.FailedOperation) + require.Empty(t, customErr.Component) + require.ErrorContains(t, customErr.Err, "invalid credential configuration ID") + }, + }, + { + name: "Error AuthorizationDetails contains CredentialConfigurationID field: invalid OIDC format", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + &profileapi.Issuer{ + CredentialMetaData: &profileapi.CredentialMetaData{ + CredentialsConfigurationSupported: map[string]*profileapi.CredentialsConfigurationSupported{ + "UniversityDegreeCredential": { + CredentialDefinition: &profileapi.CredentialDefinition{ + Type: []string{ + "VerifiableCredential", "UniversityDegreeCredential", + }, + }, + Format: vcsverifiable.JwtVCJson, // <- + }, + }, + }, + }, nil) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + var customErr *resterr.CustomError + is := errors.As(err, &customErr) + require.True(t, is) + + require.Equal(t, resterr.CredentialFormatNotSupported, customErr.Code) + require.Empty(t, customErr.FailedOperation) + require.Empty(t, customErr.Component) + require.ErrorContains(t, customErr.Err, "credential format not supported") + }, + }, + { + name: "Error AuthorizationDetails contains CredentialConfigurationID field: Credential type not supported", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "PermanentResidentCard", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + &profileapi.Issuer{ + CredentialMetaData: &profileapi.CredentialMetaData{ + CredentialsConfigurationSupported: map[string]*profileapi.CredentialsConfigurationSupported{ + "UniversityDegreeCredential": { + CredentialDefinition: &profileapi.CredentialDefinition{ + Type: []string{ + "VerifiableCredential", "UniversityDegreeCredential", + }, + }, + Format: vcsverifiable.JwtVCJsonLD, + }, + }, + }, + }, nil) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + } + }, + check: func(t *testing.T, err error) { + var customErr *resterr.CustomError + is := errors.As(err, &customErr) + require.True(t, is) + + require.Equal(t, resterr.CredentialTypeNotSupported, customErr.Code) + require.Empty(t, customErr.FailedOperation) + require.Empty(t, customErr.Component) + require.ErrorContains(t, customErr.Err, "credential type not supported") + }, + }, + { + name: "Error AuthorizationDetails contains Format field: Credential type not supported", setup: func() { mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ ID: "txID", @@ -113,7 +406,9 @@ func TestService_PushAuthorizationDetails(t *testing.T) { }, nil) ad = &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "NotSupportedCredentialType"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "NotSupportedCredentialType"}, + }, Format: vcsverifiable.Ldp, } }, @@ -122,7 +417,7 @@ func TestService_PushAuthorizationDetails(t *testing.T) { }, }, { - name: "Credential format not supported", + name: "Error AuthorizationDetails contains Format field: Credential format not supported", setup: func() { mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ ID: "txID", @@ -135,7 +430,9 @@ func TestService_PushAuthorizationDetails(t *testing.T) { }, nil) ad = &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Jwt, } }, @@ -159,7 +456,9 @@ func TestService_PushAuthorizationDetails(t *testing.T) { mockTransactionStore.EXPECT().Update(gomock.Any(), gomock.Any()).Return(errors.New("update error")) ad = &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, } }, @@ -167,12 +466,36 @@ func TestService_PushAuthorizationDetails(t *testing.T) { require.ErrorContains(t, err, "update error") }, }, + { + name: "Error neither credentialFormat nor credentialConfigurationID supplied", + setup: func() { + mockTransactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + }, + }, nil) + + ad = &oidc4ci.AuthorizationDetails{ + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, + } + }, + check: func(t *testing.T, err error) { + require.ErrorContains(t, err, "neither credentialFormat nor credentialConfigurationID supplied") + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() svc, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileSvc, TransactionStore: mockTransactionStore, }) require.NoError(t, err) @@ -184,7 +507,10 @@ func TestService_PushAuthorizationDetails(t *testing.T) { } func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { - var req *oidc4ci.PrepareClaimDataAuthorizationRequest + var ( + req *oidc4ci.PrepareClaimDataAuthorizationRequest + profileSvc = NewMockProfileService(gomock.NewController(t)) + ) tests := []struct { name string @@ -192,7 +518,7 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { check func(t *testing.T, resp *oidc4ci.PrepareClaimDataAuthorizationResponse, err error) }{ { - name: "Success", + name: "Success AuthorizationDetails contains Format field", setup: func(mocks *mocks) { mocks.transactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ ID: "txID", @@ -221,7 +547,9 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { ResponseType: "code", Scope: []string{"openid", "profile"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, }, } @@ -232,6 +560,65 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { require.Equal(t, []string{"openid", "profile", "address"}, resp.Scope) }, }, + { + name: "Success AuthorizationDetails contains CredentialConfigurationID field", + setup: func(mocks *mocks) { + mocks.transactionStore.EXPECT().FindByOpState(gomock.Any(), "opState").Return(&oidc4ci.Transaction{ + ID: "txID", + TransactionData: oidc4ci.TransactionData{ + CredentialTemplate: &profileapi.CredentialTemplate{ + Type: "UniversityDegreeCredential", + }, + CredentialFormat: vcsverifiable.Ldp, + OIDCCredentialFormat: vcsverifiable.JwtVCJsonLD, + ResponseType: "code", + Scope: []string{"openid", "profile", "address"}, + State: oidc4ci.TransactionStateIssuanceInitiated, + ProfileID: "bank_issuer1", + ProfileVersion: "v1.0", + }, + }, nil) + + profileSvc.EXPECT().GetProfile("bank_issuer1", "v1.0").Return( + &profileapi.Issuer{ + CredentialMetaData: &profileapi.CredentialMetaData{ + CredentialsConfigurationSupported: map[string]*profileapi.CredentialsConfigurationSupported{ + "UniversityDegreeCredential": { + CredentialDefinition: &profileapi.CredentialDefinition{ + Type: []string{ + "VerifiableCredential", "UniversityDegreeCredential", + }, + }, + Format: vcsverifiable.JwtVCJsonLD, + }, + }, + }, + }, nil) + + mocks.transactionStore.EXPECT().Update(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, tx *oidc4ci.Transaction) error { + assert.Equal(t, oidc4ci.TransactionStateAwaitingIssuerOIDCAuthorization, tx.State) + return nil + }).Times(2) + + mocks.eventService.EXPECT().Publish(gomock.Any(), spi.IssuerEventTopic, gomock.Any()). + DoAndReturn(expectedPublishEventFunc(t, spi.IssuerOIDCInteractionAuthorizationRequestPrepared)) + + req = &oidc4ci.PrepareClaimDataAuthorizationRequest{ + OpState: "opState", + ResponseType: "code", + Scope: []string{"openid", "profile"}, + AuthorizationDetails: &oidc4ci.AuthorizationDetails{ + CredentialConfigurationID: "UniversityDegreeCredential", + }, + } + }, + check: func(t *testing.T, resp *oidc4ci.PrepareClaimDataAuthorizationResponse, err error) { + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, []string{"openid", "profile", "address"}, resp.Scope) + }, + }, { name: "Failed sending event", setup: func(mocks *mocks) { @@ -267,7 +654,9 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { ResponseType: "code", Scope: []string{"openid", "profile"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, }, } @@ -373,7 +762,9 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { ResponseType: "code", Scope: []string{"openid"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, }, } @@ -412,7 +803,9 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { ResponseType: "code", Scope: []string{"openid"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, }, } @@ -460,7 +853,9 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { ResponseType: "code", Scope: []string{"openid", "profile"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Types: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"VerifiableCredential", "UniversityDegreeCredential"}, + }, Format: vcsverifiable.Ldp, }, } @@ -481,6 +876,7 @@ func TestService_PrepareClaimDataAuthorizationRequest(t *testing.T) { tt.setup(m) svc, err := oidc4ci.NewService(&oidc4ci.Config{ + ProfileService: profileSvc, TransactionStore: m.transactionStore, EventService: m.eventService, EventTopic: spi.IssuerEventTopic, diff --git a/pkg/service/wellknown/provider/wellknown_service.go b/pkg/service/wellknown/provider/wellknown_service.go index f9ff9e986..1987b690b 100644 --- a/pkg/service/wellknown/provider/wellknown_service.go +++ b/pkg/service/wellknown/provider/wellknown_service.go @@ -22,6 +22,7 @@ import ( "github.com/trustbloc/vcs/pkg/doc/vc" "github.com/trustbloc/vcs/pkg/kms" profileapi "github.com/trustbloc/vcs/pkg/profile" + "github.com/trustbloc/vcs/pkg/restapi/v1/common" "github.com/trustbloc/vcs/pkg/restapi/v1/issuer" ) @@ -243,7 +244,7 @@ func (s *Service) buildCredentialConfigurationsSupported( CryptographicSuitesSupported: lo.ToPtr(cryptographicSuitesSupported), Display: lo.ToPtr(display), Doctype: lo.ToPtr(credentialSupported.Doctype), - Format: credentialSupported.Format, + Format: string(credentialSupported.Format), Order: lo.ToPtr(credentialSupported.Order), ProofTypes: lo.ToPtr([]string{"jwt"}), Scope: lo.ToPtr(credentialSupported.Scope), @@ -255,15 +256,15 @@ func (s *Service) buildCredentialConfigurationsSupported( } func (s *Service) buildCredentialDefinition( - issuerCredentialDefinition *profileapi.CredentialConfigurationsSupportedDefinition, -) *issuer.CredentialConfigurationsSupportedDefinition { + issuerCredentialDefinition *profileapi.CredentialDefinition, +) *common.CredentialDefinition { credentialSubject := make(map[string]interface{}, len(issuerCredentialDefinition.CredentialSubject)) for k, v := range issuerCredentialDefinition.CredentialSubject { credentialSubject[k] = v } - return &issuer.CredentialConfigurationsSupportedDefinition{ + return &common.CredentialDefinition{ Context: lo.ToPtr(issuerCredentialDefinition.Context), CredentialSubject: lo.ToPtr(credentialSubject), Type: issuerCredentialDefinition.Type, diff --git a/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go b/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go index f3428f80d..1cb9863fc 100644 --- a/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go +++ b/pkg/storage/mongodb/oidc4cinoncestore/oidc4vc_store_test.go @@ -106,8 +106,11 @@ func TestStore(t *testing.T) { ResponseType: "123", Scope: []string{"213", "321"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Type: "321", - Types: []string{"fdsfsd"}, + Type: "321", + CredentialConfigurationID: "CredentialConfigurationID", + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"fdsfsd"}, + }, Format: "vxcxzcz", Locations: []string{"loc1", "loc2"}, }, diff --git a/pkg/storage/redis/oidc4cinoncestore/oidc4vc_store_test.go b/pkg/storage/redis/oidc4cinoncestore/oidc4vc_store_test.go index 800ac6c88..a863b728d 100644 --- a/pkg/storage/redis/oidc4cinoncestore/oidc4vc_store_test.go +++ b/pkg/storage/redis/oidc4cinoncestore/oidc4vc_store_test.go @@ -97,8 +97,11 @@ func TestStore(t *testing.T) { ResponseType: "123", Scope: []string{"213", "321"}, AuthorizationDetails: &oidc4ci.AuthorizationDetails{ - Type: "321", - Types: []string{"fdsfsd"}, + Type: "321", + CredentialConfigurationID: "CredentialConfigurationID", + CredentialDefinition: &oidc4ci.CredentialDefinition{ + Type: []string{"fdsfsd"}, + }, Format: "vxcxzcz", Locations: []string{"loc1", "loc2"}, }, diff --git a/test/bdd/pkg/v1/oidc4vc/oidc4vci.go b/test/bdd/pkg/v1/oidc4vc/oidc4vci.go index e82e2337e..adb387c48 100644 --- a/test/bdd/pkg/v1/oidc4vc/oidc4vci.go +++ b/test/bdd/pkg/v1/oidc4vc/oidc4vci.go @@ -137,7 +137,7 @@ func (s *Steps) runOIDC4VCIPreAuth(initiateOIDC4CIRequest initiateOIDC4VCIReques oidc4vci.WithFlowType(oidc4vci.FlowTypePreAuthorizedCode), oidc4vci.WithCredentialOffer(initiateOIDC4CIResponseData.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithPin(*initiateOIDC4CIResponseData.UserPin), ) if err != nil { @@ -270,7 +270,7 @@ func (s *Steps) runOIDC4CIPreAuthWithClientAttestation() error { oidc4vci.WithFlowType(oidc4vci.FlowTypePreAuthorizedCode), oidc4vci.WithCredentialOffer(initiateOIDC4CIResponseData.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithPin(*initiateOIDC4CIResponseData.UserPin), ) if err != nil { @@ -353,7 +353,7 @@ func (s *Steps) runOIDC4CIAuthWithErrorInvalidClient(updatedClientID, errorConta oidc4vci.WithFlowType(oidc4vci.FlowTypeAuthorizationCode), oidc4vci.WithCredentialOffer(resp.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithClientID(updatedClientID), oidc4vci.WithScopes([]string{"openid", "profile"}), oidc4vci.WithRedirectURI("http://127.0.0.1/callback"), @@ -478,7 +478,7 @@ func (s *Steps) runOIDC4VCIAuthWithError(errorContains string, overrideOpts ...o oidc4vci.WithFlowType(oidc4vci.FlowTypeAuthorizationCode), oidc4vci.WithCredentialOffer(resp.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithClientID("oidc4vc_client"), oidc4vci.WithScopes([]string{"openid", "profile"}), oidc4vci.WithRedirectURI("http://127.0.0.1/callback"), @@ -514,7 +514,7 @@ func (s *Steps) runOIDC4VCIAuth() error { oidc4vci.WithFlowType(oidc4vci.FlowTypeAuthorizationCode), oidc4vci.WithCredentialOffer(resp.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithClientID("oidc4vc_client"), oidc4vci.WithScopes([]string{"openid", "profile"}), oidc4vci.WithRedirectURI("http://127.0.0.1/callback"), @@ -537,7 +537,7 @@ func (s *Steps) runOIDC4VCIAuthWalletInitiatedFlow() error { oidc4vci.WithFlowType(oidc4vci.FlowTypeWalletInitiated), oidc4vci.WithIssuerState(fmt.Sprintf(vcsIssuerURL, s.issuerProfile.ID, s.issuerProfile.Version)), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithClientID("oidc4vc_client"), oidc4vci.WithScopes([]string{"openid", "profile"}), oidc4vci.WithRedirectURI("http://127.0.0.1/callback"), @@ -583,7 +583,7 @@ func (s *Steps) runOIDC4VCIAuthWithInvalidClaims() error { oidc4vci.WithFlowType(oidc4vci.FlowTypeAuthorizationCode), oidc4vci.WithCredentialOffer(resp.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithClientID("oidc4vc_client"), oidc4vci.WithScopes([]string{"openid", "profile"}), oidc4vci.WithRedirectURI("http://127.0.0.1/callback"), @@ -615,7 +615,7 @@ func (s *Steps) runOIDC4CIAuthWithClientRegistrationMethod(method string) error oidc4vci.WithFlowType(oidc4vci.FlowTypeAuthorizationCode), oidc4vci.WithCredentialOffer(resp.OfferCredentialURL), oidc4vci.WithCredentialType(s.issuedCredentialType), - oidc4vci.WithCredentialFormat(s.getIssuerCredentialFormat()), + oidc4vci.WithOIDCCredentialFormat(s.getIssuerOIDCCredentialFormat(s.issuedCredentialType)), oidc4vci.WithScopes([]string{"openid", "profile"}), oidc4vci.WithRedirectURI("http://127.0.0.1/callback"), oidc4vci.WithUserLogin("bdd-test"), @@ -925,8 +925,8 @@ func (s *Steps) initiateCredentialIssuanceWithError(errorContains string) error return nil } -func (s *Steps) getIssuerCredentialFormat() string { - for _, credentialConfigSupported := range s.issuerProfile.CredentialMetaData.CredentialsConfigurationSupported { +func (s *Steps) getIssuerOIDCCredentialFormat(credentialType string) vcsverifiable.OIDCFormat { + if credentialConfigSupported, ok := s.issuerProfile.CredentialMetaData.CredentialsConfigurationSupported[credentialType]; ok { return credentialConfigSupported.Format } diff --git a/test/stress/pkg/stress/stress_test_case.go b/test/stress/pkg/stress/stress_test_case.go index 514aef932..c5f9c2eb9 100644 --- a/test/stress/pkg/stress/stress_test_case.go +++ b/test/stress/pkg/stress/stress_test_case.go @@ -33,6 +33,7 @@ import ( "github.com/trustbloc/vcs/component/wallet-cli/pkg/oidc4vp" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wallet" "github.com/trustbloc/vcs/component/wallet-cli/pkg/wellknown" + vcsverifiable "github.com/trustbloc/vcs/pkg/doc/verifiable" "github.com/trustbloc/vcs/test/bdd/pkg/bddutil" "github.com/trustbloc/vcs/test/bdd/pkg/v1/model" ) @@ -49,7 +50,7 @@ type TestCase struct { verifierProfileVersion string credentialTemplateID string credentialType string - credentialFormat string + oidcCredentialFormat vcsverifiable.OIDCFormat token string claimData map[string]interface{} disableRevokeTestCase bool @@ -65,7 +66,7 @@ type TestCaseOptions struct { verifierProfileID string credentialTemplateID string credentialType string - credentialFormat string + oidcCredentialFormat vcsverifiable.OIDCFormat token string claimData map[string]interface{} disableRevokeTestCase bool @@ -83,7 +84,7 @@ func NewTestCase(options ...TestCaseOption) (*TestCase, error) { TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, }, - credentialFormat: "jwt_vc_json-ld", + oidcCredentialFormat: vcsverifiable.JwtVCJsonLD, } for _, opt := range options { @@ -191,7 +192,7 @@ func NewTestCase(options ...TestCaseOption) (*TestCase, error) { verifierProfileVersion: opts.verifierProfileVersion, credentialTemplateID: opts.credentialTemplateID, credentialType: opts.credentialType, - credentialFormat: opts.credentialFormat, + oidcCredentialFormat: opts.oidcCredentialFormat, token: opts.token, claimData: opts.claimData, disableRevokeTestCase: opts.disableRevokeTestCase, @@ -291,7 +292,7 @@ func (c *TestCase) Invoke() (string, interface{}, error) { oidc4vci.WithFlowType(oidc4vci.FlowTypePreAuthorizedCode), oidc4vci.WithCredentialOffer(credentialOfferURL), oidc4vci.WithCredentialType(c.credentialType), - oidc4vci.WithCredentialFormat(c.credentialFormat), + oidc4vci.WithOIDCCredentialFormat(c.oidcCredentialFormat), oidc4vci.WithPin(pin), ) if err != nil {