Skip to main content

Overview

Security is paramount when implementing webview authentication. This guide covers critical security considerations for both postMessage and query parameter methods.

IP Whitelisting

The Doshi API requires IP whitelisting. Contact [email protected] to whitelist your server IP addresses.
To get your server’s IP address:
# Run this command on your server
curl ifconfig.me
Send this IP address to [email protected] along with your organization name.

API Key Security

Never expose your API key in client-side code. Always call the Doshi API from your backend server.

Secure API Key Usage

// ❌ NEVER do this - API key exposed in frontend
const response = await fetch('https://api.doshi.app/client/auth/token', {
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY'  // Exposed to users!
  }
});

// ✅ Correct - Call your backend which securely stores the API key
const response = await fetch('https://your-backend.com/api/generate-doshi-token', {
  method: 'POST',
  body: JSON.stringify({ userId: 'user_123' })
});

Backend Implementation Example

// Your backend server
app.post('/api/generate-doshi-token', async (req, res) => {
  // Verify the user is authenticated in your system
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Call Doshi API with your API key (stored securely in environment variables)
  const response = await fetch('https://api.doshi.app/client/auth/token', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.DOSHI_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: req.user.email,
      partnerUserId: req.user.id
    })
  });

  const data = await response.json();
  res.json({ token: data.token });
});

Origin Verification

Always verify the origin of messages in production environments

For postMessage

window.addEventListener("message", (event) => {
  // ✅ Verify origin in production
  if (event.origin !== 'https://embed.doshi.app') {
    console.warn('Rejected message from unauthorized origin:', event.origin);
    return;
  }
  
  // Process message
});

Specify Target Origins

// ❌ Never use in production
webview.contentWindow.postMessage(data, "*");

// ✅ Always specify exact origin in production
webview.contentWindow.postMessage(data, "https://embed.doshi.app");

Token Security

Token Lifecycle

The nonce token received from the API:
  • Is single-use for initial authentication
  • Has a short expiration time
  • Cannot be reused after the user session is established
// After getting the token, use it immediately
const { token } = await getTokenFromBackend();

// Pass to iframe right away
passTokenToIframe(token);

// Don't store the token for long periods
// Don't reuse the token for multiple sessions

Session Tokens

Once authenticated:
  • ID Token TTL: 1 hour
  • Refresh Token TTL: 12 hours
  • The iframe manages token refresh automatically
  • Sessions are isolated per iframe instance

Cross-Origin Communication

postMessage API Security

Always validate the structure and content of messages:
    try {
      const data = typeof event.data === "string" 
        ? JSON.parse(event.data) 
        : event.data;
      
      // Validate message structure
      if (!data.type || typeof data.type !== 'string') {
        throw new Error('Invalid message structure');
      }
      
      // Validate expected message types
      const validTypes = ['PING', 'AUTH', 'AUTH_SUCCESS', 'AUTH_ERROR'];
      if (!validTypes.includes(data.type)) {
        throw new Error(`Unexpected message type: ${data.type}`);
      }
      
      // Process valid message
    } catch (error) {
      console.error("Error processing message:", error);
      return;
    }
Implement comprehensive error handling:
    const handleMessage = (event) => {
      try {
        // Verify origin
        if (event.origin !== 'https://embed.doshi.app') return;
        
        // Parse and validate
        const data = JSON.parse(event.data);
        
        // Process message
      } catch (error) {
        console.error('Message handling failed:', error);
        // Log to monitoring service
        logError(error);
      }
    };
Always use HTTPS for secure communication:
    // ✅ Secure HTTPS URL
    const webviewUrl = "https://embed.doshi.app";

    // ❌ Never use HTTP in production
    const insecureUrl = "http://embed.doshi.app";

Query Parameter Security

Query parameters are visible in URLs and browser history. The nonce token is short-lived but still sensitive.

Best Practices

  1. Clear Parameters After Use
// After iframe loads, clear sensitive params
window.addEventListener('load', () => {
  if (window.history.replaceState) {
    const url = new URL(window.location.href);
    url.search = ''; // Clear all query params
    window.history.replaceState({}, document.title, url.toString());
  }
});
  1. Use Short-Lived Tokens
The nonce token from the API is already short-lived, but you can add additional validation:
// Backend - add timestamp to token generation
const tokenData = {
  email: user.email,
  timestamp: Date.now()
};

// Frontend - check token age before using
const isTokenFresh = (timestamp: number) => {
  const maxAge = 5 * 60 * 1000; // 5 minutes
  return Date.now() - timestamp < maxAge;
};
  1. Monitor Token Usage
// Log token usage for security monitoring
app.post('/api/generate-doshi-token', async (req, res) => {
  const token = await generateDoshiToken(req.user);
  
  // Log token generation
  await logSecurityEvent({
    event: 'token_generated',
    userId: req.user.id,
    timestamp: new Date(),
    ip: req.ip
  });
  
  res.json({ token });
});
  1. Respect URL Length Limits
URLs should generally stay under 2000 characters for maximum browser compatibility
function buildWebviewUrl(baseUrl, params) {
  const urlParams = new URLSearchParams(params);
  const fullUrl = `${baseUrl}?${urlParams.toString()}`;
  
  // Check URL length
  if (fullUrl.length > 2000) {
    console.warn('URL exceeds recommended length. Consider using postMessage.');
  }
  
  return fullUrl;
}

2FA Security

When implementing 2FA:
// Only pass 2FA parameters when explicitly enabled
const authData = {
  token: nonceToken,
  email: user.email,
  // Only include these if 2FA is actually enabled
  ...(is2FaEnabled && {
    is2FaEnabled: true,
    dob: user.dob,
    organizationId: org.id,
    partnerUserId: user.id,
    firstName: user.firstName,
    lastName: user.lastName
  })
};

Don’t Send Unnecessary Data

// ❌ Don't send sensitive data if not needed
const authData = {
  token: nonceToken,
  ssn: user.ssn,  // Never send unless absolutely required
  password: user.password  // Never send passwords
};

// ✅ Only send required fields
const authData = {
  token: nonceToken,
  email: user.email,
  segment: 'premium'
};

Security Checklist

Store API key securely in backend environment variables
Never expose API key in client-side code
Call Doshi API only from your backend server
Use HTTPS for all communication
Verify message origins in production
Never use ”*” wildcard for target origins in production
Validate all incoming messages and parameters
Use short-lived tokens
Clear sensitive data from URLs after reading
Implement comprehensive error handling
Log security events for monitoring
Test across different browsers and environments

Common Security Pitfalls

PitfallRiskSolution
API key in frontendAnyone can steal your keyCall API from backend only
Using "*" for postMessage targetAny site can receive messagesSpecify exact origin
Sensitive tokens in URLsExposed in logs/historyUse postMessage or clear params
No origin verificationMalicious sites can send messagesAlways check event.origin
Missing input validationXSS vulnerabilitiesValidate all inputs
Long-lived tokensIncreased attack windowUse short expiration times
Storing tokens in localStorageXSS attacks can steal tokensLet iframe manage sessions

Environment-Specific Configuration

// config.ts
const config = {
  development: {
    doshiApiUrl: 'https://sandbox.api.doshi.app',
    doshiEmbedUrl: 'https://staging-embed.doshi.app',
    strictOriginCheck: false  // For local testing
  },
  production: {
    doshiApiUrl: 'https://api.doshi.app',
    doshiEmbedUrl: 'https://embed.doshi.app',
    strictOriginCheck: true
  }
};

// Use environment-specific config
const env = process.env.NODE_ENV || 'development';
export default config[env];

Monitoring and Logging

// Log security-relevant events
const logSecurityEvent = async (event: SecurityEvent) => {
  await logger.log({
    level: 'security',
    event: event.type,
    userId: event.userId,
    ip: event.ip,
    timestamp: new Date(),
    metadata: event.metadata
  });
};

// Monitor for suspicious activity
app.post('/api/generate-doshi-token', async (req, res) => {
  // Rate limiting
  const recentTokens = await getRecentTokensForUser(req.user.id);
  if (recentTokens.length > 10) {
    await logSecurityEvent({
      type: 'excessive_token_generation',
      userId: req.user.id,
      ip: req.ip
    });
    return res.status(429).json({ error: 'Too many requests' });
  }
  
  // Generate token...
});

Next Steps