Integrate insurEco SSO
Add secure single sign-on to your application in minutes. Choose between HTML/JavaScript or React/Next.js implementations.
SSO Overview
Architecture and features of insurEco System SSO
HTML/JavaScript
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 DemoBio-ID SSO — Developer Overview
Introduction
Bio-ID is the centralized identity and access management platform for the InsurEco ecosystem, built on OAuth 2.0 and OpenID Connect standards. It handles authentication, user management, role/permission management, and OAuth client provisioning for all Tawa platform services.
**Production URL**: https://bio.tawa.pro (also reachable at https://bio.insureco.io)
Architecture Overview
Core Components
1. Bio-ID Server - The central OAuth 2.0 authorization server
- Handles user authentication (password, MFA, passkey, magic link)
- Issues access tokens and refresh tokens (HS256 JWTs)
- Manages user profiles, roles, permissions, and departments
- Provides OIDC-compliant endpoints (discovery, JWKS, userinfo, introspection)
2. Client Applications - Your applications that integrate with Bio-ID
- 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 profiles, credentials, roles, permissions, enabled modules
- Maintains audit logs for compliance
Technology Stack
- Protocol: OAuth 2.0 with PKCE (Proof Key for Code Exchange)
- Token Format: JWT (HS256, symmetric) — verified by Janus gateway for platform services
- Transport Security: HTTPS/TLS 1.3
- Database: MongoDB for user data and sessions; Redis for token storage
SDK
npm install @insureco/bioimport { BioAuth } from '@insureco/bio'
// OAuth flow client — reads BIO_CLIENT_ID, BIO_CLIENT_SECRET, BIO_ID_URL from env
const bio = BioAuth.fromEnv()Tawa Platform Integration (Recommended)
If you're deploying on the Tawa platform, Bio-ID credentials are **auto-provisioned on every deploy** via catalog-info.yaml. You do not need to manually register an OAuth client.
catalog-info.yaml
spec:
auth:
mode: sso # provisions BIO_CLIENT_ID and BIO_CLIENT_SECRET on every deployThe builder automatically:
1. Creates an OAuth client in Bio-ID (idempotent — updates if already exists)
2. Registers the redirect URI: https://{service}.tawa.pro/api/auth/callback (prod) / https://{service}.sandbox.tawa.pro/api/auth/callback (sandbox)
3. Injects BIO_CLIENT_ID and BIO_CLIENT_SECRET as pod environment variables
BIO_ID_URL is always injected as a core platform env var — no declaration needed.
> **Important**: Without spec.auth: mode: sso, BIO_CLIENT_ID and BIO_CLIENT_SECRET will NOT be injected into your pod. Do not use internalDependencies: bio-id — that is wrong.
Your callback route MUST be at:
/api/auth/callbackThe builder registers this exact path. Any other path will fail with "Invalid Redirect URI".
Authentication Flow
Bio-ID uses 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
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
- S256 method is required (plain is not recommended)
State Parameter
Prevents CSRF attacks by generating a random state value before redirecting and verifying it on callback.
Secure Token Storage
- Access Tokens: Store in HTTP-only, Secure, SameSite cookies — never in localStorage
- Refresh Tokens: Also in HTTP-only cookies
- Never expose tokens to JavaScript to prevent XSS attacks
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
All endpoints are under https://bio.tawa.pro (or https://bio.insureco.io).
Discovery (OIDC)
GET /.well-known/openid-configurationAlways discover endpoints from here rather than hardcoding paths.
Authorization
GET /oauth/authorizeParameters:
response_type=code(required)client_id(required)redirect_uri(required — must be pre-registered)scope— space-separated, e.g.openid profile emailstate— random string for CSRF protectioncode_challenge— SHA-256 hash of code_verifiercode_challenge_method=S256
Token
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded> **Critical**: The token endpoint path is /api/oauth/token — NOT /oauth/token.
Grant types:
authorization_code— exchange auth code for tokens (with PKCE)refresh_token— refresh an expired access tokenclient_credentials— service-to-service (no user context)
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "a1b2c3d4...",
"scope": "openid profile email"
}UserInfo
GET /api/oauth/userinfo
Authorization: Bearer {access_token}Revocation
POST /api/oauth/revokeIntrospection
POST /api/auth/introspectJWKS
GET /.well-known/jwks.jsonEndpoints Reference
| Endpoint | Path | Method |
|----------|------|--------|
| Discovery | /.well-known/openid-configuration | GET |
| JWKS | /.well-known/jwks.json | GET |
| Authorization | /oauth/authorize | GET |
| **Token** | **/api/oauth/token** | POST |
| UserInfo | /api/oauth/userinfo | GET |
| Revocation | /api/oauth/revoke | POST |
| Introspection | /api/auth/introspect | POST |
Token Structure
Access Token (JWT, HS256)
{
"iss": "https://bio.tawa.pro",
"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": {}
}
}Key claims:
sub/bioId— User's unique Bio-ID (format:BIO-E{timestamp}{random})orgId— Organization ID (format:ORG-{timestamp}{random}) — use for Bio-ID admin APIsorgSlug— URL-friendly org slug (e.g.insureco) — use for service registries, wallet, Kokoroles— Assigned rolespermissions— Aggregated permissions from roles + modulesenabled_modules— Org modules the user has access to
Client Credentials Token
{
"iss": "https://bio.tawa.pro",
"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.
Token Verification
For platform services behind Janus, you typically don't need to verify tokens yourself — Janus verifies them for routes registered with auth: required in Koko.
If you need to verify tokens in your own middleware:
import { verifyTokenJWKS } from '@insureco/bio'
// Verifies using Bio-ID's JWKS endpoint — no shared secret needed
const payload = await verifyTokenJWKS(accessToken)
console.log(payload.bioId, payload.orgSlug, payload.roles)> **Never use the old verifyToken(token, secret) pattern** (HS256 with shared secret). Use verifyTokenJWKS() which fetches the public key from Bio-ID's JWKS endpoint.
UserInfo 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, phoneemailscope: email, email_verified
Available Scopes
| Scope | Description |
|-------|-------------|
| openid | Required for OIDC — includes sub claim |
| profile | User's profile info (name, job title, etc.) |
| email | User's email address and verification status |
| offline_access | Include refresh token |
User Roles and Permissions
Bio-ID uses RBAC (Role-Based Access Control):
Standard User Types
employee— Organization memberagent— External insurance agentadmin— Organization administrator
Checking Roles and Permissions
// From access token payload or userinfo response
if (user.roles.includes('admin')) { ... }
if (user.permissions.includes('spaces:write')) { ... }Session Management
Session Lifecycle
1. Creation — User authenticates, tokens issued
2. Refresh — Refresh token extends session (old token rotated)
3. Termination — User logout, token expiration, or admin revocation
Error Codes
| Error Code | Description | Resolution |
|------------|-------------|------------|
| invalid_request | Malformed request | Check required parameters |
| invalid_client | Client authentication failed | Verify client_id and secret |
| invalid_grant | Invalid auth code or 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.
Audit Logging
Authentication events logged for security and compliance:
- Login attempts (success/failure)
- OAuth authorizations
- Token exchanges and refreshes
- Logout events
- Password changes
- Role and permission modifications
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
Resources
- Integration Guide: https://bio.tawa.pro/how-to-integrate
- Button Demo: https://bio.tawa.pro/demo/sign-in-button
- **SDK**:
npm install @insureco/bio - OIDC Discovery: https://bio.tawa.pro/.well-known/openid-configuration
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.tawa.pro', // also available: 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.tawa.pro',
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
// WARNING: localStorage is accessible by any JS on the page (XSS risk).
// For production apps, use HttpOnly cookies via a backend proxy instead.
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`, { // NOTE: /api/oauth/token — not /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