insurEco

Integration Guide

insurEco System SSO

Back to Home
Developer Documentation

Integrate insurEco SSO

Add secure single sign-on to your application in minutes. Choose between HTML/JavaScript or React/Next.js implementations.

Download LLM

Interactive Button Demo

View live examples of all available button styles with copy-paste ready code snippets.

View Live Demo

insurEco 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:

bash
npm install @insureco/bio
typescript
import { 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:

code
┌─────────────┐                                      ┌─────────────┐
│   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 be code
  • client_id (required): Your application's client ID
  • redirect_uri (required): Callback URL (must be pre-registered)
  • scope (required): Space-separated scopes (e.g., openid profile email)
  • state (recommended): Random string for CSRF protection
  • code_challenge (required): SHA-256 hash of code_verifier
  • code_challenge_method (required): S256 (recommended) or plain

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 token
  • client_credentials - Service-to-service authentication (no user context)

Response:

json
{
  "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:

json
{
  "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:

  • profile scope: name, given_name, family_name, job_title, phone, address, messaging
  • email scope: email, email_verified

Introspection Endpoint

**URL**: https://bio.tawa.insureco.io/api/auth/introspect

Method: POST

**Body**: { "token": "..." }

Response (user token):

json
{
  "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)

json
{
  "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

json
{
  "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.

typescript
// 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

1. Add Sign In Button

html
<!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

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

html
<!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:

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)

html
<!-- 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

html
<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

html
<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

javascript
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

javascript
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

javascript
// 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

javascript
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

javascript
// 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

javascript
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.

Sign in with insurEco Button

Pre-built button components for integrating insurEco System SSO into your application.

React / Next.js Component

tsx
'use client';

import Image from 'next/image';

interface SignInWithInsurecoProps {
  onClick?: () => void;
  className?: string;
}

export function SignInWithInsureco({ onClick, className = '' }: SignInWithInsurecoProps) {
  const handleClick = () => {
    if (onClick) {
      onClick();
    } else {
      // Default behavior: redirect to your OAuth login endpoint
      window.location.href = '/api/auth/login';
    }
  };

  return (
    <button
      onClick={handleClick}
      className={`
        inline-flex items-center gap-3 px-6 py-3
        bg-white border-2 border-gray-300 rounded-lg
        hover:border-gray-400 hover:shadow-md
        transition-all duration-200
        font-medium text-gray-700
        ${className}
      `}
    >
      <Image
        src="https://docs.insureco.io/images/logo/insureco-globe.svg"
        alt="insurEco"
        width={24}
        height={24}
        className="animate-pulse"
      />
      Sign in with insurEco
    </button>
  );
}

Usage

tsx
import { SignInWithInsureco } from '@/components/SignInWithInsureco';

export default function LoginPage() {
  return (
    <div>
      <h1>Welcome</h1>
      <SignInWithInsureco />
    </div>
  );
}

Plain HTML/CSS Button

html
<!DOCTYPE html>
<html>
<head>
  <style>
    .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-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      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;
      animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
    }

    @keyframes pulse {
      0%, 100% {
        opacity: 1;
      }
      50% {
        opacity: 0.7;
      }
    }
  </style>
</head>
<body>
  <a href="/api/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>

Tailwind CSS Version

tsx
export function SignInWithInsureco() {
  return (
    <a
      href="/api/auth/login"
      className="inline-flex items-center gap-3 px-6 py-3 bg-white border-2 border-gray-300 rounded-lg hover:border-gray-400 hover:shadow-md transition-all duration-200 font-medium text-gray-700"
    >
      <img
        src="https://docs.insureco.io/images/logo/insureco-globe.svg"
        alt="insurEco"
        className="w-6 h-6 animate-pulse"
      />
      Sign in with insurEco
    </a>
  );
}

Gradient Button (Premium Style)

tsx
'use client';

import Image from 'next/image';

export function SignInWithInsurecoGradient() {
  return (
    <button
      onClick={() => window.location.href = '/api/auth/login'}
      className="
        inline-flex items-center gap-3 px-8 py-4
        bg-gradient-to-r from-blue-600 to-indigo-600
        hover:from-blue-700 hover:to-indigo-700
        text-white font-semibold text-lg
        rounded-xl shadow-lg hover:shadow-xl
        transition-all duration-300 transform hover:scale-105
      "
    >
      <Image
        src="https://docs.insureco.io/images/logo/insureco-globe.svg"
        alt="insurEco"
        width={28}
        height={28}
        className="drop-shadow-md"
      />
      Sign in with insurEco System
    </button>
  );
}

Minimal Button

tsx
export function SignInWithInsurecoMinimal() {
  return (
    <a
      href="/api/auth/login"
      className="inline-flex items-center gap-2 px-4 py-2 text-sm text-gray-600 hover:text-gray-900 transition-colors"
    >
      <img
        src="https://docs.insureco.io/images/logo/insureco-globe.svg"
        alt="insurEco"
        className="w-5 h-5"
      />
      Sign in with insurEco
    </a>
  );
}

Button with Loading State

tsx
'use client';

import { useState } from 'react';
import Image from 'next/image';

export function SignInWithInsurecoLoading() {
  const [loading, setLoading] = useState(false);

  const handleSignIn = () => {
    setLoading(true);
    window.location.href = '/api/auth/login';
  };

  return (
    <button
      onClick={handleSignIn}
      disabled={loading}
      className="
        inline-flex items-center gap-3 px-6 py-3
        bg-white border-2 border-gray-300 rounded-lg
        hover:border-gray-400 hover:shadow-md
        transition-all duration-200
        font-medium text-gray-700
        disabled:opacity-50 disabled:cursor-not-allowed
      "
    >
      {loading ? (
        <>
          <svg className="animate-spin h-6 w-6 text-gray-600" viewBox="0 0 24 24">
            <circle
              className="opacity-25"
              cx="12"
              cy="12"
              r="10"
              stroke="currentColor"
              strokeWidth="4"
              fill="none"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
            />
          </svg>
          Redirecting...
        </>
      ) : (
        <>
          <Image
            src="https://docs.insureco.io/images/logo/insureco-globe.svg"
            alt="insurEco"
            width={24}
            height={24}
          />
          Sign in with insurEco
        </>
      )}
    </button>
  );
}

Dark Mode Support

tsx
export function SignInWithInsurecoDark() {
  return (
    <button
      onClick={() => window.location.href = '/api/auth/login'}
      className="
        inline-flex items-center gap-3 px-6 py-3
        bg-white dark:bg-gray-800
        border-2 border-gray-300 dark:border-gray-600
        rounded-lg
        hover:border-gray-400 dark:hover:border-gray-500
        hover:shadow-md
        transition-all duration-200
        font-medium text-gray-700 dark:text-gray-200
      "
    >
      <img
        src="https://docs.insureco.io/images/logo/insureco-globe.svg"
        alt="insurEco"
        className="w-6 h-6"
      />
      Sign in with insurEco
    </button>
  );
}

Full-Width Button (Mobile Friendly)

tsx
export function SignInWithInsurecoFullWidth() {
  return (
    <button
      onClick={() => window.location.href = '/api/auth/login'}
      className="
        w-full flex items-center justify-center gap-3 px-6 py-4
        bg-white border-2 border-gray-300 rounded-lg
        hover:border-gray-400 hover:shadow-md
        transition-all duration-200
        font-medium text-gray-700
      "
    >
      <img
        src="https://docs.insureco.io/images/logo/insureco-globe.svg"
        alt="insurEco"
        className="w-6 h-6"
      />
      Sign in with insurEco
    </button>
  );
}

Custom Styling Guide

Colors

  • **Primary**: Use #3b82f6 (blue-600) or your brand colors
  • **Border**: Use #d1d5db (gray-300) for light mode
  • **Text**: Use #374151 (gray-700) for readability

Spacing

  • **Padding**: 12px 24px (py-3 px-6)
  • **Gap**: 12px between icon and text
  • **Border Radius**: 8px for modern look

Icon Size

  • Standard: 24x24px
  • Large: 28x28px
  • Small: 20x20px

Accessibility

Always include:

  • alt text for the logo image
  • Proper contrast ratios (WCAG AA minimum)
  • Focus states for keyboard navigation
  • Loading states for better UX

Logo Variants

The insurEco globe logo is available in three variants:

Color Globe (Default)

  • **URL**: https://docs.insureco.io/images/logo/insureco-globe.svg
  • Use case: Standard buttons on light backgrounds
  • Best for: Most common use cases

White Globe

  • **URL**: https://docs.insureco.io/images/logo/insureco-globe-white.svg
  • Use case: Dark or colored backgrounds
  • Best for: Black buttons, dark mode, gradient backgrounds

Black Globe

  • **URL**: https://docs.insureco.io/images/logo/insureco-globe-black.svg
  • Use case: White or very light backgrounds
  • Best for: White buttons on dark backgrounds, minimal designs

Button Examples with Logo Variants

tsx
export function SignInWithInsurecoBlack() {
  return (
    <a
      href="/api/auth/login"
      className="inline-flex items-center gap-3 px-6 py-3 bg-black border-2 border-black rounded-lg hover:bg-gray-800 transition-all duration-200 font-medium text-white"
    >
      <img
        src="https://docs.insureco.io/images/logo/insureco-globe-white.svg"
        alt="insurEco"
        className="w-6 h-6"
      />
      Sign in with insurEco
    </a>
  );
}
tsx
export function SignInWithInsurecoWhite() {
  return (
    <a
      href="/api/auth/login"
      className="inline-flex items-center gap-3 px-6 py-3 bg-white border-2 border-white rounded-lg hover:bg-gray-50 transition-all duration-200 font-medium text-gray-900 shadow-md"
    >
      <img
        src="https://docs.insureco.io/images/logo/insureco-globe-black.svg"
        alt="insurEco"
        className="w-6 h-6"
      />
      Sign in with insurEco
    </a>
  );
}

Logo Usage Guidelines

1. Choose the right variant for your background:

- Color globe for light backgrounds

- White globe for dark/colored backgrounds

- Black globe for very light/white backgrounds

2. Maintain aspect ratio - don't stretch or distort

3. Minimum size: 20x20px for readability

4. Clear space: Maintain adequate padding around the logo

5. Don't modify: Use the logo as provided without alterations

Example Integration

tsx
// app/login/page.tsx
import { SignInWithInsureco } from '@/components/SignInWithInsureco';

export default function LoginPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <div className="text-center">
          <h1 className="text-3xl font-bold text-gray-900">Welcome Back</h1>
          <p className="mt-2 text-sm text-gray-600">
            Sign in to access your account
          </p>
        </div>

        <div className="mt-8 space-y-6">
          {/* Other login options */}

          <div className="relative">
            <div className="absolute inset-0 flex items-center">
              <div className="w-full border-t border-gray-300" />
            </div>
            <div className="relative flex justify-center text-sm">
              <span className="px-2 bg-white text-gray-500">Or continue with</span>
            </div>
          </div>

          <SignInWithInsureco />
        </div>
      </div>
    </div>
  );
}

Need help? Contact our support team or visit the full documentation.

Powered by insurEco System