Overview
The postMessage API provides secure, real-time cross-origin communication between your parent window and the embedded webview. This is the recommended method for handling sensitive authentication data.How It Works
1
Generate Nonce Token
Your backend calls the Doshi API to generate a nonce token
2
Webview Sends PING
The embedded webview sends a PING message to signal it’s ready
3
Parent Receives PING
Parent window listens for the PING and prepares authentication data
4
Parent Sends AUTH
Parent sends the nonce token and user data back to the webview
5
Webview Processes AUTH
Webview receives and processes the authentication, handling 2FA if needed
TypeScript Interfaces
Copy
interface AuthData {
token: string; // Required - nonce token from API
email?: string; // Optional - user's email
segment?: string; // Optional - for multiple paths
branchId?: string; // Optional - branch identifier
// 2FA Parameters (optional, required when is2FaEnabled is true)
is2FaEnabled?: boolean; // Enable 2FA flow in iframe
dob?: string; // Date of birth (YYYY-MM-DD)
organizationId?: string; // Organization ID
partnerUserId?: string; // Partner user ID
firstName?: string; // First name
lastName?: string; // Last name
}
interface WebviewMessage {
type: string;
[key: string]: any;
}
React Implementation
Complete Example with API Call
Copy
import React, { useEffect, useState } from "react";
interface AuthData {
token: string;
email?: string;
segment?: string;
branchId?: string;
is2FaEnabled?: boolean;
dob?: string;
organizationId?: string;
partnerUserId?: string;
firstName?: string;
lastName?: string;
}
interface WebviewMessage {
type: string;
[key: string]: any;
}
interface Props {
userEmail: string;
segment?: string;
branchId?: string;
// 2FA props
is2FaEnabled?: boolean;
dob?: string;
organizationId?: string;
partnerUserId?: string;
firstName?: string;
lastName?: string;
}
const WebviewComponent: React.FC<Props> = ({
userEmail,
segment,
branchId,
is2FaEnabled = false,
dob,
organizationId,
partnerUserId,
firstName,
lastName
}) => {
const [authToken, setAuthToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Step 1: Get nonce token from your backend
useEffect(() => {
async function fetchToken() {
try {
// Call YOUR backend which securely calls Doshi API
const response = await fetch('/api/generate-doshi-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: userEmail,
branchId
})
});
if (!response.ok) {
throw new Error('Failed to generate token');
}
const data = await response.json();
setAuthToken(data.token);
} catch (error) {
console.error('Failed to fetch auth token:', error);
setError('Authentication failed. Please try again.');
} finally {
setIsLoading(false);
}
}
fetchToken();
}, [userEmail, branchId]);
// Step 2: Setup postMessage listener
useEffect(() => {
if (!authToken) return;
const handleMessage = (event: MessageEvent) => {
// ✅ Verify origin in production
if (event.origin !== 'https://embed.doshi.app') {
console.warn('Rejected message from:', event.origin);
return;
}
try {
const data: WebviewMessage =
typeof event.data === "string" ? JSON.parse(event.data) : event.data;
// When we receive PING from webview, send the auth data
if (data.type === "PING") {
handleSendAuth();
}
} catch (error) {
console.error("Error processing message:", error);
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [authToken]);
const handleSendAuth = () => {
if (!authToken) return;
// Build auth data object
const authData: AuthData = {
token: authToken,
type: "AUTH"
};
// Add optional fields
if (userEmail) authData.email = userEmail;
if (segment) authData.segment = segment;
if (branchId) authData.branchId = branchId;
// Add 2FA fields if enabled
if (is2FaEnabled) {
authData.is2FaEnabled = true;
if (dob) authData.dob = dob;
if (organizationId) authData.organizationId = organizationId;
if (partnerUserId) authData.partnerUserId = partnerUserId;
if (firstName) authData.firstName = firstName;
if (lastName) authData.lastName = lastName;
}
// Send to iframe
const webview = document.querySelector("iframe");
if (webview && webview.contentWindow) {
webview.contentWindow.postMessage(
JSON.stringify(authData),
"https://embed.doshi.app" // ⚠️ Never use "*" in production
);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<div className="spinner mb-4"></div>
<p>Loading...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-center text-red-600">
<p>{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
>
Retry
</button>
</div>
</div>
);
}
if (!authToken) {
return <div>Failed to authenticate</div>;
}
return (
<div className="webview-container h-screen w-full">
<iframe
src="https://embed.doshi.app"
className="h-full w-full"
frameBorder="0"
allowFullScreen
/>
</div>
);
};
export default WebviewComponent;
Usage Example
Copy
import WebviewComponent from './WebviewComponent';
function App() {
return (
<div className="app">
<h1>My Application</h1>
<WebviewComponent
userEmail="[email protected]"
segment="premium"
branchId="branch_789"
/>
</div>
);
}
// With 2FA enabled
function AppWith2FA() {
return (
<div className="app">
<h1>My Application</h1>
<WebviewComponent
userEmail="[email protected]"
segment="premium"
branchId="branch_789"
is2FaEnabled={true}
dob="1990-01-15"
organizationId="org_123"
partnerUserId="partner_123"
firstName="John"
lastName="Doe"
/>
</div>
);
}
Vanilla JavaScript Implementation
Parent Window
Copy
// Configuration
const config = {
doshiEmbedUrl: 'https://embed.doshi.app',
backendUrl: '/api/generate-doshi-token'
};
// User data
const userData = {
email: "[email protected]",
segment: "premium",
branchId: "branch_789"
};
// 2FA data (optional)
const twoFactorData = {
is2FaEnabled: true,
dob: "1990-01-15",
organizationId: "org_123",
partnerUserId: "partner_123",
firstName: "John",
lastName: "Doe"
};
let authToken = null;
// Step 1: Get nonce token from your backend
async function fetchAuthToken() {
try {
const response = await fetch(config.backendUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: userData.email,
branchId: userData.branchId
})
});
if (!response.ok) {
throw new Error('Failed to generate token');
}
const data = await response.json();
authToken = data.token;
// Create iframe after token is ready
createWebview();
} catch (error) {
console.error('Failed to fetch auth token:', error);
showError('Authentication failed. Please try again.');
}
}
// Step 2: Create and add webview to the page
function createWebview() {
const webview = document.createElement("iframe");
webview.src = config.doshiEmbedUrl;
webview.className = "h-full w-full";
webview.frameBorder = "0";
webview.allowFullscreen = true;
document.querySelector("#doshi-container").appendChild(webview);
// Setup message listener
setupMessageListener();
}
// Step 3: Handle messages from webview
function setupMessageListener() {
window.addEventListener("message", handleMessage);
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
window.removeEventListener("message", handleMessage);
});
}
function handleMessage(event) {
// ✅ Verify origin in production
if (event.origin !== config.doshiEmbedUrl) {
console.warn('Rejected message from:', event.origin);
return;
}
try {
const data = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
// When we receive PING from webview, send the auth data
if (data.type === "PING") {
sendAuthData();
} else if (data.type === "AUTH_SUCCESS") {
console.log('Authentication successful');
onAuthSuccess(data);
} else if (data.type === "AUTH_ERROR") {
console.error('Authentication failed:', data.error);
onAuthError(data.error);
}
} catch (error) {
console.error("Error processing message:", error);
}
}
// Step 4: Send auth data to webview
function sendAuthData() {
if (!authToken) {
console.error('No auth token available');
return;
}
// Build auth data
const authData = {
token: authToken,
email: userData.email,
segment: userData.segment,
branchId: userData.branchId,
type: "AUTH"
};
// Add 2FA data if enabled
if (twoFactorData.is2FaEnabled) {
Object.assign(authData, twoFactorData);
}
// Send to webview
const webview = document.querySelector("iframe");
if (webview && webview.contentWindow) {
webview.contentWindow.postMessage(
JSON.stringify(authData),
config.doshiEmbedUrl // ⚠️ Never use "*" in production
);
}
}
// Callback functions
function onAuthSuccess(data) {
console.log('User authenticated:', data);
// Hide loading indicator, update UI, etc.
}
function onAuthError(error) {
console.error('Authentication error:', error);
showError('Authentication failed. Please try again.');
}
function showError(message) {
const container = document.querySelector("#doshi-container");
container.innerHTML = `
<div class="error-message">
<p>${message}</p>
<button onclick="window.location.reload()">Retry</button>
</div>
`;
}
// Initialize
fetchAuthToken();
HTML Structure
Copy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Doshi Integration</title>
<style>
#doshi-container {
width: 100%;
height: 100vh;
position: relative;
}
.error-message {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
text-align: center;
}
.error-message button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
}
</style>
</head>
<body>
<div id="doshi-container"></div>
<script src="doshi-integration.js"></script>
</body>
</html>
Backend Implementation
Your backend should handle the API key securely:Copy
// backend/routes/auth.ts
import express from 'express';
const router = express.Router();
router.post('/generate-doshi-token', async (req, res) => {
try {
// Verify user is authenticated in your system
if (!req.session?.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
const { email, branchId } = req.body;
// Call Doshi API with your API key
const response = await fetch(
'https://production-doshi-api-8kq2.encr.app/client/auth/token',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
email,
partnerUserId: req.session.userId,
branchId
})
}
);
if (!response.ok) {
throw new Error('Doshi API request failed');
}
const data = await response.json();
// Log for security monitoring
console.log(`Generated token for user ${req.session.userId}`);
res.json({ token: data.token });
} catch (error) {
console.error('Error generating Doshi token:', error);
res.status(500).json({ error: 'Failed to generate authentication token' });
}
});
export default router;
Child Window (Webview) Implementation
This section is for reference only. The Doshi iframe already implements this functionality.
Sending PING
Copy
// Inside the webview, send PING to parent when ready
window.parent.postMessage(
JSON.stringify({ type: "PING" }),
"*" // ⚠️ In production: specify parent origin if known
);
Receiving AUTH Data
Copy
// Listen for AUTH response from parent
window.addEventListener("message", (event) => {
// ✅ Verify origin in production
// if (event.origin !== 'https://parent-domain.com') return;
try {
const data = typeof event.data === "string"
? JSON.parse(event.data)
: event.data;
if (data.type === "AUTH") {
// Process authentication data
const {
token,
email,
segment,
branchId,
is2FaEnabled,
dob,
organizationId,
partnerUserId,
firstName,
lastName
} = data;
// Authenticate user with token
await authenticateUser(token);
// If 2FA is enabled, show OTP screen
if (is2FaEnabled) {
await handle2FAFlow({
dob,
organizationId,
partnerUserId,
firstName,
lastName
});
}
// Notify parent of success
window.parent.postMessage(
JSON.stringify({
type: "AUTH_SUCCESS",
userId: user.id
}),
event.origin
);
}
} catch (error) {
console.error("Error processing auth message:", error);
// Notify parent of error
window.parent.postMessage(
JSON.stringify({
type: "AUTH_ERROR",
error: error.message
}),
event.origin
);
}
});
Advanced: Retry Logic
Add retry logic for more robust authentication:Copy
const WebviewWithRetry: React.FC<Props> = ({ userEmail }) => {
const [retryCount, setRetryCount] = useState(0);
const MAX_RETRIES = 3;
const RETRY_DELAY = 2000; // 2 seconds
const handleSendAuth = async () => {
try {
const authData = {
token: authToken,
email: userEmail,
type: "AUTH"
};
const webview = document.querySelector("iframe");
if (webview && webview.contentWindow) {
webview.contentWindow.postMessage(
JSON.stringify(authData),
"https://embed.doshi.app"
);
} else {
throw new Error("Webview not found");
}
} catch (error) {
console.error("Error sending auth:", error);
if (retryCount < MAX_RETRIES) {
console.log(`Retrying... (${retryCount + 1}/${MAX_RETRIES})`);
setTimeout(() => {
setRetryCount(retryCount + 1);
handleSendAuth();
}, RETRY_DELAY);
} else {
setError('Failed to authenticate after multiple attempts');
}
}
};
// ... rest of component
};
Message Types
The message type identifier
Show Available Types
Show Available Types
From Iframe to Parent:
PING: Sent by webview to signal readinessAUTH_SUCCESS: Authentication completed successfullyAUTH_ERROR: Authentication failed
AUTH: Sent by parent with authentication data
Testing postMessage Flow
Copy
// Test the authentication flow
function testPostMessageFlow() {
console.log('Testing postMessage flow...');
// Simulate PING from iframe
window.postMessage(
JSON.stringify({ type: "PING" }),
window.location.origin
);
// Listen for AUTH response
window.addEventListener("message", (event) => {
const data = JSON.parse(event.data);
console.log("Received message:", data);
if (data.type === "AUTH") {
console.log("✅ AUTH message received");
console.log("Token:", data.token);
console.log("Email:", data.email);
}
});
}
Common Issues
Messages not being received
Messages not being received
Solutions:
- Check that origin validation matches exactly
- Verify iframe has loaded before sending messages
- Ensure JSON.stringify is used when sending data
- Check browser console for errors
Token not available
Token not available
Solutions:
- Ensure backend API call completed successfully
- Check network tab for API response
- Verify error handling in token fetch logic
- Check that API key is valid
2FA not triggering
2FA not triggering
Solutions:
- Verify
is2FaEnabledis set totrue - Ensure all required 2FA fields are provided
- Check that organization has 2FA enabled
- Verify user phone number is registered
