Implement a Consent-driven Data Sharing Flow

Enable your website to securely request and receive user data using Affinidi Iota Framework.

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.

Implement Backend Endpoints

To support the data-sharing flow, implement two backend endpoints:

  1. One to generate a signed request token.
  2. One to parse the callback response after user consent.

Install and Import Backend Dependencies

  1. Install the auth-provider and iota-client modules in the backend.
npm install -S @affinidi-tdk/auth-provider @affinidi-tdk/iota-client uuid
pip install affinidi_tdk_auth_provider affinidi_tdk_iota_client uuid
composer 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:resolve
dotnet add package AffinidiTdk.AuthProvider
dotnet add package AffinidiTdk.IotaClient
  1. 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 uuid
require_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.

// 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:

  • configurationId The ID of the Affinidi Iota Framework configuration you created previously.

  • mode The data-sharing flow mode you would like to use to handle the request; in this case use Redirect mode.

  • queryId The Presentation Definition configured on the Affinidi Iota Framework configuration to query data from Affinidi Vault.

  • correlationId A unique UUID. Generate this using a UUID library. This value needs to be random on every request.

  • nonce A random string used to validate the response.

  • redirectUri One 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:

  • configurationId The ID of the Affinidi Iota Framework configuration you created previously.

  • correlationId The ID returned by the initiateDataSharingRequest method when we generate the signed request token.

  • transactionId Also returned by the initiateDataSharingRequest method when we generate the signed request token.

  • responseCode Returned 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

  1. Install the common and uuid modules in the frontend. The uuid library is used to generate the nonce required to initiate the redirect flow.
npm install -S @affinidi-tdk/common uuid
  1. 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

  Get the consent logs stored on your project from your application

  Request additional data from your user using Presentation Definition (PEX Query)