Issuing a Known Customer Credential
Known Customer Credentials (KCCs) are Verifiable Credentials designed to streamline the Know Your Customer (KYC) process for tbDEX protocol users. KCCs help in gaining access to PFIs providing regulated financial services.
For a comprehensive exploration of the practical applications and compliance considerations of KCCs, refer to our KCC Compliance Guide which provides detailed examples and insights into how KCCs align with certain regulatory requirements, offering valuable context for those overseeing the implementation and management of KCCs.
This guide is intended for educational purposes only and does not constitute legal advice. Compliance programs may have varying requirements. Consult your legal and/or compliance advisors to ensure that the KCC is consistent with your legal and compliance obligations.
In this guide, we'll cover:
- Performing Identity Verification (IDV) as a necessary step before issuing a Known Customer Credential (KCC), by implementing an HTTP API conformant with Self Issued OpenID Provider V2 (SIOPv2) and OpenID for Verifiable Presentation's (OID4VP).
- Designing and detailing the required properties of a KCC.
- Issuing a KCC conformant with OpenID for Verifiable Credential Issuance (OID4VCI) following successful IDV.
For a detailed guide on managing Known Customer Credentials as a Wallet application, please refer to the Known Customer Credential Guide for Wallet Apps
Environment Setupβ
If you haven't already, please follow the Credential Issuance Server Setup Guide for detailed instructions on the dependencies and packages needed to set up your server.
IDV Processβ
IDV is a critical component of typical KYC requirements, where certain types of Personally Identifying Information (PII) are collected and verified from an individual. IDV is a crucial step before a KCC can be issued.
The IDV flow begins when the customer's agent (e.g. Wallet application) sends an HTTP request to the Issuer's IDV service endpoint specified in the Issuer's Decentralized Identifier (DID) Document.
1. Handling Incoming Requestsβ
Implement an endpoint to handle incoming GET requests from the Wallet. When a request is received, you'll need to construct the SIOPv2 Authorization Request.
Approach 1: Requesting only id_token
β
When a Verifiable Presentation is not an accepted form of IDV, you can respond to the customer's Wallet application by requesting only an id_token
from them:
id_token
is a Self Issued Identity Token, that authenticates the customer to the Issuer by sharing the customer's Decentralized Identifier. This guide will cover an example id_token
to learn more refer to OpenID's specs.
Approach 2: Requesting both id_token
and vp_token
β
When a Verifiable Presentation is an accepted form of IDV, you can respond to the customer's Wallet application by requesting an id_token
and a vp_token
from them:
The SOIPv2 Authorization request above that you as the issuer send to your customer contains the following key information about you as well as a Presentation Definition.
client_id
: Issuer's DID, establishing the issuer/requestor's identity.response_type
: Lets the Wallet know the desired format, withid_token
being required identity token andvp_token
for optional verifiable presentation token. The inclusion ofvp_token
means a presentation definition is required within the request.response_uri
: Indicates the callback url for the Wallet, ensuring direct communication between the Wallet and the Issuer.nonce
: A unique identifier tying together the request and its response.
vp_token
is a Verifiable Presentation that contains certified and authenticated claims about the customer. In this guide, we'll cover an example vp_token
. To learn more refer to OpenID's specs.
2. Encode the SIOPv2 Authorization Request and respond:β
3. Wallet's SIOPv2 Authorization Responseβ
The Wallet will then respond with an HTTP POST SIOPv2 Authorization Response. The HTTP request body must include an id_token
and optionally a vp_token
, both of which must be JSON Web Tokens (JWTs). Here is an example HTTP POST request body (which includes the optional vp_token
):
// Wallet.js example HTTP POST request body
{
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6ZGh0OmN1c3RvbWVyRGlkIiwic3ViIjoiZGlkOmRodDpjdXN0b21lckRpZCIsImF1ZCI6Imlzc3VlckRpZC51cmkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsImV4cCI6MTYxODg4NDQ3MywiaWF0IjoxNjE4ODgwODczfQGUuY29tL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiSURDYXJkQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJnaXZlbl9uYW1lIjoiRnJlZHJpayIsImZhbWlseV9uYW1.Imlzc3VlckRpZC51cmkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsImV4cCI6MTYxODg4NDQ3MywiaWF0IjoxNjE4ODgwODcz",
"vp_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwiaWQiOiJodHRwczovL2V4YW1wbGUuY29tL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiSURDYXJkQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJnaXZlbl9uYW1lIjoiRnJlZHJpayIsImZhbWlseV9uYW1lIjoiU3RyJm9tYmVyZyIs.ImJpcnRoZGF0ZSI6IjE5NDktMDEtMjIifX1dLCJob2xkZXIiOiJkaWQ6ZXhhbXBsZTpob2xkZXIiLCJwcm9vZiI6eyJ0eXBlIjoiRWQyNTUxOVNpZ25hdHVyZTIwMTgiLCJjcmVhdGVkIjoiMjAyMS0wMy0xOVQxNTozMDoxNVoiLCJjaGFsbGVuZ2UiOiJuLTBTNl9XekEyTWoiLCJkb21haW4iOiJodHRwczovL2NsaWVudC5leGFtcGxlLm9yZy9jYiIsImp3cyI6ImV5SmhiR2NpT2lKSVV6STFOaUo5LkhvbGRlcl9GT1JfU0lHTkFUVVJFIiwicHJvb2ZQdXJwb3NlIjoiYXV0aGVudGljYXRpb24iLCJ2ZXJpZmljYXRpb25NZXRob2QiOiJkaWQ6ZXhhbXBsZTpob2xkZXIja2V5LTEifX0"
}
The decoded JWT Claims for the id_token
and vp_token
are as follows:
// Wallet.js id_token decoded JWT claims
{
"iss": "did:dht:customerDid",
"sub": "did:dht:customerDid", // Customer's DID string
"aud": "did:dht:issuerDid", // Issuer's DID string
"nonce": "n-0S6_WzA2Mj", // Unique string from the Issuer's Auth request
"exp": 1618884473, // Expiration time
"iat": 1618880873 // issued at time
}
// Wallet.js vp_token decoded JWT claims
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://identity.foundation/presentation-exchange/submission/v1"
],
"type": ["VerifiablePresentation", "PresentationSubmission"],
"presentation_submission": {
"id": "epzZXstAcVNt5MRrcyG91",
"definition_id": "IDCardCredentials",
"descriptor_map": [
{
"id": "IDCardCredential",
"format": "jwt_vc",
"path": "$.vp.verifiableCredential[0]"
},
{
"id": "nationalIdentifierVerification",
"format": "jwt_vc",
"path": "$.vp.verifiableCredential[1]"
}
]
},
"verifiableCredential": [
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyI3o2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFbXBsb3ltZW50Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjcyNDhiOTkyLTkwOTYtNDk2NS1hMGVjLTc3ZDhhODNhMWRmYiIsImlzc3VlciI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDoxMToyNVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlEMTR4UmY0cTJNWlh1ZWY2X2ZXYnBGbVlTUG94dGFxTkp1SmdEMG96Wl84UTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWVubGFNbVYzTlhKeVVXdFVjbUV3WlZsVk16WlBTblJzTURCbFJWZHhhalZhV0dkNmNEZFpSVTVKUVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpQZDJZMFQyMUViamxKWm5SNFdYWnBkRTFHWm1jMVVXeDVMVVV6VWs1b1dsUkdPVlpFTWtnNVQzVjNJaXdpZVNJNkltUnZjVmxtV2s1c1NtRlRNVll4U201bU9HdEZObEF6VkRsd2QzaDNla3hFVTJWc1ZqTlRUa2s1U2xFaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xEWm05bVFUQkpVbU5uY2tWdVVHZHdRbU5RV1ZsV2VFWlliR0pTYjJRd2RVNWZRVkJwTkVrNUxVRmZRU0o5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFFtd3pWWG80VldGT2REZGxlREJKYjJJMFJFNXNhbFJGVmpaelQwTmtjbFJ3TWxvNE5FTkJPVFJPUWtFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWk9WRldZbWRKYkUxemRraEZYMVJtTld4a1MxQjBkR3d3WVV4blNrdHNSbmt6Vms0d2QzQTJhVFpSSW4xOSIsImVtcGxveW1lbnRTdGF0dXMiOiJlbXBsb3llZCJ9fX0.Sazc8Ndhs-NKjxvtVMKeC9dxjEkI26fVsp2kFNWM-SYLtxMzKvl5ffeWd81ysHgPmBBSk2ar4dMqGgUsyM4gAQ",
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0I3o2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJOYW1lQW5kRG9iQ3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjliZjM2YzY5LTI0ODAtNDllZC1iMTYyLTRlZDEwOWE3MTc3NyIsImlzc3VlciI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0IiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDowNjowMVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlDS2o2M0FyZlBGcEpsb2lTd3gxQUhxVWtpWlNoSDZGdnZoSzRaTl9fZDFtQTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWNscFdXbTVJVkVrNWFEWkJUVmxVV0dwT01HcFhTVkYwTTI5ak4xTnJTeTF4Y2kxcVVuSTBUalEzUlNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpaVDFwRE5WSmlUMHQ1T0dadVVUWTJVWEZPUkc5aldFMXZPVXhUZEdNNVYyOWthMHd0ZFZCZlExQnZJaXdpZVNJNklsWnZZM0UxVERodFozQlhXVTFrYjFwS1JrWlJUa1ZDT0hsR0xXTndkRWQzZFdkcFRWVm5hR2t6Y21jaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xCTXpSMlMzb3llVmswZVV4dGRDMUdabkJuYWpWbGFFRm1ZWFI1YzFOa2MwNVNWbVpMYkhwUWRqTjVkeUo5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFF6ZGZjMXBzTW1wMVVXNUdhRVJIV1RSb2NFVTRiMlF4YVU5MWRuZG1PVFJ5TVVkbk9HMWFWbVJCVmxFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVKdU5sTnJiSEpWYzNKdVFuaFJPVXBqVXkxTlNVaGtWelYwTXpRM1MxWjNaMXBwVEZwMFQwcDRRVkYzSW4xOSIsIm5hbWUiOiJhbGljZSBib2IiLCJkYXRlT2ZCaXJ0aCI6IjEwLTAxLTE5OTAifX19.mNCDv_JntH-wZpYONKNL58UbOWaYXCYJO_HPI_WVlSgwzo6dhYmV_9qtpFKd_exFb-aaEYPeSE43twWlrJeSBg"
]
}
4. Handle SIOPv2 Responseβ
Implement a /siopv2/response
endpoint to process and verify the Wallet's response. The HTTP response body must always include a credential_offer
. If the Wallet's response does not include a vp_token
, as a valid form of IDV, then the HTTP response body must also include a URL directing the Wallet to an Identity Verification form:
The IDV Request Form points to a webpage for the customer to input their Personal Identifiable Information (PII) (e.g.; name, identification number, date of birth, etc).
Credential Offer Details:β
credential_issuer
: The base URL address of the Issuer's service. It is used by the Wallet to retrieve the Issuer's metadata and the Issuer's Authorization Server Metadata.credential_configuration_ids
: Unique strings that identify the credential you are offering. The Wallet can use these to request metadata.grants
: Contains grant types you will accept for this credential offer.urn:ietf:params:oauth:grant-type:pre-authorized_code
: Grant type required for the OID4VCI Pre-Authorization flow.pre-authorized_code
: The code representing the Credential Issuer's authorization for the Wallet to obtain an access token.
Dynamic Metadata Retrieval by Wallets
Wallets use the credential_issuer
from the Credential Offer to dynamically construct URLs for fetching Issuer and Authorization Server Metadata. These URLs, formed by appending /.well-known/openid-credential-issuer
and /.well-known/oauth-authorization-server
to the credential_issuer
, allow wallets to access up-to-date information on issuer capabilities and OAuth 2.0 endpoints. The next steps will cover how to properly configure these endpoints.
5. Create Issuer's Metadata Endpointβ
Configure the /.well-known/openid-credential-issuer
endpoint that will return your Issuer Metadata:
credential_issuer
: The base URL address of the Issuer's service. It is used by the Wallet to retrieve the Issuer's metadata and the Issuer's Authorization Server Metadata.credential_endpoint
: The endpoint the Wallet will use to submit requests to issue credentials. After obtaining an access token, the Wallet sends a credential request to this endpoint.credential_configurations_supported
: Defines the formatjwt_vc_json
, and the credential, including informative display markup.
6. Create Issuer's Authorization Server Metadata Endpointβ
Configure the /.well-known/oauth-authorization-server
endpoint that will return your Issuer Authorization Server Metadata:
issuer
: The base URL of the Issuer's service.token_endpoint
: The endpoint where the Wallet can exchange thepre_authorization_code
receieved in thecredential_offer
for an access token.
7. Create Access Token Endpointβ
Set up the /token
endpoint to process incoming requests from the Wallet containing the pre_authorization_code
. This code, originally provided in the credential_offer
object, is used to validate the Wallet's request:
authorization_pending
error response in the case IDV is incomplete
The Issuer must check if the IDV Process is completed prior to issuing an access token, and in the event IDV is incomplete must return an authorization_pending
error response to the Wallet whereafter the Wallet will repeat the call to /token
until IDV is completed.
Design Known Customer Credentialβ
When creating a Verifiable Credential, you can design a model class to represent the specific type of credential you'd like to issue.
Define a KccCredential
class with required and optional fields for a Known Customer Credential:
-
country_of_residence
: Is aISO-3166
2-letter country code representing the customerβs verified country of residence (ex: US) -
tier
: The tier your customer falls into, assuming you perform tier-based KYC. -
credentialSchema
: Schema used to define the shape of the credential including its required fields. -
evidence
: Is an Array of objects, where each object containskind
(String) andchecks
(array of Strings).kind
states the type of evidence andchecks
details the specific proofs or checks used during IDV.
- You'll need to assign
issuer
,subject
,type
, andexpirationDate
properties during the creation process.issuanceDate
is managed and generated by theWeb5
SDK. You'll see how this works in the upcoming section.
Issue Known Customer Credentialβ
Issuing a Known Customer Credential to a customer's Wallet confirms the successful completion of the Identity Verification Process. To create and securely sign this credential, we will utilize the web5/credentials package.
- Once the customer accepts your Credential Offer, the Wallet will then send a request for credentials to the endpoint specified in the
credential_endpoint
field of the Issuer's metadata. Here is an example of the Wallet's request:
POST /credentials HTTP/1.1
Host: issuer.example.com
Authorization: Bearer eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJblIwY0VRNWF6SnNSSGQxV2tobmQyVllVbDluVUd4U2RHWk9hMEZUZVhZek1HVlhSSGx5VUU5Uk1VVWlmUSMwIn0.eyJleHAiOjE3MTA5NDkzODAsImlhdCI6MTcxMDg2Mjk1NywiaXNzIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpZUNJNkluUjBjRVE1YXpKc1JIZDFXa2huZDJWWVVsOW5VR3hTZEdaT2EwRlRlWFl6TUdWWFJIbHlVRTlSTVVVaWZRIiwic3ViIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpZUNJNklqWnRMVTU0T0dad1IxbFRPRmhXTjFneFVWY3hhbEp1V0c0dFRXUnFVakkzWm1KS1lUaEZWa3RuYTJNaWZRIn0.5_CFzPpvvbxubtXyz7jsmwqaA3SPcebk9OvSWGnORMMfIgh6iWYj5DjXgTst3Mz7ZLjK0AaCF628hwis5hL3AA
Content-Type: application/json
{
"proof": {
"proof_type": "jwt",
"jwt": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWVDSTZJalp0TFU1NE9HWndSMWxUT0ZoV04xZ3hVVmN4YWxKdVdHNHRUV1JxVWpJM1ptSktZVGhGVmt0bmEyTWlmUSMwIiwidHlwIjoib3BlbmlkNHZjaS1wcm9vZitqd3QifQ.eyJhdWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2llQ0k2SW5SMGNFUTVhekpzUkhkMVdraG5kMlZZVWw5blVHeFNkR1pPYTBGVGVYWXpNR1ZYUkhseVVFOVJNVVVpZlEiLCJpYXQiOjE3MTA4NjMxOTEsIm5vbmNlIjoidFppZ25zbkZicCJ9.MqxVdgX2tJMDJhNYuniYE7bjLyybRgRcPlDCY2ZN9FU2acLC5kU_wAZq-IF8vfIRXfmhbiFoBbfoPUZNBgdDAw"
}
}
The proof
JWT contains various claims, most notably a nonce
to prevent replay attacks. For a detailed look at each field please refer to the OpenID spec.
Authorization: Bearer {jwt}
: Theaccess_token
that the Wallet recieved from the/token
endpoint.
Before issuing a Known Customer Credential, it's important to complete all necessary steps of your Know Your Customer (KYC) program. This may include further screenings on the user's DID/information, such as Sanctions Checks, Politically Exposed Persons (PEP) screenings, and Negative News Screenings, which aren't covered in this guide. Consult with your legal and compliance advisors to ensure that this process is consistent with your specific legal and compliance obligations.
- Implement the
/credentials
endpoint to handle incoming credential requests from the Wallet. This endpoint performs several important steps:
- Extracts and validates the
access_token
from the Auth header - Validates the Signed Challenge Nonce (
c_nonce
) - Creates the Known Customer Credential
- Responds with the Signed credential
- The
id
referenced in thecredentialSchema
field must correspond to a valid JSON schema that is hosted on an accessible server. - Signing will return a VC JSON Web Token, which is ideal for secure transmission of the credential.
To convert the signed VC JWT into a VerifiableCredential
object you can use the parseJwt()
method. Here is what your decoded Known Customer Credential would look like:
{
"vcDataModel": {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"type": ["VerifiableCredential", "KnownCustomerCredential"],
"id": "urn:uuid:12345678-1234-5678-1234-567812345678",
"issuer": "did:dht:z6Mkn4w6nSaWe4fjNJRvaHZwFnMm5VexvjzDeozEu2G7jC34",
"issuanceDate": "2024-05-29T19:23:24Z",
"expirationDate": "2026-05-19T08:02:04Z",
"credentialSubject": {
"id": "did:dht:z6MkjGSeekPGE9QfczHWyW8v2ZzJU68kqSHzV7L2dmQyuyDu",
"country_of_residence": "US",
"tier": "Gold"
},
"credentialSchema": {
id: "https://schema.org/PFI",
type: "JsonSchema"
},
"evidence": [
{
"kind": "document_verification",
"checks": ["passport", "utility_bill"]
},
{
"kind": "sanction_screening",
"checks": ["PEP"]
}
]
}
}
With that, you've successfully gone through the Identity Verification flow, and issued a Known Customer Credential. Please note that this example is a foundational implementation. For a production environment, it's crucial to enhance this basic setup with comprehensive error handling, and security measures.
Was this page helpful?
Connect with us on Discord
Submit feedback: Open a GitHub issue
Edit this page: GitHub Repo
Contribute: Contributing Guide