Implement a Consent-driven Data Sharing Flow
After setting up the Affinidi Iota Framework configuration, including Presentation Definitions to query data from Affinidi Vault, integrate the data-sharing flow with Affinidi TDK.
In this example, we request the user’s email address from Affinidi Vault using Redirect mode.
Important Note on Data Sharing Mode
If you have selected the WebSocket mode to handle data-sharing request, the user’s Decentralised Identifier (DID) is a required parameter to generate the Iota Credentials to sign the request token.
To get the DID:
- Implement Affinidi Login
- Extract the DID from the ID Token.
Explore Affinidi Labs for integration examples in different languages and frameworks.
Implement Backend Endpoints
To support the data-sharing flow, implement two backend endpoints:
- One to generate a signed request token.
- One to parse the callback response after user consent.
Install and Import Backend Dependencies
- Install the
auth-providerandiota-clientmodules in the backend.
npm install -S @affinidi-tdk/auth-provider @affinidi-tdk/iota-client uuidpip install affinidi_tdk_auth_provider affinidi_tdk_iota_client uuidcomposer require affinidi-tdk/affinidi-tdk-php
composer require ramsey/uuid// specify the dependencies in pom.xml file
<dependency>
<groupId>com.affinidi.tdk</groupId>
<artifactId>auth.provider</artifactId>
<version><version_number></version>
</dependency>
<dependency>
<groupId>com.affinidi.tdk</groupId>
<artifactId></artifactId>
<version><version_number></version>
</dependency>
// run command
mvn dependency:resolvedotnet add package AffinidiTdk.AuthProvider
dotnet add package AffinidiTdk.IotaClient- Import the required libraries to generate the Iota Credentials.
import { IotaApi, Configuration, InitiateDataSharingRequestOKData, IotaConfigurationDtoModeEnum, FetchIOTAVPResponseOK } from "@affinidi-tdk/iota-client";
import { AuthProvider } from '@affinidi-tdk/auth-provider';
import { v4 as uuidv4 } from "uuid";import affinidi_tdk_iota_client
import affinidi_tdk_auth_provider
import json
import uuidrequire_once 'vendor/autoload.php';
use AffinidiTdk\AuthProvider\AuthProvider;
use AffinidiTdk\Clients\IotaClient;
use Ramsey\Uuid\Uuid;import com.affinidi.tdk.authProvider.AuthProvider;
import com.affinidi.tdk.iota.client.Configuration;
import com.affinidi.tdk.iota.client.apis.IotaApi;
import com.affinidi.tdk.iota.client.auth.ApiKeyAuth;
import com.affinidi.tdk.iota.client.models.InitiateDataSharingRequestInput;
import com.affinidi.tdk.iota.client.models.InitiateDataSharingRequestOK;
import com.affinidi.tdk.iota.client.models.FetchIOTAVPResponseInput;
import com.affinidi.tdk.iota.client.models.FetchIOTAVPResponseOK;
import com.affinidi.tdk.iota.client.ApiClient;using AffinidiTdk.AuthProvider;
using AffinidiTdk.IotaClient.Api;
using AffinidiTdk.IotaClient.Client;
using AffinidiTdk.IotaClient.Model;Generate a Signed Request Token
Create a backend service that exposes an API endpoint. This endpoint returns a signed request token from the Affinidi Iota Framework. You can use Express or NextJS to build this service.
Assume the API endpoint is
{BACKEND_SERVICE_URL}/api/iota/init-share. Your frontend will call this using a post method.
Step 1: Generate Authorisation Token
Call the initiateDataSharingRequest method from the Iota client in the Affinidi TDK.
Initialising Auth Provider
Use the Affinidi CLI Token command to generate Personal Access Token (PAT) required by the auth-provider module.
If you have created the Personal Access Token (PAT) using the
--key-idflag in Affinidi CLI, you must also set thekeyIdin the AuthProvider class using the provided value.
// NOTE: set your variables for PAT
const privateKey = '<PAT_PRIVATE_KEY_STRING>'
const passphrase = '<PAT_KEY_PAIR_PASSPHRASE>'
const tokenId = '<PAT_ID>'
const projectId = '<PROJECT_ID>'
const authProvider = new AuthProvider({
privateKey,
passphrase,
tokenId,
projectId
})
const api = new IotaApi(
new Configuration({
apiKey: authProvider.fetchProjectScopedToken.bind(authProvider),
})
);
const { data: dataSharingRequestResponse } =
await api.initiateDataSharingRequest({
configurationId,
mode: IotaConfigurationDtoModeEnum.Redirect,
queryId,
correlationId: uuidv4(),
nonce,
redirectUri,
});
const { correlationId, transactionId, jwt } =
dataSharingRequestResponse.data as InitiateDataSharingRequestOKData;
return { correlationId, transactionId, jwt };stats = {
tokenId,
passphrase,
privateKey,
projectId,
}
authProvider = affinidi_tdk_auth_provider.AuthProvider(stats)
projectScopedToken = authProvider.fetch_project_scoped_token()
configuration = affinidi_tdk_iota_client.Configuration()
# Configure API key authorization: ProjectTokenAuth
configuration.api_key['ProjectTokenAuth'] = projectScopedToken
with affinidi_tdk_iota_client.ApiClient(configuration) as api_client:
api_instance = affinidi_tdk_iota_client.IotaApi(api_client)
request_json = {
"configurationId": "<IOTA_CONFIGURATION_ID>",
"mode": "redirect",
"queryId": "<IOTA_CONFIGURATION_QUERY_ID>",
"correlationId": uuid.uuid4(),
"nonce": "<NONCE_FROM_FRONTEND>",
"redirectUri": "<IOTA_CONFIGURATION_REDIRECT_URL>",
}
initiate_data_sharing_request_input = affinidi_tdk_iota_client.InitiateDataSharingRequestInput.from_dict(request_json)
initiateDataSharingRequestResponse = affinidi_tdk_iota_client.initiate_data_sharing_request(initiate_data_sharing_request_input)
token_request_response = {
"correlationId": initiateDataSharingRequestResponse.data.correlationId,
"transactionId": initiateDataSharingRequestResponse.data.transactionId,
"jwt": initiateDataSharingRequestResponse.data.jwt
}
return json.dumps(token_request_response)$params = [
'privateKey' => "<PAT_PRIVATE_KEY_STRING>",
'passphrase' => '<PAT_KEY_PAIR_PASSPHRASE>',
'tokenId' => '<PAT_ID>',
'projectId' => '<PROJECT_ID>'
];
$authProvider = new AuthProvider($params);
$tokenCallback = [$authProvider, 'fetchProjectScopedToken'];
// Configure API key authorization: ProjectTokenAuth
$config = IotaClient\Configuration::getDefaultConfiguration()->setApiKey('authorization', '', $tokenCallback);
$apiInstance = new IotaClient\Api\IotaApi(
new GuzzleHttp\Client(),
$config
);
$uuid = Uuid::uuid4();
try {
$request = array(
"configurationId" => "<IOTA_CONFIGURATION_ID>",
"mode" => "redirect",
"queryId" => "<IOTA_CONFIGURATION_QUERY_ID>",
"correlationId" => $uuid->toString(),
"nonce" => "<NONCE_FROM_FRONTEND>",
"redirectUri" => "<IOTA_CONFIGURATION_REDIRECT_URL>",
);
$result = $apiInstance->initiateDataSharingRequest($request);
return $result;
} catch (Exception $e) {
echo 'Exception when calling method: ', $e->getMessage(), PHP_EOL;
}// NOTE: set your variables for PAT
AuthProvider authProvider = new AuthProvider.Configurations()
.projectId(dotenv.get("<PROJECT_ID>"))
.privateKey(dotenv.get("<PAT_PRIVATE_KEY_STRING>"))
.passphrase(dotenv.get("<PAT_KEY_PAIR_PASSPHRASE>"))
.tokenId(dotenv.get("<PAT_ID>"))
.build();
ApiClient defaultClient = Configuration.getDefaultApiClient();
ApiKeyAuth ProjectTokenAuth = (ApiKeyAuth) defaultClient.getAuthentication("ProjectTokenAuth");
ProjectTokenAuth.setApiKey(authProvider.fetchProjectScopedToken());
IotaApi apiInstance = new IotaApi(defaultClient);
InitiateDataSharingRequestInput request = new InitiateDataSharingRequestInput()
.configurationId("<IOTA_CONFIGURATION_ID>")
.mode(InitiateDataSharingRequestInput.ModeEnum.REDIRECT)
.queryId("<IOTA_CONFIGURATION_QUERY_ID>")
.correlationId("<uuid4_string>")
.nonce("<NONCE_FROM_FRONTEND>")
.redirectUri("<IOTA_CONFIGURATION_REDIRECT_URL>");
InitiateDataSharingRequestOK result = apiInstance.initiateDataSharingRequest(request);var authProvider = new AuthProvider(new AuthProviderParams
{
TokenId = "YOUR_TOKEN_ID",
PrivateKey = "YOUR_PRIVATE_KEY",
ProjectId = "YOUR_PROJECT_ID"
Passphrase = "YOUR_PASSPHRASE",
});
string projectScopedToken = await authProvider.FetchProjectScopedTokenAsync();
Configuration config = new Configuration();
config.AddApiKey("authorization", projectScopedToken);
IotaApi api = new IotaApi(config);
InitiateDataSharingRequestInput input = new InitiateDataSharingRequestInput();
InitiateDataSharingRequestOK result = api.InitiateDataSharingRequest(input);Step 2: Provide Required Parameters
To initiate the data-sharing request, pass the following parameters:
configurationIdThe ID of the Affinidi Iota Framework configuration you created previously.modeThe data-sharing flow mode you would like to use to handle the request; in this case use Redirect mode.queryIdThe Presentation Definition configured on the Affinidi Iota Framework configuration to query data from Affinidi Vault.correlationIdA unique UUID. Generate this using a UUID library. This value needs to be random on every request.nonceA random string used to validate the response.redirectUriOne of the redirect URLs configured in your Affinidi Iota Framework. This must match exactly the value specified in the Redirect URL.
Your frontend will use the signed token to generate a share link. This link redirects the user to Affinidi Vault to approve the data request. The token includes the Presentation Definition and requests user consent.
Fetch Callback Response
After the user is redirected to the specified Redirect URL, you must parse the callback response. To do this, we will implement a backend endpoint that parses the callback sent to the Affinidi Iota Framework.
Create a backend endpoint to handle this. Assume the endpoint is
{BACKEND_SERVICE_URL}/api/iota/iota-response. Your frontend will call this using a POST method.
// NOTE: set your variables for PAT
const privateKey = '<PAT_PRIVATE_KEY_STRING>'
const passphrase = '<PAT_KEY_PAIR_PASSPHRASE>'
const tokenId = '<PAT_ID>'
const projectId = '<PROJECT_ID>'
const authProvider = new AuthProvider({
privateKey,
passphrase,
tokenId,
projectId
})
const api = new IotaApi(
new Configuration({
apiKey: authProvider.fetchProjectScopedToken.bind(authProvider),
})
);
const iotaVpResponse: FetchIOTAVPResponseOK = await api.fetchIotaVpResponse({
configurationId: "<IOTA_CONFIGURATION_ID>",
correlationId: "<CORRELATION_ID_FROM_LOCAL_STORAGE>",
transactionId: "<TRANSACTION_ID_FROM_LOCAL_STORAGE>",
responseCode: "<RESPONSE_CODE_FROM_REDIRECT>",
});
const vp = JSON.parse((iotaVpResponse.data as any).vpToken);
return { vp: vp, nonce: iotaVpResponse.data.nonce };stats = {
tokenId,
passphrase,
privateKey,
projectId,
}
authProvider = affinidi_tdk_auth_provider.AuthProvider(stats)
projectScopedToken = authProvider.fetch_project_scoped_token()
configuration = affinidi_tdk_iota_client.Configuration()
# Configure API key authorization: ProjectTokenAuth
configuration.api_key['ProjectTokenAuth'] = projectScopedToken
with affinidi_tdk_iota_client.ApiClient(configuration) as api_client:
api_instance = affinidi_tdk_iota_client.IotaApi(api_client)
request_json = {
"configurationId": "<IOTA_CONFIGURATION_ID>",
"correlationId": "<CORRELATION_ID_FROM_LOCAL_STORAGE>",
"transactionId": "<TRANSACTION_ID_FROM_LOCAL_STORAGE>",
"responseCode": "<RESPONSE_CODE_FROM_REDIRECT>",
}
iotaVPResponse = affinidi_tdk_iota_client.fetch_iota_vp_response(request_json)
return json.dumps(iotaVPResponse)$params = [
'privateKey' => "<PAT_PRIVATE_KEY_STRING>",
'passphrase' => '<PAT_KEY_PAIR_PASSPHRASE>',
'tokenId' => '<PAT_ID>',
'projectId' => '<PROJECT_ID>'
];
$authProvider = new AuthProvider($params);
$tokenCallback = [$authProvider, 'fetchProjectScopedToken'];
// Configure API key authorization: ProjectTokenAuth
$config = IotaClient\Configuration::getDefaultConfiguration()->setApiKey('authorization', '', $tokenCallback);
$apiInstance = new IotaClient\Api\IotaApi(
new GuzzleHttp\Client(),
$config
);
try {
$request = array(
"configurationId" => "<IOTA_CONFIGURATION_ID>",
"correlationId" => "<CORRELATION_ID_FROM_LOCAL_STORAGE>",
"transactionId" => "<TRANSACTION_ID_FROM_LOCAL_STORAGE>",
"responseCode" => "<RESPONSE_CODE_FROM_REDIRECT>",
);
$result = $apiInstance->fetchIotaVpResponse($request);
return $result;
} catch (Exception $e) {
echo 'Exception when calling method: ', $e->getMessage(), PHP_EOL;
}// NOTE: set your variables for PAT
AuthProvider authProvider = new AuthProvider.Configurations()
.projectId(dotenv.get("<PROJECT_ID>"))
.privateKey(dotenv.get("<PAT_PRIVATE_KEY_STRING>"))
.passphrase(dotenv.get("<PAT_KEY_PAIR_PASSPHRASE>"))
.tokenId(dotenv.get("<PAT_ID>"))
.build();
ApiClient defaultClient = Configuration.getDefaultApiClient();
ApiKeyAuth ProjectTokenAuth = (ApiKeyAuth) defaultClient.getAuthentication("ProjectTokenAuth");
ProjectTokenAuth.setApiKey(authProvider.fetchProjectScopedToken());
IotaApi apiInstance = new IotaApi(defaultClient);
FetchIOTAVPResponseInput request = new FetchIOTAVPResponseInput()
.configurationId("<IOTA_CONFIGURATION_ID>")
.correlationId("<CORRELATION_ID_FROM_LOCAL_STORAGE>")
.transactionId("<TRANSACTION_ID_FROM_LOCAL_STORAGE>")
.responseCode("<RESPONSE_CODE_FROM_REDIRECT>");
FetchIOTAVPResponseOK result = apiInstance.fetchIotaVpResponse(request);var authProvider = new AuthProvider(new AuthProviderParams
{
TokenId = "YOUR_TOKEN_ID",
PrivateKey = "YOUR_PRIVATE_KEY",
ProjectId = "YOUR_PROJECT_ID"
Passphrase = "YOUR_PASSPHRASE",
});
string projectScopedToken = await authProvider.FetchProjectScopedTokenAsync();
Configuration config = new Configuration();
config.AddApiKey("authorization", projectScopedToken);
IotaApi api = new IotaApi(config);
FetchIOTAVPResponseInput input = new FetchIOTAVPResponseInput();
FetchIOTAVPResponseOK result = api.FetchIotaVpResponse(input);Required Parameters
To parse the callback response from Affinidi Vault, provide:
configurationIdThe ID of the Affinidi Iota Framework configuration you created previously.correlationIdThe ID returned by the initiateDataSharingRequest method when we generate the signed request token.transactionIdAlso returned by the initiateDataSharingRequest method when we generate the signed request token.responseCodeReturned when the user is redirected back to your site.
Your frontend calls this endpoint after redirection. It parses the callback response from Affinidi Vault, whether the user consents or declines.
The endpoint returns:
- The nonce value, which your frontend can validate.
- A JSON object containing either the Verifiable Presentation (VP) token or an error message.
Implement Frontend Pages
To complete the flow, implement:
- A page to initialise the data-sharing request.
- A page to handle the redirect after user consent.
Install and Import Frontend Dependencies
- Install the
commonanduuidmodules in the frontend. Theuuidlibrary is used to generate the nonce required to initiate the redirect flow.
npm install -S @affinidi-tdk/common uuid- Import the required libraries to generate the nonce and start the data-sharing flow.
import { VaultUtils } from '@affinidi-tdk/common';
import { v4 as uuidv4 } from "uuid";Initiate Data Sharing Request
Create an asynchronous function to trigger the Affinidi Iota Framework data-sharing flow.
This function should call your backend API to get a signed request token (see previous steps).
async function handleRedirectFlowShare(queryId: string) {
const nonce = uuidv4().slice(0, 10);
const response = await fetch("<BACKEND_SERVICE_URL>/api/iota/init-share", {
method: "POST",
body: JSON.stringify({
queryId: "<IOTA_CONFIGURATION_QUERY_ID>",
nonce: nonce,
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
});
const data = await response.json();
const toStore = {
nonce,
correlationId: data.correlationId,
transactionId: data.transactionId,
}
localStorage.setItem('iotaRedirect', JSON.stringify(toStore));
const vaultLink = VaultUtils.buildShareLink(data.jwt, 'client_id');
router.push(vaultLink)
}Store the response from the init-share endpoint in the browser’s local storage. You’ll use this later to fetch the callback response from Affinidi Vault.
Attach the function to a button that users can click to start the flow. Alternatively, trigger it automatically when the page loads.
<Button
onClick={() => handleRedirectFlowShare(selectedQuery)}
>
Share
</Button>Once the request token is generated, redirect the user to the Affinidi Vault consent screen.
Fetch Data after Redirect
After the user consents to share their data, Affinidi Vault redirects them to the specified Redirect URL.
At this point, we will invoke the iota-response endpoint to parse the callback from Affinidi Vault.
const response = await fetch("/api/iota/iota-response", {
method: "POST",
body: JSON.stringify(params),
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
jsonResponse = await response.json();If the user consents, the response will include a Verifiable Presentation (VP) token in JSON format. This token contains the requested data:
{
"correlationId": "ab0a1309-bec8-4c28-828f-8a1c7e8ce606",
"presentationSubmission": {
"descriptor_map": [
{
"id": "email_vc",
"path": "$.verifiableCredential[0]",
"format": "ldp_vc"
}
],
"id": "Ub2yNyMEFlW8ziKMKwVzk",
"definition_id": "token_with_email_vc"
},
"nonce": "6c877dd0-a844-4981-be15-aceed1d2fd5c",
"vpToken": {
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"type": [
"VerifiablePresentation"
],
"verifiableCredential": [
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://schema.affinidi.com/EmailV1-0.jsonld"
],
"id": "claimId:0ecab5796c9f160b",
"type": [
"VerifiableCredential",
"Email"
],
"holder": {
"id": "did:key:zQ3shq75QjFhLLeVCC5MATFdSQPrfkDwHoXBUPKGScHSdWi8p"
},
"credentialSubject": {
"email": "info@affinidi.com"
},
"credentialSchema": {
"id": "https://schema.affinidi.com/EmailV1-0.json",
"type": "JsonSchemaValidator2018"
},
"issuanceDate": "2024-07-29T05:25:57.982Z",
"issuer": "did:key:zQ3shXLA2cHanJgCUsDfXxBi2BGnMLArHVz5NWoC9axr8pEy7",
"proof": {
...
}
}
],
"holder": {
"id": "did:key:zQ3shq75QjFhLLeVCC5MATFdSQPrfkDwHoXBUPKGScHSdWi8p"
},
"id": "claimId:O81O9IXOs-uKQcwctMpEb",
"proof": {
...
}
}
}Parse the verifiableCredential field to extract the shared data returned by the Redirect flow. If multiple credentials were requested, the field may contain several entries.
If the user declines, the response will include an access_denied error.
Learn more about the Affinidi Iota Framework and how the integration works using the sample applications for WebSocket or Redirect flow implementation.
What’s Next
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.