Integrate insurEco SSO
Add secure single sign-on to your application in minutes. Choose between HTML/JavaScript or React/Next.js implementations.
SSO Overview
Comprehensive guide to insurEco System SSO architecture and features
HTML/JavaScript
Complete integration guide for vanilla JavaScript applications
Sign-In Button Components
Ready-to-use button components for React, Next.js, and HTML
Interactive Button Demo
View live examples of all available button styles with copy-paste ready code snippets.
View Live DemoinsurEco System SSO - Developer Overview
Introduction
insurEco System Single Sign-On (SSO) is a centralized identity and access management platform built on OAuth 2.0 and OpenID Connect standards. It provides secure, seamless authentication across all insurEco ecosystem applications.
Architecture Overview
Core Components
1. BIO ID Server - The central OAuth 2.0 authorization server
- Handles user authentication and authorization
- Issues access tokens and refresh tokens
- Manages user profiles, roles, and permissions
- Provides OAuth 2.0 + OIDC endpoints
2. Client Applications - Your applications that integrate with SSO
- Redirect users to BIO ID for authentication
- Receive authorization codes and exchange them for tokens
- Make authenticated API requests using access tokens
3. User Directory - Centralized user database
- Single source of truth for user identities
- Stores user profiles, credentials, and permissions
- Maintains audit logs for security compliance
Technology Stack
- Protocol: OAuth 2.0 with PKCE (Proof Key for Code Exchange)
- Token Format: JWT (JSON Web Tokens) signed with HS256
- Transport Security: HTTPS/TLS 1.2+
- Database: MongoDB for user data and sessions
- Runtime: Node.js with Next.js 14+
SDK
The @insureco/bio npm package provides a zero-dependency SDK for integrating with BIO ID:
npm install @insureco/bioimport { BioAuth, BioAdmin } from '@insureco/bio'
// OAuth flow client (reads BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL from env)
const bio = BioAuth.fromEnv()
// Admin API client (reads INTERNAL_API_KEY, BIO_ID_URL from env)
const admin = BioAdmin.fromEnv()How It Works
Authentication Flow
The insurEco SSO uses the OAuth 2.0 Authorization Code flow with PKCE:
┌─────────────┐ ┌─────────────┐
│ Client │ │ BIO ID │
│ Application │ │ Server │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. User clicks "Sign in with insurEco" │
│ │
│ 2. Generate PKCE code_verifier & code_challenge │
│ │
│ 3. Redirect to /oauth/authorize │
│──────────────────────────────────────────────────→ │
│ │
│ 4. User authenticates │
│ (login form) │
│ │
│ 5. Redirect back with authorization code │
│←──────────────────────────────────────────────────│
│ │
│ 6. Exchange code for tokens at /api/oauth/token │
│──────────────────────────────────────────────────→ │
│ │
│ 7. Receive tokens: │
│ - access_token (JWT, 15 min lifetime) │
│ - refresh_token (30 day lifetime) │
│←──────────────────────────────────────────────────│
│ │
│ 8. Store tokens securely (HTTP-only cookies) │
│ │
│ 9. Make API requests with access_token │
│ │
│ 10. When access_token expires, use refresh_token │
│ to obtain new access_token │Key Security Features
1. PKCE (Proof Key for Code Exchange)
Prevents authorization code interception attacks:
- code_verifier: 32 random bytes, base64url-encoded
- code_challenge: SHA-256 hash of code_verifier, base64url-encoded
- Both S256 and plain methods are supported (S256 recommended)
2. State Parameter
Prevents CSRF attacks by generating a random state value before redirecting to authorization and verifying it on callback.
3. Secure Token Storage
- Access Tokens: Stored in HTTP-only, Secure, SameSite cookies
- Refresh Tokens: Also stored in HTTP-only cookies
- Never exposed to JavaScript to prevent XSS attacks
4. Token Lifetimes
| Token | Default Lifetime | Notes |
|-------|-----------------|-------|
| Access token | 15 minutes (900s) | Configurable per OAuth client |
| Refresh token | 30 days (2,592,000s) | Rotated on each use |
| Client credentials | 1 hour (3,600s) | No refresh token issued |
| Authorization code | 10 minutes | Single use, hashed in DB |
OAuth 2.0 Endpoints
Authorization Endpoint
**URL**: https://bio.tawa.insureco.io/oauth/authorize
Method: GET
Parameters:
response_type(required): Must becodeclient_id(required): Your application's client IDredirect_uri(required): Callback URL (must be pre-registered)scope(required): Space-separated scopes (e.g.,openid profile email)state(recommended): Random string for CSRF protectioncode_challenge(required): SHA-256 hash of code_verifiercode_challenge_method(required):S256(recommended) orplain
Token Endpoint
**URL**: https://bio.tawa.insureco.io/api/oauth/token
Method: POST
**Content-Type**: application/x-www-form-urlencoded
Grant Types:
authorization_code- Exchange auth code for tokens (with PKCE)refresh_token- Refresh an expired access tokenclient_credentials- Service-to-service authentication (no user context)
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "a1b2c3d4...",
"scope": "openid profile email"
}UserInfo Endpoint
**URL**: https://bio.tawa.insureco.io/api/oauth/userinfo
Method: GET
**Headers**: Authorization: Bearer {access_token}
Response:
{
"sub": "BIO-E1732912345ABC12",
"bio_id": "BIO-E1732912345ABC12",
"email": "[email protected]",
"email_verified": true,
"name": "John Smith",
"given_name": "John",
"family_name": "Smith",
"user_type": "employee",
"roles": ["employee", "admin"],
"permissions": ["spaces:read", "spaces:write"],
"status": "active",
"org_id": "ORG-1771475158MOH5U2",
"org_slug": "insureco",
"organization_id": "...",
"organization_name": "InsureCo",
"enabled_modules": ["crm", "accounting"]
}Fields returned depend on granted scopes:
profilescope: name, given_name, family_name, job_title, phone, address, messagingemailscope: email, email_verified
Introspection Endpoint
**URL**: https://bio.tawa.insureco.io/api/auth/introspect
Method: POST
**Body**: { "token": "..." }
Response (user token):
{
"active": true,
"user": {
"id": "BIO-E1732912345ABC12",
"email": "[email protected]",
"name": "John Smith",
"org": "insureco",
"roles": ["admin"]
},
"org_id": "ORG-1771475158MOH5U2",
"org_slug": "insureco"
}Token Structure
Access Token (JWT, HS256)
{
"iss": "https://bio.tawa.insureco.io",
"sub": "BIO-E1732912345ABC12",
"aud": "your-client-id",
"exp": 1732916400,
"iat": 1732912800,
"bioId": "BIO-E1732912345ABC12",
"email": "[email protected]",
"name": "John Smith",
"userType": "employee",
"roles": ["employee", "admin"],
"permissions": ["spaces:read", "spaces:write"],
"orgId": "ORG-1771475158MOH5U2",
"orgSlug": "insureco",
"client_id": "your-client-id",
"scope": "openid profile email",
"enabled_modules": ["crm", "accounting"],
"onboarding": {
"platform": true,
"modules": {}
}
}Client Credentials Token
{
"iss": "https://bio.tawa.insureco.io",
"client_id": "your-service-prod",
"scope": "service:read service:write",
"token_type": "client_credentials",
"orgId": "ORG-1771475158MOH5U2",
"orgSlug": "insureco",
"exp": 1732916400,
"iat": 1732912800
}Note: Client credentials tokens have **no sub claim** - they identify the service, not a user.
User Roles and Permissions
The insurEco System includes role-based access control (RBAC):
Standard User Types
- employee - Organization member
- agent - External agent
- admin - Organization administrator
Roles and Permissions
Roles are assigned per user and carry permissions. Permissions are included in access tokens.
// Check user role
if (user.roles.includes('admin')) { ... }
// Check specific permission
if (user.permissions.includes('spaces:write')) { ... }Session Management
Session Lifecycle
1. Session Creation: When user successfully authenticates
2. Token Issuance: Access and refresh tokens generated
3. Session Refresh: Refresh tokens extend the session (old token rotated)
4. Session Termination: User logout, token expiration, or admin revocation
Audit Logging
Authentication events are logged for security and compliance:
- Login attempts (success/failure)
- OAuth authorizations
- Token exchanges and refreshes
- Logout events
- Password changes
- Role and permission modifications
Error Codes
OAuth Error Responses
| Error Code | Description | Resolution |
|------------|-------------|------------|
| invalid_request | Malformed request | Check required parameters |
| invalid_client | Client authentication failed | Verify client_id and secret |
| invalid_grant | Invalid authorization code/refresh token | Request new authorization |
| unauthorized_client | Client not authorized for grant type | Check client configuration |
| unsupported_grant_type | Grant type not supported | Use authorization_code, refresh_token, or client_credentials |
| invalid_scope | Requested scope invalid | Check available scopes |
| access_denied | User denied authorization | User must authorize access |
Rate Limiting
- Authorization requests: 10 per minute per IP
- Token requests: 20 per minute per client
- UserInfo requests: 100 per minute per user
- Failed login attempts: 5 per 15 minutes per user
Exceeding rate limits returns HTTP 429 (Too Many Requests).
Support and Resources
- Integration Guide: https://bio.tawa.insureco.io/how-to-integrate
- Button Demo: https://bio.tawa.insureco.io/demo/sign-in-button
- **SDK**:
npm install @insureco/bio - Discovery: https://bio.tawa.insureco.io/.well-known/openid-configuration
Standards Compliance
- RFC 6749: OAuth 2.0 Authorization Framework
- RFC 7636: PKCE for OAuth Public Clients
- RFC 7519: JSON Web Token (JWT)
- OpenID Connect Core 1.0
HTML/JavaScript SSO Integration Guide
Complete guide for integrating insurEco System SSO into vanilla HTML/JavaScript applications.
Table of Contents
- Quick Start
- Complete Implementation
- Button Components
- OAuth Flow Implementation
- Session Management
- Security Best Practices
Quick Start
1. Add Sign In Button
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<a href="/auth/login" class="insureco-signin-btn">
<img src="https://docs.insureco.io/images/logo/insureco-globe.svg" alt="insurEco">
Sign in with insurEco
</a>
</body>
</html>2. Add CSS
.insureco-signin-btn {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background-color: white;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: #374151;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.insureco-signin-btn:hover {
border-color: #9ca3af;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.insureco-signin-btn img {
width: 24px;
height: 24px;
}Complete Implementation
Single Page Application (SPA) Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App - insurEco SSO</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Login Page */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
background: white;
padding: 40px;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
text-align: center;
max-width: 400px;
width: 100%;
}
.login-card h1 {
margin-bottom: 10px;
color: #1f2937;
}
.login-card p {
margin-bottom: 30px;
color: #6b7280;
}
/* Button */
.insureco-signin-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 14px 24px;
background-color: white;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: #374151;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.insureco-signin-btn:hover {
border-color: #9ca3af;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.insureco-signin-btn img {
width: 24px;
height: 24px;
}
/* Dashboard */
.dashboard {
display: none;
}
.dashboard.active {
display: block;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.logout-btn {
padding: 8px 16px;
background: #ef4444;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
.logout-btn:hover {
background: #dc2626;
}
</style>
</head>
<body>
<!-- Login Page -->
<div id="loginPage" class="login-page">
<div class="login-card">
<h1>Welcome to My App</h1>
<p>Sign in to continue</p>
<button onclick="handleSignIn()" class="insureco-signin-btn">
<img src="https://docs.insureco.io/images/logo/insureco-globe.svg" alt="insurEco">
Sign in with insurEco
</button>
</div>
</div>
<!-- Dashboard -->
<div id="dashboard" class="dashboard">
<div class="header">
<h2>Dashboard</h2>
<div class="user-info">
<span id="userName">Loading...</span>
<button onclick="handleSignOut()" class="logout-btn">Sign Out</button>
</div>
</div>
<div class="container">
<h3>Welcome!</h3>
<p>You are successfully signed in with insurEco System.</p>
<div id="userDetails"></div>
</div>
</div>
<script>
// Configuration
const CONFIG = {
bioIdUrl: 'https://bio.insureco.io',
clientId: 'your-client-id',
redirectUri: window.location.origin + '/callback.html',
scope: 'openid profile email'
};
// PKCE Helper Functions
function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const values = new Uint8Array(length);
crypto.getRandomValues(values);
return Array.from(values)
.map(v => charset[v % charset.length])
.join('');
}
async function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return crypto.subtle.digest('SHA-256', data);
}
function base64urlencode(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
async function generatePKCE() {
const codeVerifier = generateRandomString(128);
const hashed = await sha256(codeVerifier);
const codeChallenge = base64urlencode(hashed);
return { codeVerifier, codeChallenge };
}
// Sign In Flow
async function handleSignIn() {
// Generate PKCE
const { codeVerifier, codeChallenge } = await generatePKCE();
// Generate state for CSRF protection
const state = generateRandomString(32);
// Store in sessionStorage
sessionStorage.setItem('oauth_code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const params = new URLSearchParams({
client_id: CONFIG.clientId,
redirect_uri: CONFIG.redirectUri,
response_type: 'code',
scope: CONFIG.scope,
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
// Redirect to BIO ID
window.location.href = `${CONFIG.bioIdUrl}/oauth/authorize?${params.toString()}`;
}
// Sign Out
function handleSignOut() {
localStorage.removeItem('access_token');
localStorage.removeItem('user_info');
sessionStorage.clear();
showLogin();
}
// Check Session on Load
window.addEventListener('DOMContentLoaded', () => {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
loadUserInfo();
showDashboard();
} else {
showLogin();
}
});
function showLogin() {
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('dashboard').classList.remove('active');
}
function showDashboard() {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('dashboard').classList.add('active');
}
async function loadUserInfo() {
const userInfo = localStorage.getItem('user_info');
if (userInfo) {
const user = JSON.parse(userInfo);
document.getElementById('userName').textContent = user.name;
document.getElementById('userDetails').innerHTML = `
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>User Type:</strong> ${user.userType}</p>
`;
}
}
</script>
</body>
</html>OAuth Callback Handler
Create callback.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Signing in...</title>
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.loading {
text-align: center;
color: white;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="loading">
<div class="spinner"></div>
<h2>Signing you in...</h2>
</div>
<script>
// Configuration (must match main app)
const CONFIG = {
bioIdUrl: 'https://bio.insureco.io',
clientId: 'your-client-id',
clientSecret: 'your-client-secret', // Only needed for confidential clients
redirectUri: window.location.origin + '/callback.html'
};
async function handleCallback() {
try {
// Parse URL parameters
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');
if (error) {
throw new Error(params.get('error_description') || error);
}
// Validate state
const storedState = sessionStorage.getItem('oauth_state');
if (!state || state !== storedState) {
throw new Error('Invalid state parameter');
}
// Get code verifier
const codeVerifier = sessionStorage.getItem('oauth_code_verifier');
if (!codeVerifier) {
throw new Error('Missing code verifier');
}
// Exchange code for tokens
const tokenResponse = await fetch(`${CONFIG.bioIdUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: CONFIG.redirectUri,
client_id: CONFIG.clientId,
client_secret: CONFIG.clientSecret,
code_verifier: codeVerifier
})
});
if (!tokenResponse.ok) {
const errorData = await tokenResponse.json();
throw new Error(errorData.error_description || 'Token exchange failed');
}
const tokens = await tokenResponse.json();
// Fetch user info
const userInfoResponse = await fetch(`${CONFIG.bioIdUrl}/api/oauth/userinfo`, {
headers: {
'Authorization': `Bearer ${tokens.access_token}`
}
});
if (!userInfoResponse.ok) {
throw new Error('Failed to fetch user info');
}
const userInfo = await userInfoResponse.json();
// Store tokens and user info
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
localStorage.setItem('user_info', JSON.stringify(userInfo));
// Clean up session storage
sessionStorage.removeItem('oauth_state');
sessionStorage.removeItem('oauth_code_verifier');
// Redirect to main app
window.location.href = '/';
} catch (error) {
console.error('OAuth callback error:', error);
alert('Sign in failed: ' + error.message);
window.location.href = '/';
}
}
// Run callback handler
handleCallback();
</script>
</body>
</html>Button Components
Standard Button (Copy-Paste Ready)
<!-- Add to your HTML -->
<a href="/auth/login" class="insureco-btn">
<img src="https://docs.insureco.io/images/logo/insureco-globe.svg" alt="insurEco">
Sign in with insurEco
</a>
<!-- Add to your CSS -->
<style>
.insureco-btn {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background: white;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: #374151;
text-decoration: none;
transition: all 0.2s;
}
.insureco-btn:hover {
border-color: #9ca3af;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.insureco-btn img {
width: 24px;
height: 24px;
}
</style>Gradient Button
<a href="/auth/login" class="insureco-btn-gradient">
<img src="https://docs.insureco.io/images/logo/insureco-globe.svg" alt="insurEco">
Sign in with insurEco
</a>
<style>
.insureco-btn-gradient {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 28px;
background: linear-gradient(to right, #2563eb, #4f46e5);
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
color: white;
text-decoration: none;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.insureco-btn-gradient:hover {
background: linear-gradient(to right, #1d4ed8, #4338ca);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
.insureco-btn-gradient img {
width: 24px;
height: 24px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
</style>Button with Loading State
<button id="signInBtn" onclick="handleSignIn()" class="insureco-btn-loading">
<img id="btnIcon" src="https://docs.insureco.io/images/logo/insureco-globe.svg" alt="insurEco">
<span id="btnText">Sign in with insurEco</span>
</button>
<style>
.insureco-btn-loading {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 12px 24px;
background: white;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
color: #374151;
cursor: pointer;
transition: all 0.2s;
}
.insureco-btn-loading:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.insureco-btn-loading img {
width: 24px;
height: 24px;
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid #f3f4f6;
border-top-color: #374151;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<script>
async function handleSignIn() {
const btn = document.getElementById('signInBtn');
const icon = document.getElementById('btnIcon');
const text = document.getElementById('btnText');
// Show loading state
btn.disabled = true;
icon.outerHTML = '<div class="spinner"></div>';
text.textContent = 'Redirecting...';
// Initiate OAuth flow
// ... your OAuth initialization code
window.location.href = '/api/auth/login';
}
</script>Session Management
Refresh Token Implementation
async function refreshAccessToken() {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${CONFIG.bioIdUrl}/api/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CONFIG.clientId,
client_secret: CONFIG.clientSecret
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
localStorage.setItem('access_token', tokens.access_token);
localStorage.setItem('refresh_token', tokens.refresh_token);
return tokens.access_token;
}
// Auto-refresh before token expires
function setupTokenRefresh() {
// Refresh 5 minutes before expiry (adjust as needed)
const refreshInterval = (15 * 60 - 5 * 60) * 1000; // 10 minutes
setInterval(async () => {
try {
await refreshAccessToken();
console.log('Token refreshed successfully');
} catch (error) {
console.error('Token refresh failed:', error);
handleSignOut();
}
}, refreshInterval);
}Protected API Calls
async function makeAuthenticatedRequest(url, options = {}) {
let accessToken = localStorage.getItem('access_token');
if (!accessToken) {
window.location.href = '/';
return;
}
try {
// Make request with access token
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
});
// If unauthorized, try refreshing token
if (response.status === 401) {
accessToken = await refreshAccessToken();
// Retry request with new token
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
});
}
return response;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// Usage example
async function fetchUserData() {
const response = await makeAuthenticatedRequest('https://api.yourapp.com/user');
return response.json();
}Security Best Practices
1. Always Use HTTPS
// Ensure redirect URI uses HTTPS in production
const CONFIG = {
redirectUri: window.location.protocol === 'https:'
? window.location.origin + '/callback.html'
: 'http://localhost:3000/callback.html' // Development only
};2. Validate State Parameter
function validateState(receivedState) {
const storedState = sessionStorage.getItem('oauth_state');
if (!receivedState || !storedState) {
throw new Error('Missing state parameter');
}
if (receivedState !== storedState) {
throw new Error('State mismatch - possible CSRF attack');
}
// Clean up
sessionStorage.removeItem('oauth_state');
}3. Secure Token Storage
// Use sessionStorage for more secure, session-only storage
// Or implement secure cookie-based storage
class SecureStorage {
static setToken(key, value) {
// Use httpOnly cookies via your backend
// Or use sessionStorage with encryption
const encrypted = this.encrypt(value);
sessionStorage.setItem(key, encrypted);
}
static getToken(key) {
const encrypted = sessionStorage.getItem(key);
return encrypted ? this.decrypt(encrypted) : null;
}
static encrypt(data) {
// Implement encryption (example only)
return btoa(data); // Use proper encryption in production
}
static decrypt(data) {
// Implement decryption
return atob(data);
}
}4. Handle Errors Gracefully
async function handleOAuthError(error) {
console.error('OAuth Error:', error);
// Clear any stored state
sessionStorage.clear();
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
// Show user-friendly error
const errorMessages = {
'access_denied': 'You denied access to the application',
'invalid_state': 'Security validation failed. Please try again',
'invalid_request': 'Invalid request. Please try again'
};
const message = errorMessages[error.code] || 'An error occurred during sign in';
alert(message);
// Redirect to login
window.location.href = '/';
}Demo Files
View a complete working demo at:
Open this file in your browser to see all button styles and interactive examples.
Next Steps
1. Register your application as an OAuth client with BIO ID
2. Configure your redirect URIs
3. Implement the callback handler
4. Add protected routes
5. Test the complete OAuth flow
For backend implementation (Node.js, PHP, Python, etc.), see the main SSO Integration Guide.
Need help? Contact our support team or visit the full documentation.
Powered by insurEco System