diff --git a/Makefile b/Makefile index dda609d5..a79af030 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ export TERM := xterm-256color ANDROID_EMULATOR_NAME ?= WalletSDKDeviceEmulator -VCS_COMMIT ?= 977063efde1950edc11e14bfe37f370f540c76a7 +VCS_COMMIT ?= 85f957d9f6554679b3fff86828d4e97ae556d05d .PHONY: all all: checks unit-test integration-test @@ -106,7 +106,7 @@ vc-rest-docker: --build-arg ALPINE_VER=$(ALPINE_VER) . .PHONY: integration-test -integration-test: mock-login-consent-docker mock-trust-registry-docker generate-test-keys +integration-test: mock-login-consent-docker mock-trust-registry-docker vc-rest-docker generate-test-keys @cd test/integration && go mod tidy && ENABLE_COMPOSITION=true go test -count=1 -v -cover . -p 1 -timeout=10m -race .PHONY: build-integration-cli diff --git a/pkg/credentialschema/models.go b/pkg/credentialschema/models.go index 87ee6263..5381f937 100644 --- a/pkg/credentialschema/models.go +++ b/pkg/credentialschema/models.go @@ -53,6 +53,6 @@ type ResolvedClaim struct { // Logo represents display information for a logo. type Logo struct { - URL string `json:"url,omitempty"` + URL string `json:"uri,omitempty"` AltText string `json:"alt_text,omitempty"` } diff --git a/pkg/models/issuer/metadata.go b/pkg/models/issuer/metadata.go index 1c1c77a5..938cf0ef 100644 --- a/pkg/models/issuer/metadata.go +++ b/pkg/models/issuer/metadata.go @@ -228,7 +228,7 @@ func (c *Claim) OrderAsInt() (int, error) { // Logo represents display information for a logo. type Logo struct { - URL string `json:"url,omitempty"` + URL string `json:"uri,omitempty"` AltText string `json:"alt_text,omitempty"` } diff --git a/pkg/openid4ci/errors.go b/pkg/openid4ci/errors.go index b185655c..b226a2d1 100644 --- a/pkg/openid4ci/errors.go +++ b/pkg/openid4ci/errors.go @@ -11,7 +11,8 @@ package openid4ci const ( ErrorModule = "OCI" InvalidIssuanceURIError = "INVALID_ISSUANCE_URI" - InvalidCredentialOfferError = "INVALID_CREDENTIAL_OFFER" //nolint:gosec //false positive + InvalidCredentialOfferError = "INVALID_CREDENTIAL_OFFER" //nolint:gosec //false positive + InvalidCredentialConfigurationIDError = "INVALID_CREDENTIAL_CONFIGURATION_ID" //nolint:gosec //false positive UnsupportedCredentialTypeInOfferError = "UNSUPPORTED_CREDENTIAL_TYPE_IN_OFFER" IssuerOpenIDConfigFetchFailedError = "ISSUER_OPENID_CONFIG_FETCH_FAILED" MetadataFetchFailedError = "METADATA_FETCH_FAILED" @@ -59,4 +60,5 @@ const ( UnsupportedIssuanceURISchemeCode = 19 NoTokenEndpointAvailableErrorCode = 20 AcknowledgmentExpiredErrorCode = 21 + InvalidCredentialConfigurationIDCode = 22 ) diff --git a/pkg/openid4ci/interaction.go b/pkg/openid4ci/interaction.go index f4d5d5d1..04859cb3 100644 --- a/pkg/openid4ci/interaction.go +++ b/pkg/openid4ci/interaction.go @@ -153,7 +153,7 @@ func (i *interaction) instantiateCodeVerifier() error { func (i *interaction) generateAuthorizationDetails(format string, types []string) ([]byte, error) { // TODO: Add support for requesting multiple credentials at once (by sending an array). // Currently we always use the first credential type specified in the offer. - authorizationDetailsDTO := &authorizationDetails{ + authorizationDetailsDTO := authorizationDetails{ CredentialConfigurationID: "", CredentialDefinition: &issuer.CredentialDefinition{ Context: nil, @@ -169,7 +169,7 @@ func (i *interaction) generateAuthorizationDetails(format string, types []string authorizationDetailsDTO.Locations = []string{i.issuerMetadata.CredentialIssuer} } - authorizationDetailsBytes, err := json.Marshal(authorizationDetailsDTO) + authorizationDetailsBytes, err := json.Marshal([]authorizationDetails{authorizationDetailsDTO}) if err != nil { return nil, err } diff --git a/pkg/openid4ci/issuerinitiatedinteraction.go b/pkg/openid4ci/issuerinitiatedinteraction.go index b3f8f656..161bb16b 100644 --- a/pkg/openid4ci/issuerinitiatedinteraction.go +++ b/pkg/openid4ci/issuerinitiatedinteraction.go @@ -70,7 +70,8 @@ type IssuerInitiatedInteraction struct { // NewIssuerInitiatedInteraction creates a new OpenID4CI IssuerInitiatedInteraction. // If no ActivityLogger is provided (via the ClientConfig object), then no activity logging will take place. -func NewIssuerInitiatedInteraction(initiateIssuanceURI string, +func NewIssuerInitiatedInteraction( + initiateIssuanceURI string, config *ClientConfig, ) (*IssuerInitiatedInteraction, error) { timeStartNewInteraction := time.Now() @@ -87,6 +88,21 @@ func NewIssuerInitiatedInteraction(initiateIssuanceURI string, return nil, err } + issuerInteraction := &interaction{ + issuerURI: credentialOffer.CredentialIssuer, + didResolver: config.DIDResolver, + activityLogger: config.ActivityLogger, + metricsLogger: config.MetricsLogger, + disableVCProofChecks: config.DisableVCProofChecks, + documentLoader: config.DocumentLoader, + httpClient: config.HTTPClient, + } + + err = issuerInteraction.populateIssuerMetadata(getIssuerMetadataEventText) + if err != nil { + return nil, err + } + // TODO https://github.com/trustbloc/wallet-sdk/issues/457 Add support for determining // grant types when no grants are specified. // See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html#section-4.1.1 for more info. @@ -95,29 +111,25 @@ func NewIssuerInitiatedInteraction(initiateIssuanceURI string, return nil, err } - credentialTypes, credentialFormats, err := determineCredentialTypesAndFormats(credentialOffer) + credentialTypes, credentialFormats, err := determineCredentialTypesAndFormats(credentialOffer, + issuerInteraction.issuerMetadata) if err != nil { return nil, err } return &IssuerInitiatedInteraction{ - interaction: &interaction{ - issuerURI: credentialOffer.CredentialIssuer, - didResolver: config.DIDResolver, - activityLogger: config.ActivityLogger, - metricsLogger: config.MetricsLogger, - disableVCProofChecks: config.DisableVCProofChecks, - documentLoader: config.DocumentLoader, - httpClient: config.HTTPClient, - }, + interaction: issuerInteraction, preAuthorizedCodeGrantParams: preAuthorizedCodeGrantParams, authorizationCodeGrantParams: authorizationCodeGrantParams, credentialTypes: credentialTypes, credentialFormats: credentialFormats, - }, config.MetricsLogger.Log(&api.MetricsEvent{ - Event: newInteractionEventText, - Duration: time.Since(timeStartNewInteraction), - }) + }, + config.MetricsLogger.Log( + &api.MetricsEvent{ + Event: newInteractionEventText, + Duration: time.Since(timeStartNewInteraction), + }, + ) } // CreateAuthorizationURL creates an authorization URL that can be opened in a browser to proceed to the login page. @@ -565,30 +577,45 @@ func getCredentialOfferJSONFromCredentialOfferURI(credentialOfferURI string, return responseBytes, nil } -func determineCredentialTypesAndFormats(credentialOffer *CredentialOffer) ([][]string, []string, error) { - // TODO Add support for credential offer objects that contain a credentials field with JSON strings instead. - // See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html#section-4.1.1 for more info. - credentialTypes := make([][]string, len(credentialOffer.Credentials)) - credentialFormats := make([]string, len(credentialOffer.Credentials)) +func determineCredentialTypesAndFormats( + credentialOffer *CredentialOffer, + issuerMetadata *issuer.Metadata, +) ([][]string, []string, error) { + types := make([][]string, len(credentialOffer.CredentialConfigurationIDs)) + formats := make([]string, len(credentialOffer.CredentialConfigurationIDs)) + + for i := 0; i < len(credentialOffer.CredentialConfigurationIDs); i++ { + id := credentialOffer.CredentialConfigurationIDs[i] + + configuration, ok := issuerMetadata.CredentialConfigurationsSupported[id] + if !ok { + return nil, nil, walleterror.NewValidationError( + ErrorModule, + InvalidCredentialConfigurationIDCode, + InvalidCredentialConfigurationIDError, + fmt.Errorf("invalid credential configuration ID (%s) in credential offer", id), + ) + } + + types[i] = configuration.CredentialDefinition.Type - for i := 0; i < len(credentialOffer.Credentials); i++ { - if credentialOffer.Credentials[i].Format != jwtVCJSONCredentialFormat && - credentialOffer.Credentials[i].Format != jwtVCJSONLDCredentialFormat && - credentialOffer.Credentials[i].Format != ldpVCCredentialFormat { + if configuration.Format != jwtVCJSONCredentialFormat && + configuration.Format != jwtVCJSONLDCredentialFormat && + configuration.Format != ldpVCCredentialFormat { return nil, nil, walleterror.NewValidationError( ErrorModule, UnsupportedCredentialTypeInOfferCode, UnsupportedCredentialTypeInOfferError, fmt.Errorf("unsupported credential type (%s) in credential offer at index %d of "+ - "credentials object (must be jwt_vc_json or jwt_vc_json-ld)", - credentialOffer.Credentials[i].Format, i)) + "credential_configurations_supported (must be jwt_vc_json or jwt_vc_json-ld)", + configuration.Format, i), + ) } - credentialTypes[i] = credentialOffer.Credentials[i].Types - credentialFormats[i] = credentialOffer.Credentials[i].Format + formats[i] = configuration.Format } - return credentialTypes, credentialFormats, nil + return types, formats, nil } func validateSignerKeyID(jwtSigner api.JWTSigner) error { @@ -620,8 +647,7 @@ func getSubjectIDs(vcs []*verifiable.Credential) []string { func signToken(claims interface{}, signer api.JWTSigner) (string, error) { headers := jose.Headers{} - // TODO: Send "typ" header. - // headers["typ"] = "openid4vci-proof+jwt" + headers["typ"] = "openid4vci-proof+jwt" token, err := jwt.NewSigned(claims, jwt.SignParameters{AdditionalHeaders: headers}, signer) if err != nil { diff --git a/pkg/openid4ci/issuerinitiatedinteraction_test.go b/pkg/openid4ci/issuerinitiatedinteraction_test.go index a9893d63..7fb5df25 100644 --- a/pkg/openid4ci/issuerinitiatedinteraction_test.go +++ b/pkg/openid4ci/issuerinitiatedinteraction_test.go @@ -7,10 +7,12 @@ SPDX-License-Identifier: Apache-2.0 package openid4ci_test import ( + "bytes" _ "embed" "encoding/json" "errors" "fmt" + "io" "net/http" "net/http/httptest" "net/url" @@ -51,6 +53,9 @@ var ( //go:embed testdata/sample_signed_issuer_metadata.jwt sampleSignedIssuerMetadata string + + //go:embed testdata/sample_issuer_metadata.json + sampleIssuerMetadata string ) type mockIssuerServerHandler struct { @@ -190,8 +195,6 @@ func TestNewIssuerInitiatedInteraction(t *testing.T) { t.Run("Credential format is jwt_vc_json-ld", func(t *testing.T) { credentialOffer := createSampleCredentialOffer(t, true, true) - credentialOffer.Credentials[0].Format = "jwt_vc_json-ld" - credentialOfferBytes, err := json.Marshal(credentialOffer) require.NoError(t, err) @@ -287,10 +290,10 @@ func TestNewIssuerInitiatedInteraction(t *testing.T) { require.EqualError(t, err, "no supported grant types found") require.Nil(t, interaction) }) - t.Run("Unsupported credential type", func(t *testing.T) { + t.Run("Invalid credential configuration id", func(t *testing.T) { credentialOffer := createSampleCredentialOffer(t, false, true) - credentialOffer.Credentials[0].Format = "UnsupportedType" + credentialOffer.CredentialConfigurationIDs = []string{"invalid_configuration_id"} credentialOfferBytes, err := json.Marshal(credentialOffer) require.NoError(t, err) @@ -300,9 +303,8 @@ func TestNewIssuerInitiatedInteraction(t *testing.T) { credentialOfferIssuanceURI := "openid-credential-offer://?credential_offer=" + credentialOfferEscaped interaction, err := openid4ci.NewIssuerInitiatedInteraction(credentialOfferIssuanceURI, getTestClientConfig(t)) - require.EqualError(t, err, "UNSUPPORTED_CREDENTIAL_TYPE_IN_OFFER(OCI0-0002):unsupported "+ - "credential type (UnsupportedType) in credential offer at index 0 of credentials object "+ - "(must be jwt_vc_json or jwt_vc_json-ld)") + require.EqualError(t, err, "INVALID_CREDENTIAL_CONFIGURATION_ID(OCI0-0022):invalid credential configuration "+ + "ID (invalid_configuration_id) in credential offer") require.Nil(t, interaction) }) t.Run("Fail to log retrieving credential offer via HTTP GET metrics event", func(t *testing.T) { @@ -1964,9 +1966,27 @@ func getTestClientConfig(t *testing.T) *openid4ci.ClientConfig { DIDResolver: didResolver, DisableVCProofChecks: true, NetworkDocumentLoaderHTTPTimeout: &networkDocumentLoaderHTTPTimeout, + HTTPClient: &http.Client{ + Transport: &mockTransport{ + roundTripFunc: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString(sampleIssuerMetadata)), + }, nil + }, + }, + }, } } +type mockTransport struct { + roundTripFunc func(req *http.Request) (*http.Response, error) +} + +func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return m.roundTripFunc(req) +} + // makeMockDoc creates a key in the given KMS and returns a mock DID Doc with a verification method. func makeMockDoc(keyWriter api.KeyWriter) (*did.Doc, error) { _, pkJWK, err := keyWriter.Create(arieskms.ED25519Type) diff --git a/pkg/openid4ci/models.go b/pkg/openid4ci/models.go index 69a376f5..dd1098a0 100644 --- a/pkg/openid4ci/models.go +++ b/pkg/openid4ci/models.go @@ -14,17 +14,11 @@ import ( ) // CredentialOffer represents the Credential Offer object as defined in -// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-11.html#section-4.1.1. +// https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#section-4.1.1. type CredentialOffer struct { - CredentialIssuer string `json:"credential_issuer,omitempty"` - Credentials []Credentials `json:"credentials,omitempty"` - Grants map[string]map[string]interface{} `json:"grants,omitempty"` -} - -// Credentials represents the credential format and types in a Credential Offer. -type Credentials struct { - Format string `json:"format,omitempty"` - Types []string `json:"types,omitempty"` + CredentialIssuer string `json:"credential_issuer,omitempty"` + CredentialConfigurationIDs []string `json:"credential_configuration_ids"` + Grants map[string]map[string]interface{} `json:"grants,omitempty"` } // AuthorizeResult is the object returned from the Client.Authorize method. diff --git a/pkg/openid4ci/testdata/sample_credential_offer.json b/pkg/openid4ci/testdata/sample_credential_offer.json index 81c99b50..01afca45 100644 --- a/pkg/openid4ci/testdata/sample_credential_offer.json +++ b/pkg/openid4ci/testdata/sample_credential_offer.json @@ -1,13 +1,7 @@ { "credential_issuer":"example.com", - "credentials":[ - { - "format":"jwt_vc_json", - "types":[ - "VerifiableCredential", - "VerifiedEmployee" - ] - } + "credential_configuration_ids": [ + "credential_configuration_id_1" ], "grants":{ "urn:ietf:params:oauth:grant-type:pre-authorized_code":{ diff --git a/pkg/openid4ci/testdata/sample_issuer_metadata.json b/pkg/openid4ci/testdata/sample_issuer_metadata.json new file mode 100644 index 00000000..c47ee3d6 --- /dev/null +++ b/pkg/openid4ci/testdata/sample_issuer_metadata.json @@ -0,0 +1,187 @@ +{ + "authorization_endpoint": "http://localhost:8075/oidc/authorize", + "credential_ack_endpoint": "http://localhost:8075/oidc/acknowledgement", + "credential_configurations_supported": { + "credential_configuration_id_1": { + "claims": null, + "credential_definition": { + "@context": null, + "credentialSubject": { + "displayName": { + "display": [ + { + "locale": "en-US", + "name": "Employee" + } + ], + "mandatory": false, + "mask": "", + "order": "0", + "pattern": "", + "value_type": "string" + }, + "givenName": { + "display": [ + { + "locale": "en-US", + "name": "Given Name" + } + ], + "mandatory": false, + "mask": "", + "order": "1", + "pattern": "", + "value_type": "string" + }, + "jobTitle": { + "display": [ + { + "locale": "en-US", + "name": "Job Title" + } + ], + "mandatory": false, + "mask": "", + "order": "2", + "pattern": "", + "value_type": "string" + }, + "mail": { + "display": [ + { + "locale": "en-US", + "name": "Mail" + } + ], + "mandatory": false, + "mask": "", + "order": "0", + "pattern": "", + "value_type": "string" + }, + "photo": { + "display": [ + { + "locale": "", + "name": "Photo" + } + ], + "mandatory": false, + "mask": "", + "order": "0", + "pattern": "", + "value_type": "image" + }, + "preferredLanguage": { + "display": [ + { + "locale": "en-US", + "name": "Preferred Language" + } + ], + "mandatory": false, + "mask": "", + "order": "0", + "pattern": "", + "value_type": "string" + }, + "reallySensitiveID": { + "display": [ + { + "locale": "en-US", + "name": "Really Sensitive ID" + } + ], + "mandatory": false, + "mask": "regex((.*))", + "order": "0", + "pattern": "", + "value_type": "string" + }, + "sensitiveID": { + "display": [ + { + "locale": "en-US", + "name": "Sensitive ID" + } + ], + "mandatory": false, + "mask": "regex(^(.*).{4}$)", + "order": "0", + "pattern": "", + "value_type": "string" + }, + "surname": { + "display": [ + { + "locale": "en-US", + "name": "Surname" + } + ], + "mandatory": false, + "mask": "", + "order": "0", + "pattern": "", + "value_type": "string" + } + }, + "type": [ + "VerifiableCredential", + "VerifiedEmployee" + ] + }, + "cryptographic_binding_methods_supported": [ + "ion" + ], + "cryptographic_suites_supported": [ + "ED25519" + ], + "display": [ + { + "background_color": "#12107c", + "locale": "en-US", + "logo": { + "alt_text": "a square logo of an employee verification", + "uri": "https://example.com/public/logo.png" + }, + "name": "Verified Employee", + "text_color": "#FFFFFF", + "url": "" + } + ], + "doctype": "", + "format": "jwt_vc_json", + "order": null, + "proof_types": [ + "jwt" + ], + "scope": "", + "vct": "" + } + }, + "credential_endpoint": "http://localhost:8075/oidc/credential", + "credential_issuer": "http://localhost:8075/issuer/bank_issuer/v1.0", + "display": [ + { + "locale": "en-US", + "name": "Bank Issuer", + "url": "http://vc-rest-echo.trustbloc.local:8075" + } + ], + "grant_types_supported": [ + "authorization_code" + ], + "pre-authorized_grant_anonymous_access_supported": true, + "registration_endpoint": "http://localhost:8075/oidc/bank_issuer/v1.0/register", + "response_types_supported": [ + "code" + ], + "scopes_supported": [ + "openid", + "profile" + ], + "token_endpoint": "http://localhost:8075/oidc/token", + "token_endpoint_auth_methods_supported": [ + "none" + ] +} \ No newline at end of file diff --git a/test/integration/expecteddisplaydata/bank_issuer.json b/test/integration/expecteddisplaydata/bank_issuer.json index 85715eca..08eeeed2 100644 --- a/test/integration/expecteddisplaydata/bank_issuer.json +++ b/test/integration/expecteddisplaydata/bank_issuer.json @@ -9,7 +9,7 @@ "name":"Verified Employee", "locale":"en-US", "logo":{ - "url":"https://example.com/public/logo.png", + "uri":"https://example.com/public/logo.png", "alt_text":"a square logo of an employee verification" }, "background_color":"#12107c", diff --git a/test/integration/expecteddisplaydata/did_ion_issuer.json b/test/integration/expecteddisplaydata/did_ion_issuer.json index 1e513cae..c8b4a209 100644 --- a/test/integration/expecteddisplaydata/did_ion_issuer.json +++ b/test/integration/expecteddisplaydata/did_ion_issuer.json @@ -9,7 +9,7 @@ "name":"Verified Employee", "locale":"en-US", "logo":{ - "url":"https://example.com/public/logo.png", + "uri":"https://example.com/public/logo.png", "alt_text":"a square logo of an employee verification" }, "background_color":"#12107c", diff --git a/test/integration/expecteddisplaydata/drivers_license_issuer.json b/test/integration/expecteddisplaydata/drivers_license_issuer.json index ecb11fd0..447a205a 100644 --- a/test/integration/expecteddisplaydata/drivers_license_issuer.json +++ b/test/integration/expecteddisplaydata/drivers_license_issuer.json @@ -9,7 +9,7 @@ "name":"Driver's License", "locale":"en-US", "logo":{ - "url":"https://example.com/public/logo.png", + "uri":"https://example.com/public/logo.png", "alt_text": "a square logo of a driver's license verification" }, "background_color":"#12107c", diff --git a/test/integration/expecteddisplaydata/university_degree_issuer.json b/test/integration/expecteddisplaydata/university_degree_issuer.json index c8eba53c..ec0c5348 100644 --- a/test/integration/expecteddisplaydata/university_degree_issuer.json +++ b/test/integration/expecteddisplaydata/university_degree_issuer.json @@ -9,7 +9,7 @@ "name": "University Degree Credential", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a degree verification" }, "background_color": "#12107c", diff --git a/test/integration/expecteddisplaydata/university_degree_sd.json b/test/integration/expecteddisplaydata/university_degree_sd.json index 13a04b7b..f12cdd29 100644 --- a/test/integration/expecteddisplaydata/university_degree_sd.json +++ b/test/integration/expecteddisplaydata/university_degree_sd.json @@ -9,7 +9,7 @@ "name": "University Degree Credential", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a degree verification" }, "background_color": "#12107c", diff --git a/test/integration/fixtures/profile/profiles.json b/test/integration/fixtures/profile/profiles.json index 85926a35..6129f22e 100644 --- a/test/integration/fixtures/profile/profiles.json +++ b/test/integration/fixtures/profile/profiles.json @@ -161,7 +161,7 @@ "locale": "en-US", "logo": { "alt_text": "a square logo of an employee verification", - "url": "https://example.com/public/logo.png" + "uri": "https://example.com/public/logo.png" }, "name": "Verified Employee", "text_color": "#FFFFFF" @@ -361,7 +361,7 @@ "name": "Driver's License", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a driver's license verification" }, "background_color": "#12107c", @@ -534,7 +534,7 @@ "name": "Verified Employee", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of an employee verification" }, "background_color": "#12107c", @@ -667,7 +667,7 @@ "name": "University Degree Credential", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a degree verification" }, "background_color": "#12107c", @@ -799,7 +799,7 @@ "name": "University Degree Credential", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of a degree verification" }, "background_color": "#12107c", @@ -945,7 +945,7 @@ "name": "Verified Employee", "locale": "en-US", "logo": { - "url": "https://example.com/public/logo.png", + "uri": "https://example.com/public/logo.png", "alt_text": "a square logo of an employee verification" }, "background_color": "#12107c",