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
Initiate Authentication
Application calls the /api/v1/samedevice endpoint with a unique state parameter
Receive Redirect
VCVerifier responds with a 302 redirect containing the authentication parameters
Open Wallet
The redirect location uses a deep link (e.g., openid4vp://) that opens the wallet application
User Approves
User reviews and approves the credential presentation in their wallet
Wallet Responds
Wallet posts the credential to the /api/v1/authentication_response endpoint
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
JavaScript
Python
Swift (iOS)
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:
Set to vp_token to request a Verifiable Presentation
Set to direct_post - wallet will POST the response directly to the verifier
The DID of the verifier acting as the client
The endpoint where the wallet should POST the authentication response
Unique session identifier
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
Unique identifier for the authentication session
Service identifier for retrieving specific configurations
Credential scope to request (e.g., “PacketDeliveryService”)
Request encoding mode: urlEncoded, byValue, or byReference
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 ;
Deep Link Configuration
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 >
< intent-filter >
< action android:name = "android.intent.action.VIEW" />
< category android:name = "android.intent.category.DEFAULT" />
< category android:name = "android.intent.category.BROWSABLE" />
< data android:scheme = "myapp" android:host = "auth" />
< data android:scheme = "openid4vp" />
</ intent-filter >
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