Skip to main content

Overview

VCVerifier provides a login page with QR code display to simplify frontend integration for human-to-machine (H2M) authentication flows. This approach allows users to scan a QR code with their wallet app to authenticate using Verifiable Credentials.

How It Works

The frontend integration flow follows these steps:
1

User Initiates Login

Your frontend application redirects the user to the VCVerifier login page at /api/v1/loginQR
2

QR Code Display

VCVerifier displays a QR code containing the SIOP-2/OIDC4VP connection string with authentication parameters
3

Wallet Interaction

The user scans the QR code with their wallet application and approves the credential presentation
4

Credential Verification

VCVerifier verifies the presented credential and creates a signed JWT
5

Callback to Frontend

VCVerifier redirects to your client_callback URL with state and code parameters
6

Token Exchange

Your frontend exchanges the authorization code for the JWT via the /token endpoint

Integration Guide

Step 1: Redirect to Login Page

Direct users to the VCVerifier login endpoint with the required parameters:
const state = crypto.randomUUID();
const callbackUrl = encodeURIComponent('https://my-portal.com/auth_callback');

// Redirect user to VCVerifier login page
window.location.href = `https://verifier.example.com/api/v1/loginQR?state=${state}&client_callback=${callbackUrl}`;

Step 2: Handle the Callback

After successful authentication, VCVerifier redirects to your client_callback URL with query parameters:
// Example callback handler at /auth_callback
const urlParams = new URLSearchParams(window.location.search);
const state = urlParams.get('state');
const code = urlParams.get('code');

if (code && state) {
  // Exchange authorization code for JWT
  exchangeCodeForToken(code, state);
}
The callback URL will receive:
  • state: The original state parameter you provided
  • code: Authorization code to exchange for the JWT

Step 3: Exchange Code for JWT

Use the authorization code to retrieve the signed JWT from the token endpoint:
async function exchangeCodeForToken(code, state) {
  const params = new URLSearchParams({
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: 'https://my-portal.com/auth_callback'
  });

  const response = await fetch('https://verifier.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Accept': 'application/json'
    },
    body: params
  });

  const data = await response.json();
  const accessToken = data.access_token;
  
  // Store token and use for subsequent requests
  localStorage.setItem('access_token', accessToken);
  
  return accessToken;
}

Step 4: Use the JWT

The JWT contains the verified credential information and can be used for authorization:
Response
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IldPSEZ1NEhaNTlTTTg1M0M3ZU4wT3ZsS0dyTWVlckRDcEhPVVJvVFF3SHciLCJ0eXAiOiJKV1QifQ..."
}
Use the access token in subsequent API requests:
fetch('https://api.example.com/protected-resource', {
  headers: {
    'Authorization': `Bearer ${accessToken}`
  }
});

Configuration Options

Required Parameters

state
string
required
Unique identifier for tracking the authentication session. Used to match callbacks to original requests.
client_callback
string
required
URL where VCVerifier will redirect after successful authentication. Must be URL-encoded.

Optional Parameters

client_id
string
Identifier for your service. Used to retrieve specific scope and trust configurations.
nonce
string
Random value to prevent replay attacks. Generated by VCVerifier if not provided.
request_mode
string
How authentication requests are transmitted. Options: urlEncoded (default), byValue, byReference.

Customizing the Login Page

The login page template can be customized to match your brand:

Template Configuration

Configure the template directory in your server.yaml:
server:
  templateDir: "views/"
  staticDir: "views/static/"

Custom Template

Create a custom template at views/verifier_present_qr.html:
<!DOCTYPE html>
<html>
<head>
  <title>Login with Verifiable Credentials</title>
  <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
  <div class="login-container">
    <h1>Scan to Login</h1>
    <p>Use your wallet app to scan the QR code</p>
    
    <!-- QR Code Image -->
    <img src="data:{{.qrcode}}" alt="Login QR Code" class="qr-code">
    
    <p class="help-text">
      Don't have a wallet? 
      <a href="/download-wallet">Get one here</a>
    </p>
  </div>
</body>
</html>
The QR code must be included using <img src="data:{{.qrcode}}">. Static assets can be placed in the staticDir and accessed at /static/.

Complete Example

Here’s a complete React example integrating VCVerifier authentication:
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';

function LoginPage() {
  const navigate = useNavigate();
  const verifierUrl = process.env.REACT_APP_VERIFIER_URL;
  
  const handleLogin = () => {
    const state = crypto.randomUUID();
    sessionStorage.setItem('auth_state', state);
    
    const callbackUrl = encodeURIComponent(
      `${window.location.origin}/auth/callback`
    );
    
    window.location.href = 
      `${verifierUrl}/api/v1/loginQR?state=${state}&client_callback=${callbackUrl}`;
  };
  
  return (
    <div className="login-page">
      <h1>Welcome</h1>
      <button onClick={handleLogin}>
        Login with Verifiable Credentials
      </button>
    </div>
  );
}

function AuthCallback() {
  const navigate = useNavigate();
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const handleCallback = async () => {
      const params = new URLSearchParams(window.location.search);
      const code = params.get('code');
      const state = params.get('state');
      const storedState = sessionStorage.getItem('auth_state');
      
      // Verify state matches
      if (state !== storedState) {
        setError('Invalid state parameter');
        return;
      }
      
      try {
        // Exchange code for token
        const response = await fetch(`${process.env.REACT_APP_VERIFIER_URL}/token`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
          },
          body: new URLSearchParams({
            grant_type: 'authorization_code',
            code: code,
            redirect_uri: `${window.location.origin}/auth/callback`
          })
        });
        
        const data = await response.json();
        
        // Store token
        localStorage.setItem('access_token', data.access_token);
        sessionStorage.removeItem('auth_state');
        
        // Redirect to dashboard
        navigate('/dashboard');
      } catch (err) {
        setError('Failed to exchange authorization code');
      }
    };
    
    handleCallback();
  }, [navigate]);
  
  if (error) {
    return <div>Error: {error}</div>;
  }
  
  return <div>Authenticating...</div>;
}

export { LoginPage, AuthCallback };

Security Considerations

Always validate the state parameter in your callback to prevent CSRF attacks. Store the original state value and compare it with the value received in the callback.

Best Practices

  1. Use HTTPS: Always use HTTPS for production deployments to protect tokens in transit
  2. Validate State: Compare the returned state with your original value
  3. Short-Lived Tokens: Configure appropriate token expiry (default: 3600 seconds)
  4. Secure Storage: Store tokens securely (httpOnly cookies or secure storage)
  5. Token Refresh: Implement token refresh logic before expiry

Next Steps

Same-Device Flow

Learn about same-device authentication for mobile wallets

Cross-Device Flow

Understand the QR code cross-device authentication flow

Request Modes

Configure different request modes for various wallet types

API Reference

View detailed API specifications

Build docs developers (and LLMs) love