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:
User Initiates Login
Your frontend application redirects the user to the VCVerifier login page at /api/v1/loginQR
QR Code Display
VCVerifier displays a QR code containing the SIOP-2/OIDC4VP connection string with authentication parameters
Wallet Interaction
The user scans the QR code with their wallet application and approves the credential presentation
Credential Verification
VCVerifier verifies the presented credential and creates a signed JWT
Callback to Frontend
VCVerifier redirects to your client_callback URL with state and code parameters
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:
JavaScript (Fetch)
Python (Requests)
cURL
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:
{
"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
Unique identifier for tracking the authentication session. Used to match callbacks to original requests.
URL where VCVerifier will redirect after successful authentication. Must be URL-encoded.
Optional Parameters
Identifier for your service. Used to retrieve specific scope and trust configurations.
Random value to prevent replay attacks. Generated by VCVerifier if not provided.
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
Use HTTPS : Always use HTTPS for production deployments to protect tokens in transit
Validate State : Compare the returned state with your original value
Short-Lived Tokens : Configure appropriate token expiry (default: 3600 seconds)
Secure Storage : Store tokens securely (httpOnly cookies or secure storage)
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