DIDComm Credential Issuance
The DIDComm approach uses the Issue Credential protocol to exchange credentials between connected agents.import {
Agent,
DidsModule,
} from '@credo-ts/core'
import {
AnonCredsModule,
AnonCredsDidCommCredentialFormatService,
DidCommCredentialV2Protocol,
} from '@credo-ts/anoncreds'
import {
DidCommModule,
DidCommAutoAcceptCredential,
DidCommHttpOutboundTransport,
} from '@credo-ts/didcomm'
import { AskarModule } from '@credo-ts/askar'
import { IndyVdrModule, IndyVdrAnonCredsRegistry } from '@credo-ts/indy-vdr'
import { agentDependencies, DidCommHttpInboundTransport } from '@credo-ts/node'
import { anoncreds } from '@hyperledger/anoncreds-nodejs'
import { indyVdr } from '@hyperledger/indy-vdr-nodejs'
import { askar } from '@openwallet-foundation/askar-nodejs'
const issuerAgent = new Agent({
config: {
label: 'Issuer Agent',
walletConfig: {
id: 'issuer-wallet',
key: 'issuer-wallet-key',
},
},
dependencies: agentDependencies,
modules: {
didcomm: new DidCommModule({
endpoints: ['http://localhost:3001'],
transports: {
inbound: [new DidCommHttpInboundTransport({ port: 3001 })],
outbound: [new DidCommHttpOutboundTransport()],
},
credentials: {
autoAcceptCredentials: DidCommAutoAcceptCredential.ContentApproved,
credentialProtocols: [
new DidCommCredentialV2Protocol({
credentialFormats: [new AnonCredsDidCommCredentialFormatService()],
}),
],
},
}),
anoncreds: new AnonCredsModule({
registries: [new IndyVdrAnonCredsRegistry()],
anoncreds,
}),
indyVdr: new IndyVdrModule({
indyVdr,
networks: [
{
genesisTransactions: '<genesis-transactions>',
indyNamespace: 'bcovrin:test',
isProduction: false,
connectOnStartup: true,
},
],
}),
dids: new DidsModule({
resolvers: [],
registrars: [],
}),
askar: new AskarModule({
askar,
}),
},
})
await issuerAgent.initialize()
import { TypedArrayEncoder } from '@credo-ts/core'
import { transformPrivateKeyToPrivateJwk } from '@credo-ts/askar'
// Import or create a DID for the issuer
const { privateJwk } = transformPrivateKeyToPrivateJwk({
type: {
crv: 'Ed25519',
kty: 'OKP',
},
privateKey: TypedArrayEncoder.fromString('afjdemoverysercure00000000000000'),
})
const { keyId } = await issuerAgent.kms.importKey({
privateJwk,
})
const did = 'did:indy:bcovrin:test:2jEvRuKmfBJTRa7QowDpNN'
await issuerAgent.dids.import({
did,
overwrite: true,
keys: [
{
didDocumentRelativeKeyId: '#verkey',
kmsKeyId: keyId,
},
],
})
// Register schema
const schemaResult = await issuerAgent.modules.anoncreds.registerSchema({
schema: {
name: 'UniversityDegree',
version: '1.0.0',
attrNames: ['name', 'degree', 'date'],
issuerId: did,
},
options: {
endorserMode: 'internal',
endorserDid: did,
},
})
if (schemaResult.schemaState.state !== 'finished') {
throw new Error('Schema registration failed')
}
const schemaId = schemaResult.schemaState.schemaId
// Register credential definition
const credDefResult = await issuerAgent.modules.anoncreds.registerCredentialDefinition({
credentialDefinition: {
schemaId,
issuerId: did,
tag: 'latest',
},
options: {
supportRevocation: false,
endorserMode: 'internal',
endorserDid: did,
},
})
if (credDefResult.credentialDefinitionState.state !== 'finished') {
throw new Error('Credential definition registration failed')
}
const credentialDefinitionId = credDefResult.credentialDefinitionState.credentialDefinitionId
// Create out-of-band invitation
const outOfBand = await issuerAgent.didcomm.oob.createInvitation()
const invitationUrl = outOfBand.outOfBandInvitation.toUrl({
domain: 'http://localhost:3001',
})
console.log('Share this invitation URL with the holder:', invitationUrl)
// Wait for connection to be established
const [connectionRecord] = await issuerAgent.didcomm.connections.findAllByOutOfBandId(outOfBand.id)
await issuerAgent.didcomm.connections.returnWhenIsConnected(connectionRecord.id)
const credentialExchangeRecord = await issuerAgent.didcomm.credentials.offerCredential({
connectionId: connectionRecord.id,
protocolVersion: 'v2',
credentialFormats: {
anoncreds: {
attributes: [
{
name: 'name',
value: 'Alice Smith',
},
{
name: 'degree',
value: 'Computer Science',
},
{
name: 'date',
value: '2024-01-15',
},
],
credentialDefinitionId,
},
},
})
console.log('Credential offer sent:', credentialExchangeRecord.id)
import { DidCommCredentialEventTypes } from '@credo-ts/didcomm'
issuerAgent.events.on(DidCommCredentialEventTypes.DidCommCredentialStateChanged, (event) => {
console.log('Credential state changed:', event.payload.credentialExchangeRecord.state)
if (event.payload.credentialExchangeRecord.state === 'done') {
console.log('Credential successfully issued!')
}
})
OpenID4VC Credential Issuance
The OpenID4VCI approach uses modern OAuth 2.0 flows for credential issuance.import { Agent, Kms, ClaimFormat } from '@credo-ts/core'
import {
OpenId4VcModule,
OpenId4VciCredentialFormatProfile,
} from '@credo-ts/openid4vc'
import { AskarModule } from '@credo-ts/askar'
import { askar } from '@openwallet-foundation/askar-nodejs'
import express from 'express'
const app = express()
const ISSUER_HOST = 'http://localhost:2000'
const issuerAgent = new Agent({
config: {},
dependencies: agentDependencies,
modules: {
askar: new AskarModule({
askar,
store: { id: 'issuer', key: 'issuer-key' }
}),
kms: new Kms.KeyManagementModule({
backends: [],
}),
openid4vc: new OpenId4VcModule({
app,
issuer: {
baseUrl: `${ISSUER_HOST}/oid4vci`,
credentialRequestToCredentialMapper: async ({
holderBinding,
credentialConfigurationId,
}) => {
// Map credential request to actual credential
return {
type: 'credentials',
format: ClaimFormat.SdJwtDc,
credentials: holderBinding.keys.map((binding) => ({
payload: {
vct: 'UniversityDegreeCredential',
university: 'Innsbruck University',
degree: 'Bachelor of Science',
name: 'Alice Smith',
},
holder: binding,
issuer: {
method: 'did',
didUrl: `${issuerDidKey.did}#${issuerDidKey.publicJwk.fingerprint}`,
},
disclosureFrame: { _sd: ['university', 'degree', 'name'] },
})),
}
},
},
}),
},
})
await issuerAgent.initialize()
app.listen(2000)
const credentialConfigurationsSupported = {
'UniversityDegreeCredential-sdjwt': {
format: OpenId4VciCredentialFormatProfile.SdJwtVc,
vct: 'UniversityDegreeCredential',
scope: 'openid4vc:credential:UniversityDegree',
cryptographic_binding_methods_supported: ['jwk', 'did:key'],
credential_signing_alg_values_supported: ['ES256', 'EdDSA'],
proof_types_supported: {
jwt: {
proof_signing_alg_values_supported: ['ES256', 'EdDSA'],
},
},
},
}
const issuerRecord = await issuerAgent.modules.openid4vc.issuer.createIssuer({
issuerId: 'university-issuer',
credentialConfigurationsSupported,
authorizationServerConfigs: [
{
type: 'direct',
issuer: ISSUER_HOST,
clientAuthentication: {
clientId: 'holder-client',
clientSecret: 'holder-secret',
},
},
],
})
const { credentialOffer, issuanceSession } =
await issuerAgent.modules.openid4vc.issuer.createCredentialOffer({
issuerId: issuerRecord.issuerId,
credentialConfigurationIds: ['UniversityDegreeCredential-sdjwt'],
// Pre-authorized flow (no user interaction needed)
preAuthorizedCodeFlowConfig: {
authorizationServerUrl: ISSUER_HOST,
txCode: {
input_mode: 'numeric',
length: 4,
description: 'Please enter the PIN provided',
},
},
})
const credentialOfferUri = credentialOffer.credentialOfferRequestWithBaseUrl
console.log('Share this credential offer URI with the holder:', credentialOfferUri)
console.log('PIN code:', issuanceSession.preAuthorizedCode)
const { credentialOffer, issuanceSession } =
await issuerAgent.modules.openid4vc.issuer.createCredentialOffer({
issuerId: issuerRecord.issuerId,
credentialConfigurationIds: ['UniversityDegreeCredential-sdjwt'],
// Authorization code flow (requires user interaction)
authorizationCodeFlowConfig: {
authorizationServerUrl: 'https://oauth-provider.example.com',
issuerState: 'random-state-value',
},
})
console.log('Credential offer URI:', credentialOffer.credentialOfferRequestWithBaseUrl)
Next Steps
- Learn about Verifying Presentations
- Explore Connection Establishment
- Set up Multi-tenant Agents