venm-auth
v1.6.2
Published
React authentication SDK for Venm — OAuth, session management, React hooks & components
Maintainers
Readme
venm-auth
Full-stack authentication SDK for Venm — integrate Google & Facebook OAuth, passwordless Phone & SMS/WhatsApp OTP login, and secure Express API route protection middleware into any React & Node.js application with minimal code.
Key Features
- 🔐 Multi-Provider OAuth: Turnkey Google (Web & Native Capacitor One-Tap) and Facebook OAuth login flows.
- 📱 Passwordless Phone & SMS/WhatsApp OTP: Comprehensive server routes for one-time code verification, mobile number login, and 2FA workflows.
- 🛡️ Express Route Protection & Security Middleware: Turnkey JWT verification (
createAuthMiddleware), route guards (requireAuth), CSRF state validation, and API rate limiting. - 🗄️ Universal Database Adapters: Built-in MongoDB adapter with automatic
id/_iddocument normalization and refresh token TTL cleanup. - ⚡ Real-Time Event Hooks: Global event emitters for login lifecycle notifications and automated referral commission attribution.
Table of Contents
- Key Features
- What's New
- Installation
- Client Implementation
- Server Implementation & Production Guide
- OAuth Flow
- Server API Endpoints
- Components
- Hooks
- Cross-Origin-Opener-Policy (COOP) Handling
- Security
- Local Development
- Error Handling
- TypeScript
- Package Exports
- Database Adapters
What's New
🪪 Universal ID & ObjectId Normalization (v1.4.1)
The MongoDB adapter (createMongoDBAdapter) now provides universal document ID normalization, eliminating collision and lookup failures between MongoDB ObjectIds (_id) and application UUIDs (id):
- All database operations (
findUserById,updateUser,createSession,findSessionByToken) safely check bothidand_idfilters. - Document serializers (
toUser,toSession) consistently guarantee a top-level.idstring across all returned entities. - Added native collection safety wrappers (
getCol) so custom adapters and test mocks can interact with extended collections without runtimegetNativeCollectionundefined errors.
🎨 Tailwind CSS Native Components (v1.4.1)
- All
venm-authUI components (<GoogleButton>,<FacebookButton>,<VenmAuth>, etc.) are now natively built with Tailwind CSS. - Inline styles have been entirely removed, making every component 100% customizable via the
classNameprop using standard Tailwind utility classes. - (Note: Consumers must add
venm-authto theirtailwind.configcontentarray for classes to compile).
🛡️ Mongoose 7/8+ & Modern Driver Compatibility (v1.4.1)
- Fully upgraded query operations across Express backends to use modern syntax (
returnDocument: 'after'), eliminating Mongoose deprecation warnings. - Expanded
ServerSessionandCreateSessionDatatypes to natively capture client security metadata:userAgent,ipAddress, anddeviceInfo. - Strengthened TypeScript declarations for 2FA/TOTP (
otplib) and optional user status properties (roles,status,is2faEnabled).
📱 Enhanced Native Capacitor One Tap
The Google One Tap integration for Capacitor has been significantly improved:
- Simplified Client IDs: You no longer need to provide separate Android and iOS Client IDs in your config! The SDK and Server now universally use your single Web Application Client ID for initialization and token audience verification. Native platform validation is handled securely by Google Play Services (via SHA-1) and iOS (via URL schemes).
- Flexible Native Flows: The
<GoogleButton>component now accepts anativeFlowprop to customize the native Google Sign-in experience:"autoOrOneTap"(default): Attempts silent auto sign-in, falling back to the One Tap bottom sheet."oneTap": Forces the One Tap bottom sheet to appear."nativeButton": Triggers the traditional full-screen Google Sign-In prompt.- All native flows include an automatic, safe fallback to the browser-based OAuth popup if the Capacitor plugin is unavailable or fails!
🛡️ React 18 StrictMode Safety
Session initialization now uses a cancellation flag to safely handle React 18 StrictMode's double-mount behavior in development. Previously, two parallel initialize() calls could race — the first would rotate tokens on the server, the second would fail with SESSION_NOT_FOUND, clear localStorage, and log the user out. The cancellation flag ensures stale callbacks from unmounted effects are ignored.
🔁 Concurrent Refresh Guard
SessionService.refreshSession() now returns the same in-flight promise for all concurrent callers. Whether triggered by StrictMode double-mount or rapid manual refreshes, duplicate calls share a single promise instead of racing and corrupting token rotation.
⏰ Session Expiry Callback
When auto-refresh fails (e.g., expired refresh token), the SDK now fires an onRefreshFailed callback that:
- Clears the session from localStorage
- Dispatches
UNAUTHENTICATEDwith aSESSION_EXPIREDerror code - Provides a clean path for redirecting users back to the login screen
⚡ Extended Token Refresh Margin
The auto-refresh margin has been increased from 60 seconds → 120 seconds before token expiry, giving more headroom for network latency and server-side token rotation.
🗄️ Refresh Token Expiry in Database
Added refreshExpiresAt field to the ServerSession and CreateSessionData types. The MongoDB adapter now stores this field, enabling native TTL index cleanup of expired refresh tokens. The server also supports passing accessTokenExpiresIn and refreshTokenExpiresIn through the session config to override default token lifetimes.
🐛 Bug Fixes
- Memory adapter token rotation: Fixed the demo server's memory adapter to properly clean up old token keys when tokens are rotated during refresh. Previously, stale keys would accumulate and shadow updated sessions.
- Cleaner dev experience: The demo app now includes an auth event log, live access token countdown, manual refresh button, provider/layout toggles, and improved profile badges.
Installation
👋 Quick start:
bun add venm-auth(orpnpm,npm,yarn) — React 18+, Node.js 18+ required.
Prerequisites
| Requirement | Version |
|-------------|---------|
| Node.js | >= 18 |
| React | ^18.2.0 (peer dependency) |
| React DOM | ^18.2.0 (peer dependency) |
Client-side
# bun (recommended)
bun add venm-auth
# pnpm
pnpm add venm-auth
# npm
npm install venm-auth
# yarn
yarn add venm-authServer-side (Express)
The server package is bundled within venm-auth — no additional install needed.
import { createVenmAuth, createMongoDBAdapter } from "venm-auth/server";Environment Variables
Create a .env file in your server project:
# Google OAuth
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
# Facebook OAuth
FACEBOOK_APP_ID=your-app-id
FACEBOOK_APP_SECRET=your-app-secret
# JWT signing key (min 32 characters)
JWT_SECRET=your-256-bit-secret-key-change-me
# MongoDB (if using the built-in adapter)
MONGODB_URI=mongodb://localhost:27017/myapp💡 Tip: The JWT secret must be at least 32 characters long. You can generate one with
openssl rand -base64 32.
Client Implementation
import { VenmProvider, VenmAuth, Authenticated, Unauthenticated, Loading, useAuth } from "venm-auth";
import type { SDKConfig, ProviderType, Layout } from "venm-auth";
// ── Configuration ───────────────────────────────────────────────────
const config: SDKConfig = {
// Base URL of your Express auth server (required)
// Development: "http://localhost:3000/api/auth"
// Production: "/api/auth"
apiUrl: "http://localhost:3000/api/auth",
// SDK environment
// "development" — enables verbose logging
// "production" — (default) minimal logging
environment: "development",
// Automatically refresh access tokens before they expire (default: true)
autoRefresh: true,
// Persist session to localStorage (default: true)
persistSession: true,
// Storage mechanism for session persistence (default: "localStorage")
// "localStorage" — survives tab close
// "sessionStorage" — cleared on tab close
storage: "localStorage",
// HTTP request timeout in milliseconds (default: 10000)
timeout: 10000,
// Optional override for OAuth callback URI (defaults on server to "/api/auth/google/callback" or "/api/auth/facebook/callback")
redirectUri: "http://localhost:3000/api/auth/google/callback",
// OAuth provider credentials (required — at least one provider)
oauth: {
google: {
clientId: "your-google-client-id.apps.googleusercontent.com",
},
facebook: {
appId: "your-facebook-app-id",
},
},
};
// ── App Root ─────────────────────────────────────────────────────────
export default function App() {
return (
<VenmProvider
config={config}
// Called whenever auth state changes (login, logout, token refresh)
onAuthStateChange={(state) => {
console.log("[auth] State:", state);
}}
>
<AppContent />
</VenmProvider>
);
}
// ── App Content ──────────────────────────────────────────────────────
function AppContent() {
const { logout } = useAuth();
return (
<div>
{/* Shows a loading spinner while the session initializes */}
<Loading>
<p>Loading session...</p>
</Loading>
{/* Shown when the user is NOT authenticated */}
<Unauthenticated>
{/* Renders OAuth provider buttons */}
{/* layouts: "vertical" | "horizontal" | "card" | "minimal" */}
<VenmAuth
providers={["google", "facebook"] as ProviderType[]}
layout="card" // (default: "vertical")
showDivider={false} // Show "or" divider between buttons
/>
</Unauthenticated>
{/* Shown when the user IS authenticated */}
<Authenticated>
<h1>Welcome!</h1>
<button onClick={() => logout()}>Sign Out</button>
</Authenticated>
</div>
);
}🎨 Styling (Tailwind CSS)
All venm-auth components are built natively with Tailwind CSS and accept a className prop for seamless customization. To ensure the default styles compile correctly in your application, you must include the package in your tailwind.config's content array:
// tailwind.config.ts
export default {
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/venm-auth/**/*.{ts,tsx,js,jsx}", // <-- Add this line
"../../node_modules/venm-auth/**/*.{ts,tsx,js,jsx}" // (If using monorepos/workspaces)
],
// ...
};You can then override or extend styles simply by passing classes:
<GoogleButton className="!bg-black !text-white hover:opacity-80 rounded-full" />Server Implementation & Production Guide
The server package (venm-auth/server) provides modular routers, database adapters, authentication middleware, and real-time event hooks to power your full-stack backend.
1. Core Router Initialization
Mount the primary authentication router to handle Google/Facebook OAuth redirect flows, callback token exchanges, session management, and user creation:
import express from "express";
import { createVenmAuth, createMongoDBAdapter } from "venm-auth/server";
import type { VenmAuthConfig } from "venm-auth/server";
const app = express();
app.use(express.json());
// ── Database Adapter ────────────────────────────────────────────────
const database = createMongoDBAdapter({
uri: process.env.MONGODB_URI!,
databaseName: "myapp_db",
});
// ── Configuration ───────────────────────────────────────────────────
const authConfig: VenmAuthConfig = {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
facebook: {
appId: process.env.FACEBOOK_APP_ID!,
appSecret: process.env.FACEBOOK_APP_SECRET!,
},
jwtSecret: process.env.JWT_SECRET!, // Minimum 32 characters
database,
session: {
accessTokenExpiresIn: "15m",
refreshTokenExpiresIn: "30d",
},
prefix: "/api/auth",
allowedOrigins: ["http://localhost:3000", "https://yourdomain.com"],
// Natively handle multi-level referrals and attribution
referral: {
enabled: true,
maxLevels: 3,
requireAdminApproval: true,
},
events: {
onReferralApplied: async ({ referrer, referee, level }) => {
console.log(`Referral applied! ${referee} joined under ${referrer} (level ${level})`);
},
onCommissionEarned: async ({ referrerId, refereeId, amount, level }) => {
console.log(`User ${referrerId} earned ${amount} from ${refereeId} (level ${level})`);
// e.g. await creditWallet(referrerId, amount, 'REFERRAL_COMMISSION');
},
onOtpRequested: async (phone, code) => {
// Integrate with Twilio, AWS SNS, or WhatsApp Business API
console.log(`Sending OTP ${code} to ${phone}`);
}
}
};
// ── Mount Auth Router ───────────────────────────────────────────────
app.use("/api/auth", createVenmAuth(authConfig));2. Authentication & Route Protection Middleware
venm-auth/server exports ready-to-use Express middleware to protect private API endpoints, enforce role-based access control (RBAC), and defend against abuse:
| Middleware / Helper | Type | Description |
|---------------------|------|-------------|
| createAuthMiddleware({ jwtSecret, database }) | Factory | Resolves Bearer JWT from Authorization header, verifies signature, loads user from database adapter, and populates req.user and req.session. |
| requireAuth | Middleware | Enforces authentication; immediately rejects unauthenticated requests with HTTP 401 Unauthorized. |
| stateCookieMiddleware | Middleware | Sets secure, httpOnly, same-site CSRF state cookies during OAuth initiation. |
| validateState | Middleware | Validates OAuth callback parameters against stored CSRF state cookies to prevent login forgery. |
| oauthRateLimiter | Middleware | Rate limits OAuth token exchange endpoints (default: 10 requests / minute / IP). |
| resultRateLimiter | Middleware | Rate limits COOP polling endpoints (default: 30 requests / minute / session). |
| errorHandler | Middleware | Catches and standardizes all VenmAuthError, CSRF, and rate limit errors into a consistent JSON response. |
Example: Protecting Private Routes & Enforcing Roles
import { createAuthMiddleware, requireAuth } from "venm-auth/server";
import type { Request, Response, NextFunction } from "express";
const auth = createAuthMiddleware({
jwtSecret: process.env.JWT_SECRET!,
database,
});
// Admin role check helper
function requireAdmin(req: Request, res: Response, next: NextFunction) {
if (!req.user || !req.user.roles?.includes("ADMIN")) {
return res.status(403).json({ error: { message: "Admin access required" } });
}
next();
}
// Secure admin endpoint
app.get("/api/admin/users", auth, requireAuth, requireAdmin, async (req, res) => {
const users = await database.findUsers({});
res.json({ users });
});3. Real-Time Event Hooks & Referral Attribution
venm-auth/server exports a global event emitter (venmAuthEvents) that allows your application services to react asynchronously to authentication lifecycle and referral program events without coupling core auth logic:
| Event Name | Payload | Description |
|------------|---------|-------------|
| user:created | (user: User) | Emitted when a new user registers |
| user:deleted | ({ id, _id, email, phone }) | Emitted when a user account is deleted |
| user:login | (user: User, device?: Device) | Emitted when a user successfully logs in |
| user:logout | (user: User) | Emitted when a user logs out |
| referral:applied | (referral: Referral) | Emitted when a referral code is applied to a user |
| referral:commission | ({ referrerId, refereeId, level, amount }) | Emitted when a referrer earns commission |
| referral:trigger_distribution | ({ refereeId, baseAmount }) | Triggers distribution of multi-level commissions |
import { venmAuthEvents } from "venm-auth/server";
// Listen for successful user login/registration
venmAuthEvents.on("user:login", (user, device) => {
console.log(`User ${user.email} logged in on device ${device?.userAgent}`);
});
// Listen for referral commission attribution (e.g. credit user wallet natively)
venmAuthEvents.on("referral:commission", async ({ referrerId, refereeId, amount, level }) => {
console.log(`Referral reward: User ${referrerId} earned $${amount} from level ${level} referral!`);
// Example: creditWallet(referrerId, amount);
});4. Phone & SMS OTP Authentication
For mobile or passwordless SMS/WhatsApp workflows, mount the supplementary phone authentication routes:
import { createPhoneAuthRoutes } from "venm-auth/server";
app.use("/api/auth/phone", createPhoneAuthRoutes({
database,
jwtSecret: process.env.JWT_SECRET!,
sendOtp: async (phone, otp) => {
// Integrate with Twilio, AWS SNS, or WhatsApp Business API
console.log(`Sending OTP ${otp} to ${phone}`);
},
}));Client Workflow
When createPhoneAuthRoutes({ database, jwtSecret, sendOtp }) is mounted to /api/auth/phone, your client application can authenticate users via SMS or WhatsApp without passwords:
- Request OTP: Client sends
POST /api/auth/phone/request-otpwith{ "phone": "+1234567890" }. The server stores a hashed OTP and calls yoursendOtpcallback. - Verify OTP: Client sends
POST /api/auth/phone/verify-otpwith{ "phone": "+1234567890", "code": "123456" }. Upon success, the server returns JWT access/refresh tokens and the authenticated user object.
OAuth Flow
- User clicks a provider button (Google/Facebook)
- A popup opens to your Express server's OAuth authorize endpoint (
GET /google?state=...&code_challenge=...&auth_session_id=...) - The server sets a signed CSRF state cookie, stores a state-to-session mapping, and redirects to the provider's consent screen
- The user authenticates with the chosen provider
- The provider redirects back to your server's callback endpoint (
GET /google/callback) - The server validates the state cookie against the returned state parameter, then stores the authorization code server-side keyed by the
auth_session_id(see COOP Handling below) - The main page polls
GET /result/:authSessionIdevery 2 seconds until the code is available - The popup closes; the SDK exchanges the code for tokens via
POST /google - The server exchanges the code with the provider, creates/updates the user in the database, generates JWT tokens, and stores the session
- The user and session are stored in React state and localStorage
- The
onAuthStateChangecallback fires with the new auth state
Why server-side storage? Some OAuth providers (notably Google) set
Cross-Origin-Opener-Policy: same-originon their consent pages, which severs thewindow.openerreference in the popup — makingpostMessagesilently fail. Instead, the callback stores the OAuth result on the server, and the main page pollsGET /result/:authSessionIdto retrieve it. See the COOP section for details.
Ecosystem Integration (Admin & Consumer Monorepos)
venm-auth natively supports multi-app monorepos where you have separate Consumer and Admin applications that share the same backend but require isolated authentication states and distinct route protection.
1. Backend Setup (Dual Routers)
Mount two isolated routers on your Express server. One handles OAuth and Consumer logic, the other handles Admin Email/Password authentication.
import { createVenmAuth, createAdminUsersRoutes } from "venm-auth/server";
// 1. Consumer Router (OAuth, Phone, Public Users)
app.use("/api/v1/auth", createVenmAuth({
google: { clientId: "...", clientSecret: "..." },
jwtSecret: process.env.JWT_SECRET!,
database,
prefix: "/api/v1/auth",
}));
// 2. Admin Router (Email/Password, RBAC)
app.use("/api/v1/admin/auth", createAdminUsersRoutes({
database,
jwtSecret: process.env.JWT_SECRET!,
// Optional: override JWT lifetimes for admins
session: { accessTokenExpiresIn: "1h", refreshTokenExpiresIn: "1d" },
}));2. Main Consumer App Setup
In your main React app, initialize the provider pointing to your Consumer endpoint and use the standard useAuth hook.
// main-app/src/App.tsx
import { VenmProvider, VenmAuth, Authenticated, Unauthenticated } from "venm-auth";
export default function App() {
return (
<VenmProvider config={{ apiUrl: "/api/v1/auth", environment: "production" }}>
<Unauthenticated>
<VenmAuth providers={["google", "facebook"]} />
</Unauthenticated>
<Authenticated>
<Dashboard />
</Authenticated>
</VenmProvider>
);
}3. Admin App Setup
In your separate admin React app, initialize the provider pointing to your Admin endpoint. Exclusively use the useAdminAuth hook and Admin components (<AdminLoginForm>, <AdminRegisterForm>). Running your Admin app on a different subdomain (e.g., admin.domain.com) inherently prevents localStorage token conflicts.
// admin-app/src/App.tsx
import { VenmProvider, AdminLoginForm, useAdminAuth } from "venm-auth";
function AdminRoot() {
return (
<VenmProvider config={{ apiUrl: "/api/v1/admin/auth", environment: "production" }}>
<AdminRouter />
</VenmProvider>
);
}
function AdminLogin() {
const { session } = useAdminAuth();
if (session) return <Navigate to="/dashboard" />;
return (
<div className="max-w-sm mx-auto mt-20">
<h1 className="text-2xl font-bold mb-4">Admin Portal</h1>
<AdminLoginForm onSuccess={() => console.log("Admin Logged In!")} />
</div>
);
}Integration Guides
venm-auth is designed to drop seamlessly into a variety of architectures and frameworks. Below are specific guides for integrating with common tools.
1. Capacitor & Native iOS/Android (Google One Tap/Native)
For hybrid mobile applications built with Ionic Capacitor, venm-auth handles the complexity of Native Google Sign-In and Apple Sign-In.
- One Client ID: You only need your Web Application Client ID. Do NOT configure separate iOS or Android Client IDs in the
VenmAuthConfig. Native client validation is handled securely by Google Play Services and iOS URL schemes. - Native Flow Selection: The
<GoogleButton>accepts anativeFlowprop:"autoOrOneTap": Attempts silent auto sign-in, falling back to One Tap bottom sheet."oneTap": Forces the One Tap bottom sheet."nativeButton": Full-screen native Google Sign-In prompt.
- Fallback: If the native Capacitor plugin fails or the app is running in a web browser, it safely falls back to the standard web OAuth popup.
2. Phone & WhatsApp OTP Integration (Twilio/Infobip)
Integrate passwordless SMS or WhatsApp login by mounting the Phone Auth routes on your Express server and providing a sendOtp callback.
import { createPhoneAuthRoutes } from "venm-auth/server";
import twilio from "twilio";
const client = twilio(process.env.TWILIO_SID, process.env.TWILIO_AUTH_TOKEN);
app.use("/api/auth/phone", createPhoneAuthRoutes({
database,
jwtSecret: process.env.JWT_SECRET!,
sendOtp: async (phone, otp) => {
// Send SMS via Twilio
await client.messages.create({
body: `Your verification code is: ${otp}`,
from: process.env.TWILIO_PHONE_NUMBER,
to: phone
});
},
}));3. Frontend Frameworks: Next.js SSR vs Vite React SPA
Vite React SPA
For standard Single Page Applications, simply wrap your root component in <VenmProvider>. Authentication state is managed client-side in React State and localStorage.
Next.js App Router (SSR)
If using Next.js App Router, venm-auth components should be placed inside Client Components (files marked with "use client"). Server-side route protection should be handled by Next.js middleware by verifying the JWT token stored in cookies (if you configure your Express backend to set httpOnly cookies) or by passing the Bearer token manually to Next.js API routes.
4. Admin vs Consumer Monorepo Architectures
venm-auth natively supports multi-app monorepos with separate Consumer and Admin portals.
- Dual Routers: Mount two isolated routers on your Express server (
/api/v1/authfor Consumers,/api/v1/admin/authfor Admins). - Isolated State: Run your Admin app on a separate subdomain (e.g.,
admin.domain.com). This completely isolateslocalStoragetokens. - Admin Hooks: Use the dedicated
useAdminAuth()hook and<AdminLoginForm>component in your Admin app.
5. Referral Program & Gamification
venm-auth includes a built-in multi-level referral attribution engine. By enabling the referral config on the server, you can listen to real-time events to power gamification.
const authConfig = {
// ...
referral: {
enabled: true,
maxLevels: 3,
requireAdminApproval: true,
},
events: {
onCommissionEarned: async ({ referrerId, refereeId, amount, level }) => {
// Reward the referrer in your core gamification/economy service!
await walletService.creditUser(referrerId, amount, { category: 'referral_bonus' });
}
}
};Server API Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | /google | Initiate Google OAuth redirect (sets CSRF cookie) |
| GET | /google/callback | Google OAuth callback (validates CSRF cookie, stores result server-side) |
| POST | /google | Exchange Google auth code for JWT tokens |
| POST | /google/onetap | Verify Google One-Tap / Capacitor ID token and generate JWT tokens |
| GET | /facebook | Initiate Facebook OAuth redirect (sets CSRF cookie) |
| GET | /facebook/callback | Facebook OAuth callback (validates CSRF cookie, stores result server-side) |
| POST | /facebook | Exchange Facebook auth code for JWT tokens |
| POST | /phone/request-otp | Send a one-time verification code (OTP) via SMS or WhatsApp |
| POST | /phone/verify-otp | Verify OTP and authenticate/create user profile (accepts optional referralCode body field) |
| POST | /2fa/verify | Verify TOTP or WhatsApp 2FA code during login |
| POST | /register-device | Register a new device session (accepts optional referralCode body field) |
| POST | /referral/apply | Manually apply a referral code to the authenticated user |
| GET | /result/:authSessionId | Poll for OAuth auth result (bypasses COOP-broken postMessage) |
| GET | /session | Verify and return current session (requires Bearer token) |
| POST | /refresh | Refresh access token using refresh token |
| GET | /user | Return current user (requires Bearer token) |
| POST | /logout | Destroy current session |
| POST | /logout/all | Destroy all sessions for the user |
| GET | /health | Health check |
Components
VenmProvider
The root component. Must wrap all authentication-using code.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| config | SDKConfig | — | SDK configuration (apiUrl, oauth, environment, etc.) |
| onAuthStateChange | (state: AuthState) => void | — | Called when auth state changes |
VenmAuth
Renders OAuth provider buttons in a configurable layout.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| providers | ProviderType[] | ["google", "facebook"] | Which provider buttons to show |
| layout | Layout | "vertical" | "vertical", "horizontal", "card", or "minimal" |
| showDivider | boolean | false | Show "or" divider between buttons |
GoogleButton / FacebookButton
Standalone provider buttons.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| onClick | () => void | — | Override default login handler |
| disabled | boolean | false | Disable the button |
| loading | boolean | false | Show loading spinner |
| children | ReactNode | — | Custom button label |
Authenticated / Unauthenticated / Loading
Conditional rendering components.
<Authenticated fallback={<LoginPage />}>
<Dashboard />
</Authenticated>
<Unauthenticated>
<LoginPage />
</Unauthenticated>
<Loading>
<p>Loading...</p>
</Loading>Hooks
| Hook | Returns | Description |
|------|---------|-------------|
| useAuth() | { user, session, loading, error, login, logout, refresh } | Full authentication state and methods |
| useUser() | { user, loading } | Current user only |
| useSession() | { accessToken, refreshToken, expiresAt, loading } | Session tokens only |
| useLogin() | { login, loading, error } | Login method with loading state |
| useLogout() | { logout, loading } | Logout method with loading state |
Cross-Origin-Opener-Policy (COOP) Handling
The Problem
Google's OAuth consent screen sets the Cross-Origin-Opener-Policy: same-origin HTTP header on its pages. This severs the window.opener reference in the popup, so window.opener.postMessage() silently fails — the popup closes but the main page never receives the auth code.
Facebook does not set COOP headers, so
postMessageworks for Facebook logins. The fallback handles both cases transparently.
The Solution: Server-Side Result Relay
The SDK uses a dual-delivery mechanism:
- Fast path (
postMessage) — The callback HTML always attemptswindow.opener.postMessage(). Resolves instantly when it works (e.g., Facebook). - Fallback path (polling) — In parallel, the client polls
GET /result/:authSessionIdevery 2 seconds to retrieve the OAuth result. Ensures the flow works even when COOP severs the opener.
Whichever path resolves first wins.
Architecture
Main Page Express Auth Server
│ │
├── 1. Open popup + auth_session_id ──▶ │
│ │
├── 2. Poll GET /result/:id ──▶ │
│ (every 2s, returns │
│ { status: "PENDING" }) │
│ ◀────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ Google │ │
│ │ Consent │───┼── 3. POST /google/callback ──▶ storeResult(state, code)
│ │ Screen │ │
│ └──────────────┘ │
│ │
├── 4. Next poll: { code, state } ◀─┤
│ │
├── 5. POST /google (exchange) ──▶ │
│ │The OauthResultStore
An in-memory store that maps OAuth state parameters to client-generated authSessionId values:
| Step | Action | Description |
|------|--------|-------------|
| Authorization request | setStateMapping(state, authSessionId) | Stores state → authSessionId |
| OAuth callback | storeResult(state, code) | Looks up authSessionId from state, stores result |
| Client polling | getResult(authSessionId) | Returns result and deletes it (one-time retrieval) |
| Error callback | storeError(state, errorMessage) | Stores error so the client receives it promptly |
- TTL: Results expire after 5 minutes (cleanup runs every 60 seconds)
- One-time retrieval: Once read, the result is immediately deleted
- No console noise: Returns
200 { status: "PENDING" }while waiting (not 404)
Rate Limiting
Dedicated resultRateLimiter — 30 requests per minute per authSessionId (compared to 10/min for other OAuth endpoints), matching the 2-second polling interval.
Security
- PKCE (Proof Key for Code Exchange) for Google OAuth — protects against authorization code interception
- Cookie-based CSRF state — state parameter stored in a signed, httpOnly, short-lived cookie; validated on callback
- Server-side result storage — auth codes stored in memory with 5-minute TTL; retrieved once and immediately deleted
- Popup origin validation — only accepts messages from the expected origin
- Token refresh margin — refreshes tokens 60 seconds before expiry
- Rate limiting — built-in limiters for OAuth (10/min), result polling (30/min), and session (30/min) endpoints
Local Development
1. Install dependencies
# bun (recommended)
bun install
# pnpm
pnpm install
# npm
npm install
# yarn
yarn install2. Available development scripts
# Type-check
bun run typecheck # or: pnpm typecheck | npm run typecheck | yarn typecheck
# Run tests
bun test # or: pnpm test | npm test | yarn test
# Build
bun run build # or: pnpm build | npm run build | yarn build
# Watch mode
bun run dev # or: pnpm dev | npm run dev | yarn devSupported localhost origins:
http://localhost:3000http://localhost:5173http://localhost:4173
You can also run the included demo app:
cd examples/venm-auth-demo
# Create a .env file with your OAuth credentials
# bun
bun install && bun run dev
# pnpm
pnpm install && pnpm dev
# npm
npm install && npm run dev
# yarn
yarn install && yarn devError Handling
Client-side error codes:
| Code | Description |
|------|-------------|
| POPUP_BLOCKED | Browser blocked the popup |
| POPUP_CLOSED | User closed the popup |
| POPUP_TIMEOUT | Authentication timed out |
| STATE_MISMATCH | OAuth state parameter mismatch (potential CSRF) |
| PROVIDER_ERROR | OAuth provider returned an error |
| UNAUTHORIZED | Invalid or expired token |
| TIMEOUT | HTTP request timed out |
| NO_REFRESH_TOKEN | No refresh token available for session refresh |
| SESSION_EXPIRED | Auto-refresh failed — session expired and has been cleared |
Server-side error codes:
| Code | Description |
|------|-------------|
| MISSING_CODE | Authorization code missing from request |
| TOKEN_EXCHANGE_FAILED | OAuth token exchange with provider failed |
| USER_CREATE_FAILED | Failed to create or update user |
| MISSING_TOKEN | No Bearer token in request |
| SESSION_NOT_FOUND | Session not found in database |
| USER_NOT_FOUND | User not found in database |
| INVALID_TOKEN | Access token is invalid or expired |
| INVALID_REFRESH_TOKEN | Refresh token is invalid or expired |
| MISSING_REFRESH_TOKEN | Refresh token missing from request |
| CSRF_INVALID_STATE | CSRF state parameter missing or mismatch |
| RATE_LIMIT_EXCEEDED | Too many requests |
TypeScript
import type { User, Session, AuthState, ProviderType, SDKConfig, OAuthConfig } from "venm-auth";
import type { DatabaseAdapter, ServerSession, VenmAuthConfig } from "venm-auth/server";Package Exports
| Import Path | Contents |
|-------------|----------|
| venm-auth | React components, hooks, context, services, types |
| venm-auth/server | Express router, JWT utilities, database adapters, middleware |
| venm-auth/components | React components only |
| venm-auth/hooks | React hooks only |
Database Adapters
MongoDB (built-in)
import { createMongoDBAdapter } from "venm-auth/server";
const db = createMongoDBAdapter({
uri: "mongodb://localhost:27017/myapp",
databaseName: "myapp-auth",
});The adapter creates a TTL index on refreshExpiresAt so expired sessions are automatically cleaned up by MongoDB. The access token expiry (expiresAt) is not TTL-indexed — sessions remain valid until the refresh token expires, allowing auto-refresh to revive them.
Custom Adapters
Implement the DatabaseAdapter interface for PostgreSQL, SQLite, Redis, etc.:
import type { DatabaseAdapter, CreateSessionData } from "venm-auth/server";
const myAdapter: DatabaseAdapter = {
async createSession(data: CreateSessionData) {
// data includes:
// accessToken, refreshToken, expiresAt,
// refreshExpiresAt ← absolute timestamp for cleanup
// ...
},
// ... implement all required methods
};Note: The
refreshExpiresAtfield was added in v1.2.0 — custom adapters must include it inCreateSessionDatafor proper session cleanup.
License: MIT
