owned this note
owned this note
Published
Linked with GitHub
# Guide Draft
[ Moe's GitHub for reference](https://github.com/tbdeng/known-customer-credential?tab=readme-ov-file#implementation-details) (this line isn't apart of the guide)
notes:
- add section in setup for guidance on how server is configured for issuer, when it comes to their metadata
# Known Customer Credential
Known Customer Credentials (KCCs) are [Verifiable Credentials](https://developer.tbd.website/docs/web5/learn/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 Guide](https://docs.google.com/document/d/1vwcqzXOpVrCUv7U7g3A4jJs99Lj18gAVy4FaA3COEb8/edit). This resource 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.
:::danger
IMPORTANT: 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), following Self Issued OpenID Provider V2 (SIOPv2) and [OpenID for Verifiable Presentation's (OID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) set of specifications.
- Designing a KCC, detailing the required properties.
- Issuing a KCC, following successful IDV.
:::info
For a detailed guide on Known Customer Credentials for Wallets, please refer to our [Wallet's Known Customer Credential Guide](https://hackmd.io/SQwE3SnrRkmeipGGtznJTw)
:::
## Environment Setup
If you haven't already, please follow our [Credential Issuance Server Setup Guide](https://hackmd.io/4cW3D9BVQGCEuf1CEg47jA?both#Credential-Issuance-Enviornment-Setup-Guide) for detailed instructions on the dependencies and packages needed to set up your server.
## IDV Process
Identity Verification (IDV) is a critical component of typical Know Your Customer (KYC) requirements, where certain types of Personally Identifying Information (PII) are collected and verified from an individual. IDV is a crucial step before a Known Customer Credential (KCC) can be issued.
<<<<<<<<<<<<<< IDV Flow diagram >>>>>>>>>>>>>>
The IDV flow begins when the customer's agent (e.g. Wallet application) sends an HTTP request to the Issuer's [IDV service endpoint](https://hackmd.io/4cW3D9BVQGCEuf1CEg47jA?both#Issuers-Identity) specified in the Issuer's DID.
### Handling Incoming Requests
1. Create an endpoint to handle incoming GET requests from the Wallet. When a request is received, you'll need to construct the [SIOPv2]() (Self Issued OpenID Provider V2) Authorization Request.
### Approach 1: Requesting only `id_token`
This approach is used when the goal is solely to authenticate the user without requesting any additional Verifiable Credentials.
```javascript
// Issuer api.js for id_token only
app.get('/idv/siopv2/initiate', async (req, res) => {
const siopRequestOnlyIdToken = {
client_id: issuerDid, // Issuer's Decentralized Identifier
scope: "openid", // Standard OpenID Connect scope
response_type: "id_token", // Only requesting an ID Token
response_uri: "https://issuer.example.com/siopv2/response", // Endpoint for SIOP response delivery
response_mode: "direct_post", // Delivery method of the SIOP response
nonce: "n-0S6_WzA2Mj" // Unique string to link the request and response
// Note: No presentation_definition is included, as we're not requesting a vp_token
};
// Construct and send the SIOPv2 Authorization Request
});
```
### Approach 2: Requesting both `id_token` and `vp_token`
This approach is used when you need to authenticate the user and also want to request specific Verifiable Credentials.
```javascript
// Issuer api.js for id_token and vp_token
app.get('/idv/siopv2/initiate', async (req, res) => {
// Construct the SIOPv2 Authorization Request
const siopRequest = {
client_id: issuerDid, // Issuer's Decentralized Identifier
scope: "openid", // Standard OpenID Connect scope
response_type: "id_token vp_token", // Expected response formats: ID Token and optionally, Verifiable Presentation Token
response_uri: "https://issuer.example.com/siopv2/response", // Endpoint for SIOP response delivery
response_mode: "direct_post", // Delivery method of the SIOP response
nonce: "n-0S6_WzA2Mj", // Unique string to link the request and response
client_metadata: {
// Descriptive metadata about the requesting party (issuer)
subject_syntax_types_supported: "did:dht did:jwk",
client_name: "Issuance Service Name",
client_uri: "https://issuer.example.com",
logo_uri: "https://issuer.example.com/logo.png",
tos_uri: "https://issuer.example.com/tos",
policy_uri: "https://issuer.example.com/privacy"
},
presentation_definition: {
id: "IDCardCredentials",
input_descriptors: [
{
id: "IDCardCredential",
schema: {
uri: [
"https://www.w3.org/2018/credentials#VerifiableCredential",
"https://www.w3.org/2018/credentials/examples/v1#IDCardCredential"
],
name: "ID Card Credential",
purpose: "We need to verify your identity."
},
constraints: {
fields: [
{
path: ["$.vc.credentialSubject.given_name"],
purpose: "The given name on your ID card."
},
{
path: ["$.vc.credentialSubject.family_name"],
purpose: "The family name on your ID card."
},
{
path: ["$.vc.credentialSubject.birthdate"],
purpose: "Your birth date."
},
{
path: ["$.vc.credentialSubject.national_identifier"],
purpose: "Your national identifier."
}
]
}
}
]
}
};
});
```
This request contains information about you, the issuer, along with a [Presentation Definition](https://developer.tbd.website/docs/web5/build/verifiable-credentials/presentation-definition).
* **`client_id`**: Issuer's DID, establishing the issuer/requestor's identity
* **`response_type`**: Lets the wallet know the desired format, with `id_token` being required identity token and `vp_token` for *optional* verifiable presentation token. The inclusion of `vp_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
:::info
For more information on the schema, refer to the [Presentation Definition spec](https://identity.foundation/presentation-exchange/spec/v1.0.0/#json-schema-2).
:::
2. Encode the SIOPv2 Authorization Request and respond
```javascript
// Issuer api.js
const queryString = Object.entries(siopRequest).map(([key, value]) => {
if (typeof value === 'object') {
value = encodeURIComponent(JSON.stringify(value));
} else {
value = encodeURIComponent(value);
}
return `${encodeURIComponent(key)}=${value}`;
}).join('&');
res.send(queryString);
```
3. The Wallet will then respond with a POST SIOPv2 Authorization response. This response will be [JSON Web Tokens (JWTs)](https://jwt.io/). Here's an example response where `vp_token` and `id_token` is in the Auth Response:
```javascript
// wallet id_token JWT response
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6ZGh0OmN1c3RvbWVyRGlkIiwic3ViIjoiZGlkOmRodDpjdXN0b21lckRpZCIsImF1ZCI6Imlzc3VlckRpZC51cmkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsImV4cCI6MTYxODg4NDQ3MywiaWF0IjoxNjE4ODgwODczfQGUuY29tL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiSURDYXJkQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJnaXZlbl9uYW1lIjoiRnJlZHJpayIsImZhbWlseV9uYW1.Imlzc3VlckRpZC51cmkiLCJub25jZSI6Im4tMFM2X1d6QTJNaiIsImV4cCI6MTYxODg4NDQ3MywiaWF0IjoxNjE4ODgwODcz
```
```javascript
// wallet vp_token JWT response
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwiaWQiOiJodHRwczovL2V4YW1wbGUuY29tL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiSURDYXJkQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZTppc3N1ZXIifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJnaXZlbl9uYW1lIjoiRnJlZHJpayIsImZhbWlseV9uYW1lIjoiU3RyJm9tYmVyZyIs.ImJpcnRoZGF0ZSI6IjE5NDktMDEtMjIifX1dLCJob2xkZXIiOiJkaWQ6ZXhhbXBsZTpob2xkZXIiLCJwcm9vZiI6eyJ0eXBlIjoiRWQyNTUxOVNpZ25hdHVyZTIwMTgiLCJjcmVhdGVkIjoiMjAyMS0wMy0xOVQxNTozMDoxNVoiLCJjaGFsbGVuZ2UiOiJuLTBTNl9XekEyTWoiLCJkb21haW4iOiJodHRwczovL2NsaWVudC5leGFtcGxlLm9yZy9jYiIsImp3cyI6ImV5SmhiR2NpT2lKSVV6STFOaUo5LkhvbGRlcl9GT1JfU0lHTkFUVVJFIiwicHJvb2ZQdXJwb3NlIjoiYXV0aGVudGljYXRpb24iLCJ2ZXJpZmljYXRpb25NZXRob2QiOiJkaWQ6ZXhhbXBsZTpob2xkZXIja2V5LTEifX0
```
When decoded here is the Human-Readable JSON of `id_token` and `vp_token`:
```json
// wallet
{
"id_token": {
"iss": "did:dht:customerDid",
"sub": "did:dht:customerDid",
"aud": "did:dht:issuerDid", // issuer's did string
"nonce": "n-0S6_WzA2Mj",
"exp": 1618884473,
"iat": 1618880873
},
"vp_token": {
"@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": "$.verifiableCredential[0]"
},
{
"id": "nationalIdentifierVerification",
"format": "jwt_vc",
"path": "$.verifiableCredential[1]"
}
]
},
"verifiableCredential": [
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyI3o2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtlcjQ5Q251Zzdoc3ZIRGd2NDR5dnBkdnRNaDR5TGlEWHhTNjdobnJVaHR0MiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFbXBsb3ltZW50Q3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjcyNDhiOTkyLTkwOTYtNDk2NS1hMGVjLTc3ZDhhODNhMWRmYiIsImlzc3VlciI6ImRpZDprZXk6ejZNa2VyNDlDbnVnN2hzdkhEZ3Y0NHl2cGR2dE1oNHlMaURYeFM2N2huclVodHQyIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDoxMToyNVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlEMTR4UmY0cTJNWlh1ZWY2X2ZXYnBGbVlTUG94dGFxTkp1SmdEMG96Wl84UTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWVubGFNbVYzTlhKeVVXdFVjbUV3WlZsVk16WlBTblJzTURCbFJWZHhhalZhV0dkNmNEZFpSVTVKUVNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpQZDJZMFQyMUViamxKWm5SNFdYWnBkRTFHWm1jMVVXeDVMVVV6VWs1b1dsUkdPVlpFTWtnNVQzVjNJaXdpZVNJNkltUnZjVmxtV2s1c1NtRlRNVll4U201bU9HdEZObEF6VkRsd2QzaDNla3hFVTJWc1ZqTlRUa2s1U2xFaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xEWm05bVFUQkpVbU5uY2tWdVVHZHdRbU5RV1ZsV2VFWlliR0pTYjJRd2RVNWZRVkJwTkVrNUxVRmZRU0o5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFFtd3pWWG80VldGT2REZGxlREJKYjJJMFJFNXNhbFJGVmpaelQwTmtjbFJ3TWxvNE5FTkJPVFJPUWtFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVOWk9WRldZbWRKYkUxemRraEZYMVJtTld4a1MxQjBkR3d3WVV4blNrdHNSbmt6Vms0d2QzQTJhVFpSSW4xOSIsImVtcGxveW1lbnRTdGF0dXMiOiJlbXBsb3llZCJ9fX0.Sazc8Ndhs-NKjxvtVMKeC9dxjEkI26fVsp2kFNWM-SYLtxMzKvl5ffeWd81ysHgPmBBSk2ar4dMqGgUsyM4gAQ",
"eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0I3o2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtqcFM0R1VBaGJnQkpoNmsyZ2U2b1k0NFFMckVwNzVyWnRzamFUS29yUkRkdCIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJOYW1lQW5kRG9iQ3JlZGVudGlhbCJdLCJpZCI6InVybjp1dWlkOjliZjM2YzY5LTI0ODAtNDllZC1iMTYyLTRlZDEwOWE3MTc3NyIsImlzc3VlciI6ImRpZDprZXk6ejZNa2pwUzRHVUFoYmdCSmg2azJnZTZvWTQ0UUxyRXA3NXJadHNqYVRLb3JSRGR0IiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMi0yMVQyMDowNjowMVoiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDppb246RWlDS2o2M0FyZlBGcEpsb2lTd3gxQUhxVWtpWlNoSDZGdnZoSzRaTl9fZDFtQTpleUprWld4MFlTSTZleUp3WVhSamFHVnpJanBiZXlKaFkzUnBiMjRpT2lKeVpYQnNZV05sSWl3aVpHOWpkVzFsYm5RaU9uc2ljSFZpYkdsalMyVjVjeUk2VzNzaWFXUWlPaUprZDI0dGMybG5JaXdpY0hWaWJHbGpTMlY1U25kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaWNscFdXbTVJVkVrNWFEWkJUVmxVV0dwT01HcFhTVkYwTTI5ak4xTnJTeTF4Y2kxcVVuSTBUalEzUlNKOUxDSndkWEp3YjNObGN5STZXeUpoZFhSb1pXNTBhV05oZEdsdmJpSmRMQ0owZVhCbElqb2lTbk52YmxkbFlrdGxlVEl3TWpBaWZTeDdJbWxrSWpvaVpIZHVMV1Z1WXlJc0luQjFZbXhwWTB0bGVVcDNheUk2ZXlKamNuWWlPaUp6WldOd01qVTJhekVpTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpaVDFwRE5WSmlUMHQ1T0dadVVUWTJVWEZPUkc5aldFMXZPVXhUZEdNNVYyOWthMHd0ZFZCZlExQnZJaXdpZVNJNklsWnZZM0UxVERodFozQlhXVTFrYjFwS1JrWlJUa1ZDT0hsR0xXTndkRWQzZFdkcFRWVm5hR2t6Y21jaWZTd2ljSFZ5Y0c5elpYTWlPbHNpYTJWNVFXZHlaV1Z0Wlc1MElsMHNJblI1Y0dVaU9pSktjMjl1VjJWaVMyVjVNakF5TUNKOVhTd2ljMlZ5ZG1salpYTWlPbHQ3SW1sa0lqb2laSGR1SWl3aWMyVnlkbWxqWlVWdVpIQnZhVzUwSWpwN0ltVnVZM0o1Y0hScGIyNUxaWGx6SWpwYklpTmtkMjR0Wlc1aklsMHNJbTV2WkdWeklqcGJJbWgwZEhCek9pOHZaSGR1TG5SaVpHUmxkaTV2Y21jdlpIZHVOaUlzSW1oMGRIQnpPaTh2WkhkdUxuUmlaR1JsZGk1dmNtY3ZaSGR1TUNKZExDSnphV2R1YVc1blMyVjVjeUk2V3lJalpIZHVMWE5wWnlKZGZTd2lkSGx3WlNJNklrUmxZMlZ1ZEhKaGJHbDZaV1JYWldKT2IyUmxJbjFkZlgxZExDSjFjR1JoZEdWRGIyMXRhWFJ0Wlc1MElqb2lSV2xCTXpSMlMzb3llVmswZVV4dGRDMUdabkJuYWpWbGFFRm1ZWFI1YzFOa2MwNVNWbVpMYkhwUWRqTjVkeUo5TENKemRXWm1hWGhFWVhSaElqcDdJbVJsYkhSaFNHRnphQ0k2SWtWcFF6ZGZjMXBzTW1wMVVXNUdhRVJIV1RSb2NFVTRiMlF4YVU5MWRuZG1PVFJ5TVVkbk9HMWFWbVJCVmxFaUxDSnlaV052ZG1WeWVVTnZiVzFwZEcxbGJuUWlPaUpGYVVKdU5sTnJiSEpWYzNKdVFuaFJPVXBqVXkxTlNVaGtWelYwTXpRM1MxWjNaMXBwVEZwMFQwcDRRVkYzSW4xOSIsIm5hbWUiOiJhbGljZSBib2IiLCJkYXRlT2ZCaXJ0aCI6IjEwLTAxLTE5OTAifX19.mNCDv_JntH-wZpYONKNL58UbOWaYXCYJO_HPI_WVlSgwzo6dhYmV_9qtpFKd_exFb-aaEYPeSE43twWlrJeSBg"
]
}
}
```
4. Create a `/siopv2/response` endpoint to verify and handle the Wallet's response. If the Wallet provides a [Verifiable Presentation](https://developer.tbd.website/docs/glossary#verifiable-presentation) (vp_token), we'll proceed with sending just a `credential_offer`. If the response only has `id_token`, the `credential_offer` object will also include a URL to an Identity Verification form.
```javascript
// Issuer api.js
import { VerifiableCredential, Jwt } from '@web5/credentials';
// replaced with a more secure storage & DID linking solution
const preAuthCodeToDidMap = new Map();
app.post('/siopv2/response', async (req, res) => {
const walletResponse = req.body; // The SIOPv2 Authorization Response from the wallet
/************************************************************
* Extract and verify the ID Token from the wallet's response
*************************************************************/
try {
const compactIdToken = walletResponse.id_token;
if (!compactIdToken) {
return res.status(400).json({ message: "Missing ID Token" });
}
const idTokenVerificationResult = await Jwt.verify({ jwt: compactIdToken })
/************************************************************
* Extract customers Did from verificationResult
*************************************************************/
const customersDidUri = idTokenVerificationResult.payload.sub;
// Perform additional checks (e.g., nonce, audience, expiration)
}
/************************************************************
* Extract and verify the VP Token from the wallet's response
*************************************************************/
let credentialOffer;
if (walletResponse.vp_token) {
const compactVpToken = walletResponse.vp_token;
const vpTokenVerificationResult = await Jwt.verify({ jwt: compactVpToken });
/**************************************************************************
* Generate a unique Pre-Authorization Code and map it to the Customer's DID
***************************************************************************/
const preAuthCode = generateUniquePreAuthCode();
preAuthCodeToDidMap.set(preAuthCode, customersDidUri);
/************************************************************************
* Construct Credential Offer without URL since VP Token is present
*************************************************************************/
credentialOffer = {
"credential_issuer": "https://issuer.example.com/credentials/.well-known/openid-credential-issuer", // this a set value: .well-known/openid-credential-issuer
"credential_configuration_ids": [
"knownCustomerCredential-basic",
"knownCustomerCredential-extended"
],
"grants": {
"urn:ietf:params:oauth:grant-type:pre-authorized_code":
preAuthCode
}
};
} else {
/************************************************************************
* If VP token is not present, include URL for IDV form
*************************************************************************/
credentialOffer = {
"url": "https://issuer.example.com/idv/form", // IDV Request form
"credential_issuer": "https://issuer.example.com",
"credential_configuration_ids": ["knownCustomerCredential-basic", "knownCustomerCredential-extended"],
"grants": {"urn:ietf:params:oauth:grant-type:pre-authorized_code": generateUniquePreAuthCode()}
};
res.json(credentialOffer);
} catch (error) {
// Handle verification errors
return res.status(401).json({
errors: ['Invalid token', error.message]
});
}
});
```
:::info
When the Wallet only responds with `id_token`, your response must include a **`url`** (IDV Request Form). This will point to a webpage for the customer to input their information (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 that allows the Wallet to follow a Pre-Authorized code flow skipping any PII steps because they've already completed it.
- **`pre-authorized_code`**: The code representing the Credential Issuer's authorization for the Wallet to obtain an access token.
:::info
**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.
:::
6. Implement the `/.well-known/openid-credential-issuer` endpoint that will return the Issuer's Metadata:
```javascript
// Issuer api.js
app.get('/.well-known/openid-credential-issuer', (req, res) => {
const issuerMetadata = {
"credential_issuer": "https://issuer.example.com",
"credential_endpoint": "https://issuer.example.com/credentials",
"credential_configurations_supported": { // type of credentials issuer supports & what credential will look like
"KnownCustomerCredential": {
"format": "jwt_vc_json",
"scope": "CustomerIdentity",
"cryptographic_binding_methods_supported": ["did:example"],
"credential_signing_alg_values_supported": ["ES256"],
"credential_definition": {
"type": ["VerifiableCredential", "KnownCustomerCredential"],
"credentialSubject": {
"country": {
"display": [{"name": "Country", "locale": "en-US"}]
}
}
},
"proof_types_supported": {
"jwt": {
"proof_signing_alg_values_supported": ["ES256"]
}
},
"display": [
{
"name": "Known Customer Credential",
"locale": "en-US",
"logo": {
"url": "https://issuer.example.com/public/logo.png",
"alt_text": "Issuer Logo"
},
"background_color": "#FFFFFF",
"text_color": "#000000"
}
]
}
}
};
res.json(issuerMetadata);
});
```
* `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 format `jwt_vc_json` and the credential
7. Create the `/.well-known/oauth-authorization-server` endpoint that will return the Issuer's Authorization Server Metadata:
```javascript
// Issuer api.js
app.get('/.well-known/oauth-authorization-server', (req, res) => {
const oauthAuthorizationServerMetadata = {
"issuer": "https://issuer.example.com", // URL of the Credential Issuer
"token_endpoint": "https://issuer.example.com/token", // URL for the Access Token Request
};
res.json(oauthAuthorizationServerMetadata);
});
```
* `issuer`: The base URL of the issuer's service.
* `token_endpoint`: The endpoint where Wallet's can exchange the `pre_authorization_code` receieved in the `credential_offer` for an access token.
8. Implement the `/token` endpoint to accept the `pre_authorization_code`, from the Wallet, which is from the `credential_offer` object:
```javascript
// Issuer api.js
import jwt from 'jsonwebtoken';
app.post('/token', async (req, res) => {
const { grant_type, code } = req.body;
if (grant_type !== 'urn:ietf:params:oauth:grant-type:pre-authorized_code') {
return res.status(400).json({ error: 'unsupported_grant_type' });
}
const customersDidUri = preAuthCodeToDidMap.get(code);
if (!customersDidUri) {
return res.status(400).json({ error: 'invalid_grant' });
}
/*******************************************
Create the payload for the access token
********************************************/
const accessTokenPayload = {
sub: customersDidUri, // Customer's DID string
iss: issuersBearerDid.uri, // Issuer's DID string
iat: Math.floor(Date.now() / 1000), // Issued at
exp: Math.floor(Date.now() / 1000) + 86400, // Expiration time
};
/*******************************************
sign accessToken and generate a c_nonce
********************************************/
const accessToken = jwt.sign(accessTokenPayload, JWT_SECRET_KEY);
const cNonce = generateCNonce();
storeCNonce(accessToken, cNonce);
res.json({
"access_token": accessToken,
"token_type": "bearer",
"expires_in": 86400, // Token expiration time
"c_nonce": cNonce, // Challenge nonce to be signed
"c_nonce_expires_in": 86400, // cNonce expiration time
});
});
```
## 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.
1. Define a `KccCredential` class with the required fields for the credential:
```javascript
// Issuer kccCredential.js
class KccCredential {
constructor(country) {
this.country = country
}
}
```
:::info
- You'll need to assign`issuer`, `subject`, and `expirationDate` properties during the creation process. `issuanceDate` is managed and generated by the `Web5` 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](https://www.npmjs.com/package/@web5/credentials) package.
1. 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:
```json
POST /credentials HTTP/1.1
Host: issuer.example.com
Authorization: Bearer czZCaGRSa3F0MzpnWDFmQmF0M2JW // access_token
Content-Type: application/json
{
"proof": {
"proof_type": "jwt",
"jwt": "eyJ0eXAiOiJvcGVu...ZoipdP-jvh1WlA"
}
}
```
The `proof` section contains various claims, for a detailed look at each field please refer to the [OpenID specs](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#section-7.2.1.1-2.1.1).
* `czZCaGRSa3F0MzpnWDFmQmF0M2JW`: The access token that the wallet recieved from the `/token` endpoint.
:::danger
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.
:::
2. Create an `app.post('/credentials')` endpoint to validate the request, create and sign the Known Customer Credential
```javascript
import KccCredential from './kccCredential.js';
app.post('/credentials', async (req, res) => {
try {
/********************************************************
* Extract and validate the JWT from Authorization header
*********************************************************/
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(401).json({ errors: ['Authorization header required'] });
}
const tokenParts = authHeader.split('Bearer ');
if (tokenParts.length !== 2) {
return res.status(401).json({ errors: ['Authorization header format is Bearer <token>'] });
}
const compactJwt = tokenParts[1];
let customersDidUri, payload;
try {
const verificationResult = await Jwt.verify({ jwt: compactJwt });
customersDidUri = verificationResult.payload.sub; // Customer's DID string
payload = verificationResult.payload;
} catch (error) {
return res.status(401).json({ errors: ['Invalid token'] });
}
/***********************************************
* Validate the signed c_nonce
************************************************/
const { proof } = req.body;
if (!proof || proof.proof_type !== 'jwt' || !proof.jwt) {
return res.status(400).json({ errors: ['Invalid proof provided'] });
}
if (!validateSignedCNonce(proof.jwt, payload.nonce, customersDidUri)) {
return res.status(401).json({ errors: ['Invalid proof'] });
}
/***********************************************
* Create and sign the credential
************************************************/
const known_customer_credential = await VerifiableCredential.create({
type: 'KnownCustomerCredential',
issuer: issuerDid.uri, // Issuer's DID string
subject: customersDidUri, // Customer's DID string from the verified JWT
expirationDate: '2026-05-19T08:02:04Z',
data: new KccCredential('US'),
});
const credential_token = await known_customer_credential.sign({
did: issuerDid, // Signing with the issuer's bearer DID
});
/***********************************************
* Respond with the signed credential
************************************************/
return res.status(200).json({ credential: credential_token });
} catch (error) {
/***********************************************
* Generic error handling
************************************************/
return res.status(500).json({
errors: [`An unexpected error occurred: ${error.message}`],
});
}
});
```
:::info
Signing will return a VC JSON Web Token, which is ideal for secure transmission of the credential.
:::
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.
# Credential Issuance Enviornment Setup Guide
:::spoiler
In this guide we're going to cover how to set your system up to create, issue and sign [Verifiable Credentials](https://developer.tbd.website/docs/web5/learn/verifiable-credentials).
### Install Express
Follow these instructions to [install Express](https://expressjs.com/en/starter/installing.html).
### Install Credentials Package
```bash
npm i @web5/credentials
```
### Issuer's Identity
To issue a credential, you need a [Decentralized Identifier (DID)](https://developer.tbd.website/docs/web5/learn/decentralized-identifiers/) which will serve as your Issuer's identifier. If you don't already have one, you'll need to create a DID with an IDV(Identity Verification) service endpoint.
```javascript
const issuerDid = await DidDht.create({
options:{
publish: true,
services: [
{
id: 'idv',
type: 'IDV',
serviceEndpoint:
'https://exampleIdvEndpoint.com/idv/siopv2/initiate'
}
]
}
})
// create a did with 2 service endpoints
const issuerDid = await DidDht.create({
options:{
publish: true,
services: [
{
id: 'idv',
type: 'IDV',
serviceEndpoint:
'https://exampleIdvEndpoint.com/idv/siopv2/initiate'
},
{
id: 'pfi',
type: 'PFI',
serviceEndpoint:
'https://example.com/' // should be the URL to your PFI's service entry
}
]
}
})
// update existing did with 'idv' endpoint
issuerDid.document.service.push({
id: 'idv',
type: 'IDV',
serviceEndpoint: 'https://exampleIdvEndpoint.com/idv/siopv2/initiate'
});
await DidDht.publish({did: issuerDid});
```
The value for `serviceEndpoint` should be the full URL to your IDV service's entry point where the SIOPv2 initiation request can be sent.
:::info
- To use a different DID method, refer to our [Create A DID](https://developer.tbd.website/docs/web5/build/decentralized-identifiers/how-to-create-did) guide
- It's essential to keep your private keys secure. Please review our [Key Management guide](https://developer.tbd.website/docs/web5/build/decentralized-identifiers/key-management) for more details
:::
:::spoiler
### Create API Service
( can link them to Follow [steps 1-4](https://developer.tbd.website/docs/web5/learn/decentralized-identifiers/) from Issuance Guide)
1. Ensure you have a file, such as `main.js`, to act as your API's entry point, which was created during the Express setup.
2. Within the file, import the following:
```javascript
import { api } from './api.js';
```
3. To start your Express API service, add the following:
```javascript
const config = {
port: 3000, // this can be any port you want to listen on
};
const server = api.listen(config.port, () => {
log.info(`Server listening on port ${config.port}`);
});
```
4. Next, create an `api.js` file. This file will define the core API routes and logic for the IDV flow and issuing credentials:
```javascript
// Issuer api.js
import express from 'express';
const app = express();
app.use(express.json());
// Express route to handle incoming GET requests
app.get('/siopv2/request', async (req, res) => {
// implement API route
});
// Export the app so it can be imported in your main.js file
export { app as api };
```
:::