API Examples - HTML/JavaScript

Browser sessions use httpOnly cookies with credentials: 'include' - never store JWT or access tokens in localStorage/sessionStorage for LoginMe browser auth. Prefer the LoginMe widget for embeds and social login; mint short-lived assertions via POST /v1/session/assertion only for your customer backend to verify.

Prerequisites

Before using these examples, you need:

API Key - Get it from your organization settings in the dashboard

Organization Settings

ORGANIZATION ID

4f7f5937-ad07-4dd3-8860-97dc16db66bf

CREATED

January 4, 2026

API Key

⚠️API keys are shown once at creation - store them securely (secrets manager / env). Session auth uses httpOnly cookies, not localStorage JWTs.

This is where you'll find your API key in the dashboard

Navigate to your Dashboard → Organization Settings to find your API key

Cross-origin (CORS) and credentials

If you call the API from a different origin than the backend (e.g. your app at https://app.example.com calling https://api.loginme.io), and you are not using the LoginMe widget:

  • Cross-origin /api/v1/* is supported. The backend reflects the request Origin in Access-Control-Allow-Origin (it does not use *), so credentialed requests work from any domain.
  • You do not set the Origin header. The browser sends it automatically on cross-origin fetch / XHR.
  • Using cookies (e.g. httpOnly session): Use credentials: 'include' in fetch. The response will include Access-Control-Allow-Origin: <your origin> and Access-Control-Allow-Credentials: true.
  • Allowlisted Origin required: /v1/session/* and /v1/widget/* require your app Origin to be allowlisted for the tenant. Prefer socialPopup: true for social login to avoid third-party cookie issues on customer origins.

For more detail and code examples, see API_EXAMPLES.md in the repo (section: Cross-origin (CORS) and credentials). Running your own LoginMe backend? Ensure CORS reflects the request origin for /api/v1/* and never returns * for routes called with credentials.

🚀 Quick Integration: LoginMe Widget

Recommended path for embeds and social login. The widget establishes an httpOnly cookie session - no JWT is exposed to your page JavaScript. Load the script from https://app.loginme.io/loginme-widget.js.

Method 1: One-Liner (Easiest)

<div id="loginme-root"></div>
<script
  src="https://app.loginme.io/loginme-widget.js"
  loginme-key="your-api-key-here"
  data-container="#loginme-root"
></script>

Requires data-container pointing at a mount element. The widget renders there and manages the httpOnly session for you.

Alternative: Custom Element

<script src="https://app.loginme.io/loginme-widget.js"></script>
<loginme-widget loginme-key="your-api-key-here"></loginme-widget>

Renders into the host element. Session storage is always httpOnly cookies (no localStorage).

Method 2: Function API + socialPopup (Recommended)

<script src="https://app.loginme.io/loginme-widget.js"></script>
<div id="login-container"></div>

<script>
  const widget = LoginMe.init({
    apiKey: 'your-api-key-here',
    mode: 'login', // or 'register'
    theme: 'light', // or 'dark'
    socialPopup: true, // preferred: OAuth in popup via /widget/callback
    onLogin: (result) => {
      // console.log('User logged in:', result.user); // Auto-commented by CI
      // Session is httpOnly cookie; call your APIs with credentials: 'include'
      window.location.href = '/dashboard';
    },
    onError: (error) => {
      // console.error('Login error:', error); // Auto-commented by CI
    }
  });
  
  widget.render('#login-container');
</script>

Social buttons call POST /v1/widget/auth/start. After OAuth, the popup exchanges via POST /v1/widget/auth/callback/exchange(same-site session + CSRF), then postMessages identity to your opener. Prefer popup over redirect to avoid third-party cookie blocks.

Method 3: Programmatic API (No UI)

<script src="https://app.loginme.io/loginme-widget.js"></script>

<script>
  LoginMe.init({ apiKey: 'your-api-key-here', socialPopup: true });

  LoginMe.login('user@example.com', 'password')
    .then(result => {
      // console.log('Logged in:', result.user); // Auto-commented by CI
      // Session is httpOnly cookie; no token exposed to JS.
    })
    .catch(error => {
      // console.error('Login failed:', error); // Auto-commented by CI
    });

  LoginMe.getCurrentUser()
    .then(user => {
      // console.log('Current user:', user); // Auto-commented by CI
    });

  LoginMe.logout()
    .then(() => {
      // console.log('Logged out'); // Auto-commented by CI
    });
</script>

Widget Features

  • Zero dependencies - Pure vanilla JavaScript
  • Secure by default - httpOnly cookie session; fingerprint tracking optional
  • Social login - start + exchange APIs; prefer socialPopup: true
  • Auto session refresh - keeps the cookie session alive
  • Dark mode - Built-in light/dark themes
  • Framework agnostic - Works with React, Vue, Angular, or vanilla JS
  • Mobile responsive - Works on all devices

Widget Configuration Options

LoginMe.init({
  apiKey: 'your-api-key',              // Required
  apiUrl: 'https://api.loginme.io',    // Optional
  mode: 'login',                       // 'login' or 'register'
  theme: 'light',                      // 'light' or 'dark'
  socialPopup: true,                   // Recommended for social OAuth
  widgetCallbackUrl: 'https://loginme.io/widget/callback',
  onLogin: (result) =&gt; { /* result.user */ },
  onError: (error) =&gt; { /* ... */ },
});

Note: For detailed widget documentation, see the Widget Integration Guide.

Setup

// Configuration - store the API key securely (env / secrets), not as a JWT
const API_URL = 'https://api.loginme.io';
const API_KEY = 'ak_your_api_key_here';

function getApiKey() {
  return API_KEY;
}

// All authenticated browser calls must send cookies:
// credentials: 'include'
// Do NOT store access_token / JWT in localStorage or sessionStorage.

1. User Registration

Register a new user in your organization. Use credentials: 'include' so any session cookie is stored by the browser.

async function registerUser(email, password, metadata = {}) {
  try {
    const response = await fetch(`${API_URL}/api/v1/auth/register`, {
      method: 'POST',
      credentials: 'include',
      headers: {
        'X-API-Key': getApiKey(),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email: email,
        password: password,
        metadata: metadata,
        role: 'user',
      }),
    });

    if(!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Registration failed');
    }

    const data = await response.json();
    // Session cookie (if set) is httpOnly - keep user in app state only
    // console.log('User registered:', data.user); // Auto-commented by CI
    return data;
  } catch(error) {
    // console.error('Registration error:', error); // Auto-commented by CI
    throw error;
  }
}

registerUser('user@example.com', 'securePassword123', {
  name: 'John Doe',
  company: 'Acme Corp'
}).then(result =&gt; {
  // console.log('Registration successful:', result.user); // Auto-commented by CI
});

Response:

{
  "user": {
    "id": "user-uuid",
    "email": "user@example.com",
    "role": "user",
    "tenant_id": "org-uuid"
  }
}

2. User Login

Authenticate and establish an httpOnly cookie session. Prefer POST /api/v1/auth/login-secure for tenant apps (legacy tenant path). Identity Control Plane also exposes POST /v1/session/login.

async function loginUser(email, password) {
  try {
    // Tenant app / console pattern (useAuth): login-secure + credentials
    const response = await fetch(`${API_URL}/api/v1/auth/login-secure`, {
      method: 'POST',
      credentials: 'include',
      headers: {
        'X-API-Key': getApiKey(),
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ email, password }),
    });

    if(!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Login failed');
    }

    const data = await response.json();
    // Cookie session is set by the server - store user in memory/state only
    // console.log('Login successful:', data.user); // Auto-commented by CI
    return data;
  } catch(error) {
    // console.error('Login error:', error); // Auto-commented by CI
    throw error;
  }
}

// Identity plane alternative:
// POST ${API_URL}/v1/session/login with credentials: 'include'

loginUser('user@example.com', 'securePassword123')
  .then(result =&gt; {
    // console.log('Logged in:', result.user); // Auto-commented by CI
  });

Response:

{
  "user": {
    "id": "user-uuid",
    "email": "user@example.com",
    "role": "user",
    "tenant_id": "org-uuid"
  }
}

3. Get Current User

Read the current session via cookies - no Bearer JWT in browser JS.

async function getCurrentUser() {
  try {
    // Tenant: GET /api/v1/auth/me - Identity plane: GET /v1/session/me
    const response = await fetch(`${API_URL}/api/v1/auth/me`, {
      method: 'GET',
      credentials: 'include',
      headers: {
        'X-API-Key': getApiKey(),
      },
    });

    if(!response.ok) {
      if(response.status === 401) {
        throw new Error('Session expired. Please login again.');
      }
      const error = await response.json();
      throw new Error(error.error || 'Failed to fetch user');
    }

    const data = await response.json();
    // console.log('Current user:', data); // Auto-commented by CI
    return data;
  } catch(error) {
    // console.error('Error fetching user:', error); // Auto-commented by CI
    throw error;
  }
}

getCurrentUser().then(user =&gt; {
  // console.log('User email:', user.email); // Auto-commented by CI
  // console.log('User role:', user.role); // Auto-commented by CI
});

Response:

{
  "id": "user-uuid",
  "email": "user@example.com",
  "role": "user",
  "tenant_id": "org-uuid",
  "metadata": {
    "name": "John Doe"
  },
  "auth_type": "password",
  "auth_providers": []
}

4. Refresh Session

Rotate the httpOnly session cookie. Identity plane: POST /v1/session/refresh with credentials and CSRF header.

async function refreshSession() {
  // Fetch CSRF for mutating session routes (double-submit)
  const csrfRes = await fetch(`${API_URL}/api/v1/auth/csrf`, {
    credentials: 'include',
    headers: { 'X-API-Key': getApiKey() },
  });
  const { csrf_token } = await csrfRes.json();

  const response = await fetch(`${API_URL}/v1/session/refresh`, {
    method: 'POST',
    credentials: 'include',
    headers: {
      'X-API-Key': getApiKey(),
      'X-CSRF-Token': csrf_token,
      'Content-Type': 'application/json',
    },
  });

  if(!response.ok) {
    if(response.status === 401) {
      throw new Error('Session invalid. Please login again.');
    }
    const error = await response.json();
    throw new Error(error.error || 'Failed to refresh session');
  }

  // Cookie rotated server-side - no JWT to store in JS
  // console.log('Session refreshed'); // Auto-commented by CI
  return response.json().catch(() =&gt; ({}));
}

refreshSession().then(() =&gt; {
  // console.log('Cookie session refreshed'); // Auto-commented by CI
});

Notes:

Refresh mutates credentials and requires X-CSRF-Token. Do not implement JWT expiry timers in the browser for LoginMe console auth.

5. Logout

Clear the server session cookie, then clear client user state only (no localStorage JWT cleanup).

async function logoutUser() {
  try {
    // Identity plane: POST /v1/session/logout (Origin allowlisted; CSRF not required)
    // Tenant legacy: POST /api/v1/auth/logout
    await fetch(`${API_URL}/v1/session/logout`, {
      method: 'POST',
      credentials: 'include',
      headers: {
        'X-API-Key': getApiKey(),
      },
    });
  } catch(error) {
    // console.error('Logout error:', error); // Auto-commented by CI
  } finally {
    // Clear in-memory / React user state only - cookies are cleared by the server
    // console.log('User logged out'); // Auto-commented by CI
  }
}

logoutUser();

6. List Users (Admin)

Get a list of all users in your organization. Requires admin role. Use cookie session + API key (CSRF on mutating routes).

async function listUsers() {
  const response = await fetch(`${API_URL}/api/v1/users`, {
    method: 'GET',
    credentials: 'include',
    headers: {
      'X-API-Key': getApiKey(),
    },
  });

  if(!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to fetch users');
  }

  const data = await response.json();
  // console.log('Users:', data); // Auto-commented by CI
  return data;
}

listUsers().then(users =&gt; {
  // console.log(`Found ${users.length} users`); // Auto-commented by CI
  users.forEach(user =&gt; {
    // console.log(`- ${user.email} (${user.role})`); // Auto-commented by CI
  });
});

7. Get User by ID (Admin)

Get details of a specific user. Requires admin role.

async function getUser(userId) {
  const response = await fetch(`${API_URL}/api/v1/users/${userId}`, {
    method: 'GET',
    credentials: 'include',
    headers: {
      'X-API-Key': getApiKey(),
    },
  });

  if(!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to fetch user');
  }

  return response.json();
}

getUser('user-uuid').then(user =&gt; {
  // console.log('User:', user.email); // Auto-commented by CI
});

8. Create User (Admin)

Create a new user. Requires admin role. Mutating calls should include CSRF when your tenant enforces it.

async function createUser(email, password, metadata = {}, role = 'user') {
  const csrfRes = await fetch(`${API_URL}/api/v1/auth/csrf`, {
    credentials: 'include',
    headers: { 'X-API-Key': getApiKey() },
  });
  const { csrf_token } = await csrfRes.json();

  const response = await fetch(`${API_URL}/api/v1/users`, {
    method: 'POST',
    credentials: 'include',
    headers: {
      'X-API-Key': getApiKey(),
      'X-CSRF-Token': csrf_token,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email,
      password,
      metadata,
      role,
    }),
  });

  if(!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to create user');
  }

  return response.json();
}

createUser('newuser@example.com', 'securePassword123', {
  name: 'Jane Doe',
  department: 'Engineering'
}, 'user').then(user =&gt; {
  // console.log('Created user:', user.email || user.user?.email); // Auto-commented by CI
});

9. Delete User (Admin)

Delete a user from your organization. Requires admin role.

async function deleteUser(userId) {
  const csrfRes = await fetch(`${API_URL}/api/v1/auth/csrf`, {
    credentials: 'include',
    headers: { 'X-API-Key': getApiKey() },
  });
  const { csrf_token } = await csrfRes.json();

  const response = await fetch(`${API_URL}/api/v1/users/${userId}`, {
    method: 'DELETE',
    credentials: 'include',
    headers: {
      'X-API-Key': getApiKey(),
      'X-CSRF-Token': csrf_token,
    },
  });

  if(!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to delete user');
  }

  // console.log('User deleted successfully'); // Auto-commented by CI
  return true;
}

deleteUser('user-uuid').then(() =&gt; {
  // console.log('User deleted'); // Auto-commented by CI
});

10. Invite User (Admin)

Send an invitation email to a user. Requires admin role.

async function inviteUser(email, role = 'user') {
  const csrfRes = await fetch(`${API_URL}/api/v1/auth/csrf`, {
    credentials: 'include',
    headers: { 'X-API-Key': getApiKey() },
  });
  const { csrf_token } = await csrfRes.json();

  const response = await fetch(`${API_URL}/api/v1/users/invite`, {
    method: 'POST',
    credentials: 'include',
    headers: {
      'X-API-Key': getApiKey(),
      'X-CSRF-Token': csrf_token,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email, role }),
  });

  if(!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to invite user');
  }

  return response.json();
}

inviteUser('newuser@example.com', 'user').then(result =&gt; {
  // console.log('Invitation sent:', result); // Auto-commented by CI
});

11. Get Organization Info

Get information about your organization.

async function getOrganizationInfo() {
  try {
    const response = await fetch(`${API_URL}/api/v1/org`, {
      method: 'GET',
      credentials: 'include',
      headers: {
        'X-API-Key': getApiKey(),
      },
    });

    if(!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Failed to fetch organization');
    }

    const data = await response.json();
    // Auto-commented by CI
    // console.log('Organization:', data); // Auto-commented by CI
    return data;
  } catch(error) {
    // Auto-commented by CI
    // console.error('Error fetching organization:', error); // Auto-commented by CI
    throw error;
  }
}

// Usage
getOrganizationInfo().then(org =&gt; {
  // Auto-commented by CI
  // console.log('Organization name:', org.org.name); // Auto-commented by CI
  // Auto-commented by CI
  // console.log('Plan:', org.org.plan); // Auto-commented by CI
  // Auto-commented by CI
  // console.log('User count:', org.org.user_count); // Auto-commented by CI
});

12. Social Login - List Providers

Get list of available social login providers.

async function getSocialProviders() {
  try {
    const response = await fetch(`${API_URL}/api/v1/auth/social/providers`, {
      method: 'GET',
      credentials: 'include',
      headers: {
        'X-API-Key': getApiKey(),
      },
    });

    if(!response.ok) {
      const error = await response.json();
      throw new Error(error.error || 'Failed to fetch providers');
    }

    const data = await response.json();
    // Auto-commented by CI
    // console.log('Social providers:', data); // Auto-commented by CI
    return data;
  } catch(error) {
    // Auto-commented by CI
    // console.error('Error fetching providers:', error); // Auto-commented by CI
    throw error;
  }
}

// Usage
getSocialProviders().then(providers =&gt; {
  // Auto-commented by CI
  // console.log('Available providers:', providers); // Auto-commented by CI
});

13. Social Login - Widget start / exchange

Do not redirect directly to /api/v1/auth/social/... or expect a token in the URL. Use the widget social flow: POST /v1/widget/auth/start then POST /v1/widget/auth/callback/exchange, with socialPopup: true.

// Preferred: let the widget own the OAuth popup
LoginMe.init({
  apiKey: getApiKey(),
  socialPopup: true,
  onLogin: ({ user }) =&gt; {
    // console.log('Social login complete:', user.user_id || user.id); // Auto-commented by CI
  },
});

// Under the hood (do not reinvent unless you must):
// 1) POST /v1/widget/auth/start  → authorize URL
// 2) OAuth completes → popup hits widget/callback
// 3) POST /v1/widget/auth/callback/exchange (credentials + CSRF)
// 4) postMessage identity to opener; httpOnly session cookie set

Note: Prefer popup mode so exchange runs same-site on LoginMe hosts. Redirect mode on a customer origin can fail when browsers block third-party cookies.

Complete Example: User Management Flow

Here's a complete example showing a typical user management flow:

// Complete user management example
async function userManagementExample() {
  try {
    // 1. Login as admin
    const loginResult = await loginUser('admin@example.com', 'password123');
    // Auto-commented by CI
    // console.log('Logged in as:', loginResult.user.email); // Auto-commented by CI
    
    // 2. Get organization info
    const orgInfo = await getOrganizationInfo();
    // Auto-commented by CI
    // console.log('Organization:', orgInfo.org.name); // Auto-commented by CI
    
    // 3. List all users
    const users = await listUsers();
    // Auto-commented by CI
    // console.log(`Total users: ${users.length}`); // Auto-commented by CI
    
    // 4. Create a new user
    const newUser = await createUser(
      'newuser@example.com',
      'securePassword123',
      { name: 'New User' },
      'user'
    );
    // Auto-commented by CI
    // console.log('Created user:', newUser.user.email); // Auto-commented by CI
    
    // 5. Get user details
    const userDetails = await getUser(newUser.user.id);
    // Auto-commented by CI
    // console.log('User details:', userDetails); // Auto-commented by CI
    
    // 6. Get current user info
    const currentUser = await getCurrentUser();
    // Auto-commented by CI
    // console.log('Current user:', currentUser.email); // Auto-commented by CI
    
    // 7. Logout
    await logoutUser();
    // Auto-commented by CI
    // console.log('Logged out'); // Auto-commented by CI
    
  } catch(error) {
    // Auto-commented by CI
    // console.error('Error in user management flow:', error); // Auto-commented by CI
  }
}

Error Handling

All API calls should handle errors appropriately:

async function safeApiCall(apiFunction) {
  try {
    const result = await apiFunction();
    return { success: true, data: result };
  } catch(error) {
    // Auto-commented by CI
    // console.error('API Error:', error); // Auto-commented by CI
    return { 
      success: false, 
      error: error.message || 'Unknown error' 
    };
  }
}

// Usage
const result = await safeApiCall(() =&gt; loginUser('user@example.com', 'password'));
if(result.success) {
  // Auto-commented by CI
  // console.log('Login successful:', result.data); // Auto-commented by CI
} else {
  // Auto-commented by CI
  // console.error('Login failed:', result.error); // Auto-commented by CI
}

Session Management

Browser sessions are httpOnly cookies. Refresh the cookie session when needed - do not run JWT expiry timers in page JavaScript.

// Optional: refresh cookie session periodically (CSRF required)
async function keepSessionAlive() {
  try {
    await refreshSession(); // see Refresh Session section
  } catch(error) {
    // console.error('Session refresh failed:', error); // Auto-commented by CI
    // Redirect to login - clear in-memory user state only
  }
}

// After login-secure / widget onLogin - no JWT storage
loginUser('user@example.com', 'password').then(() =&gt; {
  // Optionally schedule keepSessionAlive(); cookie handles auth
});

logoutUser().then(() =&gt; {
  // Clear React/app user state; server cleared cookies
});

Assertions (FE / BE) - customer backends

After the browser has a LoginMe httpOnly session (widget or login), use a dual path:

  • Profile (UI only): GET /v1/session/me - email / org membership. Never authorize from this alone.
  • Authz: mint a short-lived assertion, send it to your backend as Authorization: LoginMe <jwt>, verify with JWKS.

Mint and GET /v1/session/me require an allowlisted Origin and a session cookie the browser actually sends to api.loginme.io. Do not treat widget onLogin as proof the cookie is present - always dual-path load. See above.

Assertions are not LoginMe console tokens - do not store them in localStorage/sessionStorage, do not put them in URLs or logs, and do not treat them as a substitute for the cookie session. Hold the JWT in memory only for the outbound request. Scheme is LoginMe, not Bearer. XSS on your page can still mint (cookie + API key) - harden XSS; never authorize from the widget alone.

1. Mint assertion (frontend)

CSRF + POST /v1/session/assertion with credentials: 'include'.audience must match what your backend configures as ExpectedAudience. Your app Origin must be allowlisted for the tenant (same as other /v1/session/* routes).

const API_URL = 'https://api.loginme.io';
// Opaque audience string - must match your backend LOGINME_AUDIENCE
const AUDIENCE = 'https://api.yourapp.com';

async function fetchCsrfToken(apiKey) {
  const res = await fetch(`${API_URL}/api/v1/auth/csrf`, {
    method: 'GET',
    credentials: 'include',
    headers: {
      Accept: 'application/json',
      'X-API-Key': apiKey,
    },
  });
  if(!res.ok) throw new Error('CSRF failed');
  const data = await res.json();
  return data.csrf_token;
}

async function mintAssertion(apiKey, audience = AUDIENCE) {
  const csrf = await fetchCsrfToken(apiKey);
  const res = await fetch(`${API_URL}/v1/session/assertion`, {
    method: 'POST',
    credentials: 'include',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
      'X-API-Key': apiKey,
      'X-CSRF-Token': csrf,
    },
    body: JSON.stringify({ audience }),
  });
  if(!res.ok) throw new Error('Assertion mint failed');
  // { assertion, expires_in } - short TTL; no email/name in JWT (opaque ids only).
  // Do not log or persist the assertion string.
  return res.json();
}

2. Call your API (frontend)

Mint a fresh assertion per request (or within TTL), then call your backend. Use scheme LoginMe - Bearer is rejected by ginmw when Scheme is LoginMe.

async function yourApiFetch(apiKey, path, { method = 'GET', body } = {}) {
  const { assertion } = await mintAssertion(apiKey);
  const headers = {
    Accept: 'application/json',
    Authorization: `LoginMe ${assertion}`, // NOT Bearer
  };
  if(body !== undefined) {
    headers['Content-Type'] = 'application/json';
  }
  const res = await fetch(`https://api.yourapp.com${path}`, {
    method,
    headers,
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  if(!res.ok) throw new Error(`API error ${res.status}`);
  if(res.status === 204) return null;
  return res.json();
}

// Authz claims after verify (example): GET /api/me → { sub, tenant_id, roles }
// Prefer not echoing session_id to the browser UI.
const claims = await yourApiFetch(apiKey, '/api/me');

3. Verify on backend (Go + ginmw)

Use loginme/customertrust/ginmw AssertionMiddleware.Reject empty trust config at process start - ginmw skips issuer/audience/tenant checks when those expected values are empty (fail-open). Prefer a jti replay guard for mutating routes within the assertion TTL.

import(
  "fmt"
  "strings"
  "time"
  "loginme/customertrust/ginmw"
  "github.com/gin-gonic/gin"
)

jwksURL := "https://api.loginme.io/.well-known/jwks.json" // or https://api.loginme.io/v1/jwks if JWKS is gated
issuer := "https://api.loginme.io"
audience := "https://api.yourapp.com" // must match mint audience
tenantID := "<your-tenant-uuid>"

// Fail closed: empty env/config must not reach AssertionMiddleware
if strings.TrimSpace(jwksURL) == "" || strings.TrimSpace(issuer) == "" ||
  strings.TrimSpace(audience) == "" || strings.TrimSpace(tenantID) == "" {
  panic(fmt.Errorf("LOGINME trust config incomplete (JWKS/issuer/audience/tenant)"))
}

authMW := ginmw.AssertionMiddleware(ginmw.AssertionMiddlewareConfig{
  JWKSURL:          jwksURL,
  ExpectedIssuer:   issuer,
  ExpectedAudience: audience,
  ExpectedTenantID: tenantID,
  ClockSkew:        time.Minute,
  Scheme:           "LoginMe", // not Bearer
})

api := r.Group("/api", authMW) // add jti replay middleware for POST/DELETE when needed
api.GET("/me", func(c *gin.Context) {
  claims, ok := ginmw.LoginMeAuthClaims(c)
  if !ok {
    c.AbortWithStatusJSON(401, gin.H{"error": "missing claims"})
    return
  }
  // Authorize from verified claims (sub, roles, tenant_id) - never from session/me alone.
  // Do not return email/name (not in assertion). Avoid echoing session_id to browsers.
  c.JSON(200, gin.H{
    "sub":       claims["sub"],
    "tenant_id": claims["tenant_id"],
    "roles":     claims["roles"],
  })
})

4. Fail closed (dual path)

Do not trust widget onLogin as authority (UI trigger only). Load profile and authz claims in parallel; require both and bind profile.user_id === claims.sub. Use email from session/me for display only - never for authz.

async function fetchSessionMe(apiKey) {
  const res = await fetch(`${API_URL}/v1/session/me`, {
    credentials: 'include',
    headers: { Accept: 'application/json', 'X-API-Key': apiKey },
  });
  if(!res.ok) throw new Error('session/me failed');
  return res.json(); // { user_id, email, role, tenant_id, ... } - UI identity only
}

async function loadSessionSurfaces(apiKey) {
  const [profileSettled, claimsSettled] = await Promise.allSettled([
    fetchSessionMe(apiKey),
    yourApiFetch(apiKey, '/api/me'),
  ]);
  if(profileSettled.status !== 'fulfilled' || claimsSettled.status !== 'fulfilled') {
    return { ok: false }; // fail closed
  }
  const profile = profileSettled.value;
  const claims = claimsSettled.value;
  if(!claims?.sub) return { ok: false };
  // Prefer user_id from session/me; identity_id/sub are shape fallbacks only
  const userId = profile.user_id || profile.identity_id || profile.sub;
  if(!userId || String(userId) !== String(claims.sub)) {
    return { ok: false }; // subject mismatch - refuse signed-in UI
  }
  return { ok: true, profile, claims };
}

// Widget: onLogin only triggers dual-path load - never authorize from the callback alone
LoginMe.init({
  apiKey,
  socialPopup: true,
  onLogin: async() =&gt; {
    const surfaces = await loadSessionSurfaces(apiKey);
    if(!surfaces.ok) { /* show error; stay signed out */ return; }
    // UI from surfaces.profile (email); authz from surfaces.claims (sub/roles)
  },
});

Mint response: { assertion, expires_in }. TTL is typically a few minutes. Full working reference: the Acme Notes integrator demo (loginme-integrator-demo).

Notes

  • API Key: Always include X-API-Key where required; store keys securely (shown once at creation)
  • Browser sessions: Use httpOnly cookies with credentials: 'include' - never put LoginMe JWTs in localStorage/sessionStorage
  • Assertions: Mint via POST /v1/session/assertion + CSRF with matching audience; call your API as Authorization: LoginMe <jwt> (not Bearer); verify with ginmw + /.well-known/jwks.json; reject empty issuer/audience/tenant at startup; never store or log the assertion JWT
  • Error Responses: All errors return JSON with an error field
  • HTTPS: Always use HTTPS in production
  • Rate Limiting: Be aware of rate limits on your plan