Skip to main content

Overview

The same-device flow enables authentication when the user’s Verifiable Credential wallet is on the same device as the application requesting authentication. This is ideal for mobile applications or scenarios where the wallet is browser-based. Instead of displaying a QR code, the verifier creates a deep link that directly opens the user’s wallet application on the same device.

Use Cases

Mobile Apps

Native mobile applications where the wallet app is installed on the same phone

Browser Wallets

Browser extensions or web-based wallets on desktop

Progressive Web Apps

PWAs that can communicate with wallet applications

Desktop Applications

Desktop apps with locally installed credential wallets

How It Works

1

Initiate Authentication

Application calls the /api/v1/samedevice endpoint with a unique state parameter
2

Receive Redirect

VCVerifier responds with a 302 redirect containing the authentication parameters
3

Open Wallet

The redirect location uses a deep link (e.g., openid4vp://) that opens the wallet application
4

User Approves

User reviews and approves the credential presentation in their wallet
5

Wallet Responds

Wallet posts the credential to the /api/v1/authentication_response endpoint
6

Retrieve Token

Application exchanges the authorization code for a JWT via the /token endpoint

Implementation

Step 1: Start the Same-Device Flow

Call the samedevice endpoint to initiate authentication:
curl -X 'GET' \
  'http://localhost:8080/api/v1/samedevice?state=274e7465-cc9d-4cad-b75f-190db927e56a'

Step 2: Handle the Redirect

The response contains a 302 redirect with the authentication request:
HTTP/1.1 302 Found
Location: http://localhost:8080/?response_type=vp_token&response_mode=direct_post&client_id=did:key:z6MkigCEnopwujz8Ten2dzq91nvMjqbKQYcifuZhqBsEkH7g&redirect_uri=http://verifier-one.batterypass.fiware.dev/api/v1/authentication_response&state=OUBlw8wlCZZOcTwRN2wURA&nonce=wqtpm60Jwx1sYWITRRZwBw
The location header contains all necessary parameters for the SIOP-2/OIDC4VP flow:
response_type
string
Set to vp_token to request a Verifiable Presentation
response_mode
string
Set to direct_post - wallet will POST the response directly to the verifier
client_id
string
The DID of the verifier acting as the client
redirect_uri
string
The endpoint where the wallet should POST the authentication response
state
string
Unique session identifier
nonce
string
Random value to prevent replay attacks

Step 3: Submit Authentication Response

The wallet posts the Verifiable Presentation to the authentication_response endpoint:
curl -X 'POST' \
  'https://localhost:8080/api/v1/authentication_response?state=OUBlw8wlCZZOcTwRN2wURA' \
  -H 'accept: */*' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'presentation_submission=ewogICJpZCI6ICJzdHJpbmciLAogICJkZWZpbml0aW9uX2lkIjogIjMyZjU0MTYzLTcxNjYtNDhmMS05M2Q4LWZmMjE3YmRiMDY1MyIsCiAgImRlc2NyaXB0b3JfbWFwIjogWwogICAgewogICAgICAiaWQiOiAiaWRfY3JlZGVudGlhbCIsCiAgICAgICJmb3JtYXQiOiAibGRwX3ZjIiwKICAgICAgInBhdGgiOiAiJCIsCiAgICAgICJwYXRoX25lc3RlZCI6ICJzdHJpbmciCiAgICB9CiAgXQp9&vp_token=ewogICJAY29udGV4dCI6IFsKICAgICJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIKICBdLAogICJ0eXBlIjogWwogICAgIlZlcmlmaWFibGVQcmVzZW50YXRpb24iCiAgXSwKICAidmVyaWZpYWJsZUNyZWRlbnRpYWwiOiBbCiAgICB7CiAgICAgICJ0eXBlcyI6IFsKICAgICAgICAiUGFja2V0RGVsaXZlcnlTZXJ2aWNlIiwKICAgICAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiCiAgICAgIF0sCiAgICAgICJAY29udGV4dCI6IFsKICAgICAgICAiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLAogICAgICAgICJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3N1aXRlcy9qd3MtMjAyMC92MSIKICAgICAgXSwKICAgICAgImNyZWRlbnRpYWxzU3ViamVjdCI6IHt9LAogICAgICAiYWRkaXRpb25hbFByb3AxIjoge30KICAgIH0KICBdLAogICJpZCI6ICJlYmM2ZjFjMiIsCiAgImhvbGRlciI6IHsKICAgICJpZCI6ICJkaWQ6a2V5Ono2TWtzOW05aWZMd3kzSldxSDRjNTdFYkJRVlMyU3BSQ2pmYTc5d0hiNXZXTTZ2aCIKICB9LAogICJwcm9vZiI6IHsKICAgICJ0eXBlIjogIkpzb25XZWJTaWduYXR1cmUyMDIwIiwKICAgICJjcmVhdG9yIjogImRpZDprZXk6ejZNa3M5bTlpZkx3eTNKV3FINGM1N0ViQlFWUzJTcFJDamZhNzl3SGI1dldNNnZoIiwKICAgICJjcmVhdGVkIjogIjIwMjMtMDEtMDZUMDc6NTE6MzZaIiwKICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiZGlkOmtleTp6Nk1rczltOWlmTHd5M0pXcUg0YzU3RWJCUVZTMlNwUkNqZmE3OXdIYjV2V002dmgjejZNa3M5bTlpZkx3eTNKV3FINGM1N0ViQlFWUzJTcFJDamZhNzl3SGI1dldNNnZoIiwKICAgICJqd3MiOiAiZXlKaU5qUWlPbVpoYkhObExDSmpjbWwwSWpwYkltSTJOQ0pkTENKaGJHY2lPaUpGWkVSVFFTSjkuLjZ4U3FvWmphME53akYwYWY5WmtucXgzQ2JoOUdFTnVuQmY5Qzh1TDJ1bEdmd3VzM1VGTV9abmhQald0SFBsLTcyRTlwM0JUNWYycHRab1lrdE1LcERBIgogIH0KfQ'
The vp_token and presentation_submission must use Base64 URL-safe encoding, not standard Base64. Replace + with -, / with _, and remove = padding.

Step 4: Get Authorization Code

After successful verification, VCVerifier redirects with the authorization code:
HTTP/1.1 302 Found
Location: http://localhost:8080/?state=274e7465-cc9d-4cad-b75f-190db927e56a&code=IwMTgvY3JlZGVudGlhbHMv

Step 5: Exchange Code for JWT

Finally, exchange the authorization code for the JWT token:
curl -X 'POST' \
  'https://localhost:8080/token' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code&code=IwMTgvY3JlZGVudGlhbHMv&redirect_uri=https%3A%2F%2Flocalhost%3A8080%2F'
Response:
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6IldPSEZ1NEhaNTlTTTg1M0M3ZU4wT3ZsS0dyTWVlckRDcEhPVVJvVFF3SHciLCJ0eXAiOiJKV1QifQ..."
}

Configuration Options

Query Parameters

state
string
required
Unique identifier for the authentication session
client_id
string
Service identifier for retrieving specific configurations
scope
string
Credential scope to request (e.g., “PacketDeliveryService”)
request_mode
string
Request encoding mode: urlEncoded, byValue, or byReference
redirect_path
string
Alternative redirect path. If not provided, an oid4vp deep link is returned

Complete Mobile Example (React Native)

Here’s a complete React Native example implementing the same-device flow:
import React, { useState, useEffect } from 'react';
import { View, Button, Linking, Alert } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';

function SameDeviceAuth() {
  const [authState, setAuthState] = useState(null);
  const VERIFIER_URL = 'https://verifier.example.com';
  
  useEffect(() => {
    // Handle deep link when returning from wallet
    const handleDeepLink = async (event) => {
      const url = event.url;
      const params = new URLSearchParams(url.split('?')[1]);
      const code = params.get('code');
      const state = params.get('state');
      
      if (code && state === authState) {
        await exchangeCodeForToken(code);
      }
    };
    
    // Listen for deep links
    const subscription = Linking.addEventListener('url', handleDeepLink);
    
    return () => subscription.remove();
  }, [authState]);
  
  const startAuthentication = async () => {
    try {
      // Generate unique state
      const state = generateUUID();
      setAuthState(state);
      await AsyncStorage.setItem('auth_state', state);
      
      // Call samedevice endpoint
      const response = await fetch(
        `${VERIFIER_URL}/api/v1/samedevice?state=${state}`,
        { redirect: 'manual' }
      );
      
      // Get redirect location
      const redirectUrl = response.headers.get('location');
      
      // Open wallet app
      const canOpen = await Linking.canOpenURL(redirectUrl);
      if (canOpen) {
        await Linking.openURL(redirectUrl);
      } else {
        Alert.alert('Error', 'Cannot open wallet app');
      }
    } catch (error) {
      Alert.alert('Error', 'Failed to start authentication');
    }
  };
  
  const exchangeCodeForToken = async (code) => {
    try {
      const params = new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: 'myapp://auth/callback'
      });
      
      const response = await fetch(`${VERIFIER_URL}/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: params.toString()
      });
      
      const data = await response.json();
      
      // Store token
      await AsyncStorage.setItem('access_token', data.access_token);
      await AsyncStorage.removeItem('auth_state');
      
      Alert.alert('Success', 'Authentication successful!');
      
      // Navigate to home screen
      // navigation.navigate('Home');
    } catch (error) {
      Alert.alert('Error', 'Token exchange failed');
    }
  };
  
  const generateUUID = () => {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  };
  
  return (
    <View style={{ padding: 20 }}>
      <Button 
        title="Login with Wallet" 
        onPress={startAuthentication}
      />
    </View>
  );
}

export default SameDeviceAuth;
For mobile applications, configure deep link handling:
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
      <string>openid4vp</string>
    </array>
  </dict>
</array>

Comparison: Same-Device vs Cross-Device

When to use same-device:
  • Mobile app with wallet on same device
  • Browser wallet extensions
  • Streamlined mobile user experience
When to use cross-device:
  • Desktop application with mobile wallet
  • Better security separation
  • User prefers mobile wallet for authentication

Troubleshooting

Wallet Not Opening

Issue: The deep link doesn’t open the wallet application. Solutions:
  • Verify the wallet app is installed
  • Check deep link scheme is registered
  • Test with openid4vp:// scheme
  • Fall back to QR code if deep link fails

Redirect Loop

Issue: Application keeps redirecting without completing authentication. Solutions:
  • Ensure state parameter is properly tracked
  • Check redirect_uri matches your app’s scheme
  • Verify callback handling is implemented correctly

Base64 Encoding Errors

Issue: Authentication fails with encoding errors. Solutions:
  • Use Base64 URL-safe encoding (not standard Base64)
  • Replace + with -, / with _
  • Remove = padding characters
  • Test with online URL-safe Base64 encoder

Next Steps

Cross-Device Flow

Learn about QR code authentication for cross-device scenarios

Request Modes

Understand different request encoding modes

API Reference

View detailed API specifications

Frontend Integration

Complete frontend integration guide

Build docs developers (and LLMs) love