Skip to main content
Version: v0.5.x

Issuing Credentials using the OpenID4VC Issuer Module

This tutorial will guide you through the process of issuing credentials using the OpenID4VC Issuer Module. Before starting this tutorial, make sure you have completed the OpenID4VC Issuer Module Setup.

This guides only covers the issuance of credentials using the OpenID4VC Issuer Module. Follow the Receiving and Proving Credentials using the OpenID4VC Holder Module guide to learn how to receive and prove credentials using the OpenID4VC Holder Module.

Creating the issuer

Once you have set-up your agent (under issuer variable), we first need to configure your issuer and the credentials you want to issue.

import { JwaSignatureAlgorithm } from '@credo-ts/core'

// Create an issuer with one supported credential: AcmeCorpEmployee
const openid4vcIssuer = await issuer.modules.openId4VcIssuer.createIssuer({
display: [
{
name: 'ACME Corp.',
description: 'ACME Corp. is a company that provides the best services.',
text_color: '#000000',
background_color: '#FFFFFF',
logo: {
url: 'https://acme.com/logo.png',
alt_text: 'ACME Corp. logo',
},
},
],
credentialsSupported: [
{
format: 'vc+sd-jwt',
vct: 'AcmeCorpEmployee',
id: 'AcmeCorpEmployee',
cryptographic_binding_methods_supported: ['did:key'],
cryptographic_suites_supported: [JwaSignatureAlgorithm.ES256],
},
],
})

// Create a did:key that we will use for issuance
const issuerDidResult = await issuer.dids.create<KeyDidCreateOptions>({
method: 'key',
options: {
keyType: KeyType.Ed25519,
},
})

if (issuerDidResult.didState.state !== 'finished') {
throw new Error('DID creation failed.')
}

If you want to update the display metadata or the credentials supported by the issuer, you can use the issuer.modules.openId4VcIssuer.updateIssuer method.

Creating a credential offer

Once you have configured the issuer, you can create a credential offer. The credential offer method will generate a credential offer URI that you can share with a holder.

const { credentialOffer, issuanceSession } = await issuer.modules.openId4VcIssuer.createCredentialOffer({
issuerId: openid4vcIssuer.issuerId,
// values must match the `id` of the credential supported by the issuer
offeredCredentials: ['AcmeCorpEmployee'],

// Only pre-authorized code flow is supported
preAuthorizedCodeFlowConfig: {
userPinRequired: false,
},

// You can store any metadata about the issuance here
issuanceMetadata: {
someKey: 'someValue',
},
})

// Listen and react to changes in the issuance session
issuer.events.on<OpenId4VcIssuanceSessionStateChangedEvent>(
OpenId4VcIssuerEvents.IssuanceSessionStateChanged,
(event) => {
if (event.payload.issuanceSession.id === issuanceSession.id) {
console.log('Issuance session state changed to ', event.payload.issuanceSession.state)
}
}
)

We have also added an event listener that listens for state changed events, this allows us to know when the issuance session is done.

Implementing the credential mapper

The OpenID4VC Issuer Module setup didn't cover the implementation of the credentialRequestToCredentialMapper yet. When you create a credential offer with the OpenID4VC Issuer Module in Credo, you don't have to provide the credential data directly.

Instead, you provide a credentialRequestToCredentialMapper function in the agent configuration, that will be called when the holder requests the credential.

This allows you to dynamically generate the credential data based on the holder's request, and means you also don't have to store any credential data in the agent.

Below is an example credentialRequestToCredentialMapper function that generates a credential based on the holder's request. Make sure to register this function in the agent configuration modules.openId4VcIssuer.endpoints.credential.credentialsRequestToCredentialMapper.

import {
OpenId4VcIssuanceSessionStateChangedEvent,
OpenId4VcIssuerEvents,
OpenId4VcVerificationSessionState,
OpenId4VcVerificationSessionStateChangedEvent,
OpenId4VcVerifierEvents,
OpenId4VciCredentialFormatProfile,
OpenId4VciCredentialRequestToCredentialMapper,
} from '@credo-ts/openid4vc'

const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper = async ({
// agent context for the current wallet / tenant
agentContext,
// the credential offer related to the credential request
credentialOffer,
// the received credential request
credentialRequest,
// the list of credentialsSupported entries
credentialsSupported,
// the cryptographic binding provided by the holder in the credential request proof
holderBinding,
// the issuance session associated with the credential request and offer
issuanceSession,
}) => {
const firstSupported = credentialsSupported[0]

// We only support vc+sd-jwt in this example, but you can add more formats
if (firstSupported.format !== OpenId4VciCredentialFormatProfile.SdJwtVc) {
throw new Error('Only vc+sd-jwt is supported')
}

// We only support AcmeCorpEmployee in this example, but you can support any type
if (firstSupported.vct !== 'AcmeCorpEmployee') {
throw new Error('Only AcmeCorpEmployee is supported')
}

// find the first did:key did in our wallet. You can modify this based on your needs
const didsApi = agentContext.dependencyManager.resolve(DidsApi)
const [didKeyDidRecord] = await didsApi.getCreatedDids({
method: 'key',
})

const didKey = DidKey.fromDid(didKeyDidRecord.did)
const didUrl = `${didKey.did}#${didKey.key.fingerprint}`

return {
credentialSupportedId: firstSupported.id,
format: 'vc+sd-jwt',
// We can provide the holderBinding as is, if we don't want to make changes
holder: holderBinding,
payload: {
vct: firstSupported.vct,
firstName: 'John',
lastName: 'Doe',
},
disclosureFrame: {
_sd: ['lastName'],
},
issuer: {
method: 'did',
didUrl,
},
}
}