Skip to main content
Version: v0.3.x

Issue a credential

Using AnonCreds and the Issue Credential V2 Protocol or the Issue Credential V1 Protocol.

This section assumes the following items:

  1. A valid environment for development

  2. Basic knowledge of the required fields in the agent config

  3. A connection between the Holder and Issuer

In this tutorial we will issue a credential from the Issuer to a Holder. We will start with setting up both their agents with the minimal configuration required to follow this tutorial. After the initialization we will then create a schema, credential definition and a credential as the Issuer and send the credential over to the Holder. The holder will then accept this credential and automatically store it in their wallet.

1. Setting up the agents

First for both agents we must setup and initialize an agent to work with. Depending on your target, React Native or Node.js, it might vary.

In this tutorial the Holder will be in a Node.js environment and the Issuer also in a Node.js environment.

Holder

For the Holder we need to setup a basic agent with a wallet, mediator, outbound transport and a ledger.

If you want to follow this tutorial in a mobile environment:

  1. Use the agentDependencies from @aries-framework/react-native
  2. It is very important to note that mobile agents do not support HTTP by default. It is recommended to do everything over HTTPS, but for development HTTP can be enabled for iOS and Android.
🗄holder
const initializeHolderAgent = async () => {
const genesisTransactionsBCovrinTestNet = await getGenesisTransaction('http://test.bcovrin.vonx.io/genesis')
// Simple agent configuration. This sets some basic fields like the wallet
// configuration and the label. It also sets the mediator invitation url,
// because this is most likely required in a mobile environment.
const config: InitConfig = {
label: 'demo-agent-holder',
walletConfig: {
id: 'demo-agent-holder',
key: 'demoagentholder00000000000000000',
},
indyLedgers: [
{
id: 'bcovrin-test-net',
isProduction: false,
indyNamespace: 'bcovrin:test',
genesisTransactions: genesisTransactionsBCovrinTestNet,
},
],
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
autoAcceptConnections: true,
endpoints: ['http://localhost:3002'],
}

// A new instance of an agent is created here
const agent = new Agent({ config, dependencies: agentDependencies })

// Register a simple `WebSocket` outbound transport
agent.registerOutboundTransport(new WsOutboundTransport())

// Register a simple `Http` outbound transport
agent.registerOutboundTransport(new HttpOutboundTransport())

// Register a simple `Http` inbound transport
agent.registerInboundTransport(new HttpInboundTransport({ port: 3002 }))

// Initialize the agent
await agent.initialize()

return agent
}

Issuer

For the Issuer the setup is almost the same as the Holder. The difference is, is that the Issuer does not need a mediator but an HttpInboundTransport.

It is also very important for the Issuer to have a public DID, for the binding with a credential definition, amongst other things. For this demo we will use BCovrin Test. If you want to follow this tutorial, you have to register a public DID here via the Wallet seed field (this must be the same as the seed inside the config under the key publicDidSeed).

In order to reach the Issuer we have to add a list of endpoints of the agent that exposes the inboundTransport to the public. In the example below we add an inboundTransport and use port 3000. For development purposes it is recommended to use a tunneling service for this, like Ngrok. Ngrok will allow you to reach your locally exposed endpoint from the public. If a tunneling service is used, make sure to use the HTTPS variant as mobile environments, by default, do not accept HTTP anymore.

To install Ngrok and expose the port to the public the following commands can be used:

yarn global add ngrok

ngrok http <PORT>
📄issuer
const initializeIssuerAgent = async () => {
const genesisTransactionsBCovrinTestNet = await getGenesisTransaction('http://test.bcovrin.vonx.io/genesis')
// Simple agent configuration. This sets some basic fields like the wallet
// configuration and the label.
const config: InitConfig = {
label: 'demo-agent-issuer',
walletConfig: {
id: 'demo-agent-issuer',
key: 'demoagentissuer00000000000000000',
},
publicDidSeed: 'demoissuerdidseed000000000000000',
indyLedgers: [
{
id: 'bcovrin-test-net',
isProduction: false,
indyNamespace: 'bcovrin:test',
genesisTransactions: genesisTransactionsBCovrinTestNet,
},
],
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
autoAcceptConnections: true,
endpoints: ['http://localhost:3001'],
}

// A new instance of an agent is created here
const agent = new Agent({ config, dependencies: agentDependencies })

// Register a simple `WebSocket` outbound transport
agent.registerOutboundTransport(new WsOutboundTransport())

// Register a simple `Http` outbound transport
agent.registerOutboundTransport(new HttpOutboundTransport())

// Register a simple `Http` inbound transport
agent.registerInboundTransport(new HttpInboundTransport({ port: 3001 }))

// Initialize the agent
await agent.initialize()

return agent
}

2. Registering the schema and credential definition

When we want to issue a credential we must first create a blueprint, the schema, of the credential and bind it to a specific issuer, the credential definition. This binding makes sure that when you want to verify the credential, you can request that it must be issued from a specific party.

📄issuer
const registerSchema = async (issuer: Agent) =>
issuer.ledger.registerSchema({ attributes: ['name', 'age'], name: 'Identity', version: '1.0' })

const registerCredentialDefinition = async (issuer: Agent, schema: Schema) =>
issuer.ledger.registerCredentialDefinition({ schema, supportRevocation: false, tag: 'default' })

3. Listening for incoming credentials

When we want to accept a credential, we have to listen to incoming credentials and handle accordingly. In this example we do not have any user interaction, but is likely that your application would have a user-interface which would display the credential. When receiving a credential offer you can get the values from credentialExchangeRecord.credentialAttributes.

🗄holder
const setupCredentialListener = (holder: Agent) => {
holder.events.on<CredentialStateChangedEvent>(CredentialEventTypes.CredentialStateChanged, async ({ payload }) => {
switch (payload.credentialRecord.state) {
case CredentialState.OfferReceived:
console.log('received a credential')
// custom logic here
await holder.credentials.acceptOffer({ credentialRecordId: payload.credentialRecord.id })
case CredentialState.Done:
console.log(`Credential for credential id ${payload.credentialRecord.id} is accepted`)
// For demo purposes we exit the program here.
process.exit(0)
}
})
}

4. Issuing a credential

Now that everything is setup on both sides, the Issuer can now offer a credential to the Holder.

In this example we do not instantiate a connection and assume that there is one. Please refer to this guide Create a connection to get a connection and connectionId

📄issuer
const issueCredential = async (issuer: Agent, credentialDefinitionId: string, connectionId: string) =>
issuer.credentials.offerCredential({
protocolVersion: 'v1',
connectionId,
credentialFormats: {
indy: {
credentialDefinitionId,
attributes: [
{ name: 'name', value: 'Jane Doe' },
{ name: 'age', value: '23' },
],
},
},
})

5. Full code snippets

Below are both code snippets for each agent. These can be used as base but should be editted to fit your use case. The walletConfig.key must be changed as it can lead to other people knowing your "password" to your wallet.

import {
InitConfig,
Agent,
WsOutboundTransport,
HttpOutboundTransport,
ConnectionEventTypes,
ConnectionStateChangedEvent,
DidExchangeState,
AutoAcceptCredential,
CredentialEventTypes,
CredentialState,
CredentialStateChangedEvent,
OutOfBandRecord,
} from '@aries-framework/core'
import { agentDependencies, HttpInboundTransport } from '@aries-framework/node'
import { Schema } from 'indy-sdk'
import fetch from 'node-fetch'

const getGenesisTransaction = async (url: string) => {
// Legacy code has a small issue with the call-signature from node-fetch
// @ts-ignore
const response = await fetch(url)

return await response.text()
}

const initializeHolderAgent = async () => {
const genesisTransactionsBCovrinTestNet = await getGenesisTransaction('http://test.bcovrin.vonx.io/genesis')
// Simple agent configuration. This sets some basic fields like the wallet
// configuration and the label. It also sets the mediator invitation url,
// because this is most likely required in a mobile environment.
const config: InitConfig = {
label: 'demo-agent-holder',
walletConfig: {
id: 'demo-agent-holder',
key: 'demoagentholder00000000000000000',
},
indyLedgers: [
{
id: 'bcovrin-test-net',
isProduction: false,
indyNamespace: 'bcovrin:test',
genesisTransactions: genesisTransactionsBCovrinTestNet,
},
],
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
autoAcceptConnections: true,
endpoints: ['http://localhost:3002'],
}

// A new instance of an agent is created here
const agent = new Agent({ config, dependencies: agentDependencies })

// Register a simple `WebSocket` outbound transport
agent.registerOutboundTransport(new WsOutboundTransport())

// Register a simple `Http` outbound transport
agent.registerOutboundTransport(new HttpOutboundTransport())

// Register a simple `Http` inbound transport
agent.registerInboundTransport(new HttpInboundTransport({ port: 3002 }))

// Initialize the agent
await agent.initialize()

return agent
}

const initializeIssuerAgent = async () => {
const genesisTransactionsBCovrinTestNet = await getGenesisTransaction('http://test.bcovrin.vonx.io/genesis')
// Simple agent configuration. This sets some basic fields like the wallet
// configuration and the label.
const config: InitConfig = {
label: 'demo-agent-issuer',
walletConfig: {
id: 'demo-agent-issuer',
key: 'demoagentissuer00000000000000000',
},
publicDidSeed: 'demoissuerdidseed000000000000000',
indyLedgers: [
{
id: 'bcovrin-test-net',
isProduction: false,
indyNamespace: 'bcovrin:test',
genesisTransactions: genesisTransactionsBCovrinTestNet,
},
],
autoAcceptCredentials: AutoAcceptCredential.ContentApproved,
autoAcceptConnections: true,
endpoints: ['http://localhost:3001'],
}

// A new instance of an agent is created here
const agent = new Agent({ config, dependencies: agentDependencies })

// Register a simple `WebSocket` outbound transport
agent.registerOutboundTransport(new WsOutboundTransport())

// Register a simple `Http` outbound transport
agent.registerOutboundTransport(new HttpOutboundTransport())

// Register a simple `Http` inbound transport
agent.registerInboundTransport(new HttpInboundTransport({ port: 3001 }))

// Initialize the agent
await agent.initialize()

return agent
}

const registerSchema = async (issuer: Agent) =>
issuer.ledger.registerSchema({ attributes: ['name', 'age'], name: 'Identity', version: '1.0' })

const registerCredentialDefinition = async (issuer: Agent, schema: Schema) =>
issuer.ledger.registerCredentialDefinition({ schema, supportRevocation: false, tag: 'default' })

const setupCredentialListener = (holder: Agent) => {
holder.events.on<CredentialStateChangedEvent>(CredentialEventTypes.CredentialStateChanged, async ({ payload }) => {
switch (payload.credentialRecord.state) {
case CredentialState.OfferReceived:
console.log('received a credential')
// custom logic here
await holder.credentials.acceptOffer({ credentialRecordId: payload.credentialRecord.id })
case CredentialState.Done:
console.log(`Credential for credential id ${payload.credentialRecord.id} is accepted`)
// For demo purposes we exit the program here.
process.exit(0)
}
})
}

const issueCredential = async (issuer: Agent, credentialDefinitionId: string, connectionId: string) =>
issuer.credentials.offerCredential({
protocolVersion: 'v1',
connectionId,
credentialFormats: {
indy: {
credentialDefinitionId,
attributes: [
{ name: 'name', value: 'Jane Doe' },
{ name: 'age', value: '23' },
],
},
},
})

const createNewInvitation = async (issuer: Agent) => {
const outOfBandRecord = await issuer.oob.createInvitation()

return {
invitationUrl: outOfBandRecord.outOfBandInvitation.toUrl({ domain: 'https://example.org' }),
outOfBandRecord,
}
}

const receiveInvitation = async (holder: Agent, invitationUrl: string) => {
const { outOfBandRecord } = await holder.oob.receiveInvitationFromUrl(invitationUrl)

return outOfBandRecord
}

const setupConnectionListener = (
issuer: Agent,
outOfBandRecord: OutOfBandRecord,
cb: (...args: any) => Promise<unknown>
) => {
issuer.events.on<ConnectionStateChangedEvent>(ConnectionEventTypes.ConnectionStateChanged, async ({ payload }) => {
if (payload.connectionRecord.outOfBandId !== outOfBandRecord.id) return
if (payload.connectionRecord.state === DidExchangeState.Completed) {
// the connection is now ready for usage in other protocols!
console.log(`Connection for out-of-band id ${outOfBandRecord.id} completed`)

// Custom business logic can be included here
// In this example we can send a basic message to the connection, but
// anything is possible
await cb(payload.connectionRecord.id)
}
})
}

const flow = (issuer: Agent) => async (connectionId: string) => {
console.log('Registering the schema...')
const schema = await registerSchema(issuer)
console.log('Registering the credential definition...')
const credentialDefinition = await registerCredentialDefinition(issuer, schema)
console.log('Issuing the credential...')
await issueCredential(issuer, credentialDefinition.id, connectionId)
}

const run = async () => {
console.log('Initializing the holder...')
const holder = await initializeHolderAgent()
console.log('Initializing the issuer...')
const issuer = await initializeIssuerAgent()

console.log('Initializing the credential listener...')
setupCredentialListener(holder)

console.log('Initializing the connection...')
const { outOfBandRecord, invitationUrl } = await createNewInvitation(issuer)
setupConnectionListener(issuer, outOfBandRecord, flow(issuer))
await receiveInvitation(holder, invitationUrl)
}

void run()

Useful resources

Side notes

As of v0.3.0 you should keep in mind that:

  1. When when attempting to register a credential that already exists on the ledger but is not in your wallet, AFJ will throw an error (as opposed to returning the credential definition from the ledger in prior versions)
  2. Attempting to register a new credential definition that is already in the wallet in AFJ will return the stored definition without attempting to register it on the ledger.

These choices are intentional. In case 1, it is assumed that this workflow is a mistake. In case 2. it is assumed that having registered the credential on the ledger is implied.