Verified Credentials Integration

Decide ID Integration Guide

Overview

Decide ID provides proof of personhood verification for applications built on the Internet Computer Protocol (ICP). By leveraging ICP's Internet Identity verifiable credentials system, applications can verify that their users are unique individuals through Decide ID's verification service.

Prerequisites

  • Your application must be built on the Internet Computer Protocol

  • Install the DFINITY Verifiable Credentials SDK:

npm install @dfinity/verifiable-credentials

Integration Steps

1. Import Required Dependencies

import { requestVerifiablePresentation, VerifiablePresentationResponse } from "@dfinity/verifiable-credentials/request-verifiable-presentation";
import { Principal } from "@dfinity/principal";

2. Request Verification

To initiate the verification process, call requestVerifiablePresentation with the following configuration:

const requestVerification = async (verifyPrincipal: Principal): Promise<void> => {
  try {
    const jwt: string = await new Promise((resolve, reject) => {
      requestVerifiablePresentation({
        onSuccess: async (verifiablePresentation: VerifiablePresentationResponse) => {
          if ('Ok' in verifiablePresentation) {
            resolve(verifiablePresentation.Ok);
          } else {
            reject(new Error(verifiablePresentation.Err));
          }
        },
        onError(err) {
          reject(new Error(err));
        },
        issuerData: {
          origin: 'https://id.decideai.xyz',
          canisterId: Principal.fromText('qgxyr-pyaaa-aaaah-qdcwq-cai'),
        },
        credentialData: {
          credentialSpec: {
            credentialType: 'ProofOfUniqueness',
            arguments: {
              // Specify the minimum date when the user's Decide ID verification must have occurred
              // Format: ISO 8601 timestamp
              minimumVerificationDate: "2024-12-01T00:00:00Z",
            },
          },
          credentialSubject: verifyPrincipal,
        },
        identityProvider: new URL('https://identity.ic0.app/'),
        derivationOrigin: window.location.origin,
      });
    });

    // Verify the JWT credentials received
    await verifyCredentials(jwt);

  } catch (error) {
    console.error('Verification failed:', error);
    throw error;
  }
};

2. Frontend: Request Verification

The frontend code initiates the verification process and sends the received JWT to your backend canister:

const requestVerification = async (verifyPrincipal: Principal): Promise<void> => {
  try {
    const jwt: string = await new Promise((resolve, reject) => {
      requestVerifiablePresentation({
        onSuccess: async (verifiablePresentation: VerifiablePresentationResponse) => {
          if ('Ok' in verifiablePresentation) {
            resolve(verifiablePresentation.Ok);
          } else {
            reject(new Error(verifiablePresentation.Err));
          }
        },
        onError(err) {
          reject(new Error(err));
        },
        issuerData: {
          origin: 'https://id.decideai.xyz',
          canisterId: Principal.fromText('qgxyr-pyaaa-aaaah-qdcwq-cai'),
        },
        credentialData: {
          credentialSpec: {
            credentialType: 'ProofOfUniqueness',
            arguments: {
              minimumVerificationDate: "2024-12-10T00:00:00Z",
            },
          },
          credentialSubject: verifyPrincipal,
        },
        identityProvider: new URL('https://identity.ic0.app/'),
        derivationOrigin: window.location.origin,
      });
    });

    // Send JWT to your backend canister for verification
    await backendCanister.verifyCredential(jwt);

  } catch (error) {
    console.error('Verification failed:', error);
    throw error;
  }
};

3. Backend: Verify the Credentials

Verification should be performed in your backend canister. Here are examples in both Rust and Motoko:

Rust Canister Example

#[update]
async fn verify_credential(jwt: String) -> Result<(), String> {
    // Decode and verify JWT signature using IC crypto primitives
    let decoded = decode_and_verify_jwt(&jwt)?;
    
    // Extract and verify the credentials
    let credentials = decoded.get_verifiable_credentials()?;
    
    // Verify we have exactly two credentials (ID alias and PoH)
    if credentials.len() != 2 {
        return Err("Invalid credential structure".to_string());
    }

    // Verify the proof of uniqueness credential
    let poh_credential = &credentials[1];
    
    // Verify credential type
    if !poh_credential.credential_type.contains("ProofOfUniqueness") {
        return Err("Invalid credential type".to_string());
    }

    // Verify issuer is Decide ID canister
    if poh_credential.issuer != Principal::from_text("qgxyr-pyaaa-aaaah-qdcwq-cai")? {
        return Err("Invalid issuer".to_string());
    }

    // Verify the minimum verification date matches exactly what we requested
    let received_date = poh_credential.get_minimum_verification_date()?;
    let requested_date = "2024-12-10T00:00:00Z";
    
    if received_date != requested_date {
        return Err("Minimum verification date does not match requested date".to_string());
    }

    // Store the verified state
    self.verified_users.insert(caller(), true);
    Ok(())
}

Motoko Canister Example

public shared(msg) func verifyCredential(jwt : Text) : async Result<(), Text> {
    // Decode and verify JWT signature using IC crypto primitives
    let decoded = try {
        DecodeJWT.decode(jwt);
    } catch (e) {
        return #err("Invalid JWT");
    };
    
    // Extract and verify the credentials
    let credentials = decoded.verifiableCredentials;
    
    // Verify we have exactly two credentials
    if (credentials.size() != 2) {
        return #err("Invalid credential structure");
    };

    // Verify the proof of uniqueness credential
    let pohCredential = credentials[1];
    
    // Verify credential type
    if (not Array.find(pohCredential.types, func(t) { t == "ProofOfUniqueness" })) {
        return #err("Invalid credential type");
    };

    // Verify issuer is Decide ID canister
    if (pohCredential.issuer != Principal.fromText("qgxyr-pyaaa-aaaah-qdcwq-cai")) {
        return #err("Invalid issuer");
    };

    // Verify the minimum verification date matches exactly what we requested
    let receivedDate = pohCredential.getMinimumVerificationDate();
    let requestedDate = "2024-12-10T00:00:00Z";
    
    if (receivedDate != requestedDate) {
        return #err("Minimum verification date does not match requested date");
    };

    // Store the verified state
    verifiedUsers.put(msg.caller, true);
    #ok()
};

These backend verification steps are crucial for security as they:

  1. Cannot be tampered with by end users

  2. Use secure IC cryptographic primitives

  3. Can maintain authoritative state about verified users

  4. Can integrate with your canister's other security measures

⚠️ Important: Never rely on client-side verification alone. Always verify credentials in your backend canister.


## Error Handling
The verification process can return several types of errors:

```typescript
type VerificationError = {
  version: string;
  code: 'UNKNOWN' | 'TIMEOUT' | 'USER_ABORT' | 'INVALID_CREDENTIALS';
};

// Handle errors appropriately in your application
const handleVerificationError = (error: VerificationError) => {
  switch (error.code) {
    case 'TIMEOUT':
      // Handle timeout
      break;
    case 'USER_ABORT':
      // Handle user cancellation
      break;
    case 'INVALID_CREDENTIALS':
      // Handle invalid credentials
      break;
    default:
      // Handle unknown errors
      break;
  }
};

Verification Date Requirements

The minimumVerificationDate parameter is a critical security feature that helps ensure the authenticity of user verifications. When specified, Decide ID will only accept verifications that were performed on or after the given date. This parameter is a measure that can be used to deter farmed accounts from accessing your platform as they will continuously need to ensure their accounts are reverified to comply with the minimum verification date.

Usage

minimumVerificationDate: "2024-01-01T00:00:00Z" // ISO 8601 format

Security Note

The minimumVerificationDate parameter in the credential request must be verified exactly in your backend canister. This ensures that:

  • The date hasn't been modified from what your application requested

  • Prevents tampering with the verification requirements

  • Maintains consistent security standards across your application

When verifying, don't just check if the date is recent enough - verify it matches exactly what you requested.

Configuration Tips

  • Set the date based on your application's security requirements

  • Consider updating the minimum date periodically

  • Use recent dates for higher-security applications

  • Balance security needs with user convenience

Best Practices

  1. Error Handling: Implement comprehensive error handling for various failure scenarios.

  2. User Experience: Provide clear feedback to users during the verification process.

  3. Security: Always verify both the cryptographic signatures and semantic content of received credentials.

  4. Caching: Consider caching verification results (with appropriate expiration) to improve user experience.

Security Considerations

  • Always verify the issuer's canister ID matches Decide ID's official canister

  • Implement proper session management for verified users

  • Never skip credential verification steps

  • Use secure storage for verification results

  • Implement rate limiting for verification requests

Additional Resources

Last updated