Issue a credential
Using AnonCreds and the Issue Credential V2 Protocol or the Issue Credential V1 Protocol.
This section assumes the following items:
A valid environment for development
Basic knowledge of the required fields in the agent config
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:
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
- npm
yarn global add ngrok
ngrok http <PORT>
npm install --global ngrok
ngrok http <PORT>
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.
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
.
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
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:
- 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)
- 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.