Implement a Seamless Data Sharing Flow with Affinidi Iota Framework

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

After setting up the Affinidi Iota Framework Configuration and the presentation definitions to query the data from Affinidi Vault, enable requesting data on your website using the Affinidi TDK.

In this example, we will request the user’s Email Address from the Affinidi Vault using the Redirect mode to handle data-sharing requests. Follow the sample code below using NextJS to implement the Affinidi Iota Framework on your website.

Implement Backend Endpoints

To initialise the data-sharing flow of Affinidi Iota Framework and later parse the callback response from Affinidi Vault, we will implement two backend endpoints: one for initialising the data-sharing flow to generate a signed request token and one for parsing the callback response from Affinidi Vault after the user consents to share their data.

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
  1. Import the libraries required 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

Generate a Signed Request Token

To sign the request token and initiate the Affinidi Vault request, implement a backend service that exposes an API endpoint and returns a signed request token from the Affinidi Iota Framework service. You may use frameworks like Express and NextJS to implement the backend.

Let’s assume the API endpoint is {BACKEND_SERVICE_URL}/api/iota/init-share, which the client side will later call using a POST method.

  1. Generate authorisation token and call the initiateDataSharingRequest method from Iota client of Affinidi TDK.
// NOTE: set your variables for PAT
const privateKey = '<PAT_PRIVATE_KEY_STRING>'
const passphrase = '<PAT_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)

In this method, it requires the following parameters to be able to initiate the data-sharing request:

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

  • mode is the data-sharing flow mode you would like to use to handle the request; in this case, we are using Redirect mode.

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

  • correlationId which is the UUID value that you can generate using UUID libraries. This value needs to be random on every request.

  • nonce is a random string value that will be used to validate the response later.

  • redirectUri is one of the redirect URLs configured on the Affinidi Iota Framework configuration. It should match exactly the value specified in the Redirect URL.

The front end will use the signed request token to generate the share link and automatically redirect the user to the Affinidi Vault to request the data. The signed request contains the Presentation Definition to query the data from Affinidi Vault and request user consent.

Fetch Callback Response

After the user is redirected to the specified Redirect URL from the consent screen of the Affinidi Vault, we have to parse the callback response. To do this, we will implement a backend endpoint that parses the callback sent to the Affinidi Iota Framework.

Let’s assume the API endpoint is {BACKEND_SERVICE_URL}/api/iota/iota-response, which the client side will later call using a POST method.

// NOTE: set your variables for PAT
const privateKey = '<PAT_PRIVATE_KEY_STRING>'
const passphrase = '<PAT_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)

In this method, it requires the following parameters to parse the callback response from Affinidi Vault:

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

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

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

  • responseCode is the code returned when the user is redirected back to the website with the specified Redirect URL.

The front end will call this endpoint after the user is redirected to the specified Redirect URL to parse the callback response from Affinidi Vault after the user consents to share their data or when they decline the request.

The endpoint will return the nonce value that the front end can validate if it matches the value it generated when it initialised the data-sharing flow and the JSON data that contains either the Verifiable Presentation token (VP token) data shared by the user or an error message.

Implement Frontend Pages

To enable the data-sharing flow on your website, implement a page that initialises the flow and another page to which the user is redirected after consenting to share their data from Affinidi Vault.

Install and Import Frontend Dependencies

  1. Install the common and uuid modules in the frontend. We use the uuid library to generate the nonce required to initiate redirect flow.
npm install -S @affinidi-tdk/common uuid
  1. Import the libraries required to generate nonce and initialise data-sharing flow.
import { VaultUtils } from '@affinidi-tdk/common';
import { v4 as uuidv4 } from "uuid";

Initiate Data Sharing Request

Create an async function that triggers the Affinidi Iota Framework data-sharing flow and gets the signed request token through an API call from the backend API created from the previous steps to redirect the user to the Affinidi Vault.

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)
}

In the above code, we are storing the response from the init-share endpoint in the browser’s local storage to use later to fetch the callback response from Affinidi Vault.

After that, attach the async function to a button that users can click to trigger the data-sharing flow and share their data based on your query, or it can be automatically triggered upon loading the page.

<Button
  onClick={() => handleRedirectFlowShare(selectedQuery)}
>
  Share
</Button>

After successfully initiating the data-sharing flow and generating the Request Token required for the Affinidi Vault, the website redirects the user to the Affinidi Vault consent screen.

Fetch Data after Redirect

Once the user consents to allow access to the requested data, the Affinidi Vault redirects the user back to the specified Redirect URL. In this Redirect URL, 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 consent to share their data, the sample Verifiable Presentation token (VP token) response will be in JSON format and contain 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 returned by the Redirect flow to extract the requested data from the Affinidi Vault shared by the user. The verifiableCredential can have multiple entries if more than one credentials are requested and shared by the user.

If the user decline to share their data, an access_denied error is returned.

Learn more about the Affinidi Iota Framework and how the integration works using the sample applications for WebSocket or Redirect flow implementation.