diff --git a/.biomeignore b/.biomeignore
new file mode 100644
index 0000000..849ddff
--- /dev/null
+++ b/.biomeignore
@@ -0,0 +1 @@
+dist/
diff --git a/README.md b/README.md
index 8639786..7df599e 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,17 @@ A headless, isomorphic authentication toolkit that runs seamlessly across server
- [3️⃣ Configure Server](#3️⃣-configure-server)
- [4️⃣ Set up Auth Client and React Integration](#4️⃣-set-up-auth-client-and-react-integration)
- [Architecture](#architecture)
+ - [Middleware Architecture](#middleware-architecture)
+- [Account Linking](#account-linking)
+ - [Provider-Consumer Model](#provider-consumer-model)
+ - [Account Linking Flow](#account-linking-flow)
+ - [Implementation](#implementation)
+ - [Security Considerations](#security-considerations)
+ - [Benefits](#benefits)
- [API Reference](#api-reference)
- [Client API](#client-api)
+ - [Provider Client API](#provider-client-api)
+ - [Consumer Client API](#consumer-client-api)
- [Server API](#server-api)
- [React API](#react-api)
- [Test API](#test-api)
@@ -22,6 +31,8 @@ A headless, isomorphic authentication toolkit that runs seamlessly across server
- [Troubleshooting](#troubleshooting)
- [TypeScript Types](#typescript-types)
- [Testing with Storybook](#testing-with-storybook)
+- [Package Structure](#package-structure)
+- [Recent Changes](#recent-changes)
## Installation
@@ -35,716 +46,417 @@ pnpm add @open-game-collective/auth-kit
## Key Features
-- 🌐 **Isomorphic & Headless**: Runs anywhere - server-side, web browsers, or React Native. Bring your own UI components.
-- 🎭 **Anonymous-First Auth**: Users start with an anonymous session that can be upgraded to a verified account.
-- 📧 **Email Verification**: Secure email verification flow with customizable storage and delivery options.
-- 🔐 **JWT-Based Tokens**: Secure session and refresh tokens with automatic refresh.
-- ⚡️ **Edge-Ready**: Optimized for Cloudflare Workers for minimal latency.
-- 🎯 **Type-Safe**: Full TypeScript support with detailed types.
-- 🎨 **React Integration**: Ready-to-use hooks and components for auth state management.
-- 🔌 **Customizable**: Integrate with your own storage, email delivery systems, and UI components.
-- 📱 **Platform Agnostic**: Same API and behavior across web and mobile platforms.
+- **Isomorphic Design**: Works seamlessly across server-side, web, and React Native environments
+- **Email Verification**: Secure email-based authentication with verification codes
+- **Token Management**: Automatic handling of session and refresh tokens
+- **Flexible Middleware**: Separation of authentication middleware from route handlers
+- **Account Linking**: Cross-application authentication between provider and consumer applications
+- **React Integration**: Ready-to-use React hooks and components
+- **Cloudflare Workers Support**: Optimized for edge computing environments
+- **TypeScript Support**: Full type safety and autocompletion
## Authentication Flow
-```mermaid
-sequenceDiagram
- participant U as User
- participant B as Browser
- participant RN as React Native
- participant S as Server
- participant E as Email
- participant DB as Storage
-
- Note over U,S: Anonymous Session
-
- alt Browser Client
- U->>B: First Visit
- B->>S: Request
- S->>S: Create Anonymous User (userId: "anon-123")
- S->>B: Response with Set-Cookie (HTTP-only cookies) sessionToken (15m) & refreshToken (7d)
- Note over B: Cookies stored in browser
- else React Native Client
- U->>RN: First Open
- RN->>S: POST /auth/anonymous
- S->>S: Create Anonymous User (userId: "anon-123")
- S->>RN: Return JSON {userId, sessionToken, refreshToken}
- Note over RN: Tokens stored in secure storage Consider biometric protection for refreshToken
- end
-
- Note over U,S: Email Verification
- U->>B: Enter Email "user@example.com"
- B->>S: POST /auth/request-code {email: "user@example.com"}
- S->>E: Send Code "123456"
- E->>U: Deliver Code "123456"
- U->>B: Submit Code
- B->>S: POST /auth/verify {email: "user@example.com", code: "123456"}
- S->>DB: Check if email exists in system
-
- alt New User (Anonymous Session Upgrade)
- Note over S: Email not found in system
- S->>DB: Update same userId "anon-123" from anonymous to verified
-
- alt Browser Client
- S->>B: Set new cookies & return {userId: "anon-123", email: "user@example.com"}
- Note over B: Same userId, upgraded permissions
- else React Native Client
- S->>RN: Return {userId: "anon-123", sessionToken: "jwt...", refreshToken: "jwt...", email: "user@example.com"}
- Note over RN: Store tokens in secure storage
- end
-
- Note over U: Show verified user interface
- else Existing User (Session Switch)
- Note over S: Email found with existing userId "user-456"
- S->>DB: Look up existing userId for this email
-
- alt Browser Client
- S->>B: Set new cookies & return {userId: "user-456", email: "user@example.com"}
- Note over B: Different userId, switch to existing account
- else React Native Client
- S->>RN: Return {userId: "user-456", sessionToken: "jwt...", refreshToken: "jwt...", email: "user@example.com"}
- Note over RN: Replace tokens in secure storage
- end
-
- Note over U: Show existing user interface
- end
-
- Note over U,S: Session Management
-
- alt Browser Client
- B->>S: API Requests with session cookie
- S->>S: Validate Session Cookie
- alt Session Expired (15m)
- S->>S: Check Refresh Cookie (7d)
- S->>B: Set new session cookie
- end
- else React Native Client
- RN->>S: API Requests with Authorization: Bearer {sessionToken}
- S->>S: Validate Session Token
- alt Session Expired (15m)
- RN->>S: POST /auth/refresh with refreshToken
- S->>RN: Return new sessionToken
- Note over RN: Update sessionToken in secure storage
- end
- end
-
- Note over U,S: Logout
-
- alt Browser Client
- U->>B: Logout
- B->>S: POST /auth/logout
- S->>B: Clear Cookies with Set-Cookie header
- else React Native Client
- U->>RN: Logout
- RN->>S: POST /auth/logout
- RN->>RN: Delete tokens from secure storage
- end
-
- Note over U,S: Next visit starts new anonymous session
-```
+Auth Kit uses a secure, token-based authentication flow:
-### User Data in JWT Tokens
+1. **Request Verification Code**: User enters email and requests a verification code
+2. **Email Delivery**: Verification code is sent to the user's email
+3. **Code Verification**: User enters the code, which is verified on the server
+4. **Token Generation**: Upon successful verification, session and refresh tokens are generated
+5. **Authenticated Requests**: Subsequent requests include the session token for authentication
+6. **Token Refresh**: When the session token expires, the refresh token is used to obtain a new one
-Auth Kit uses JWT tokens to securely store and transmit user information. By default, the tokens include minimal data:
+This flow provides a secure, passwordless authentication system that works across all platforms.
-1. **Session Tokens** include:
- - `userId`: The unique identifier for the user
- - `sessionId`: A unique identifier for the session
- - `email`: The user's email address (if verified)
- - `aud`: Audience claim set to "SESSION"
- - `exp`: Expiration time (default: 15 minutes)
+## Usage Guide
-2. **Refresh Tokens** include:
- - `userId`: The unique identifier for the user
- - `aud`: Audience claim set to "REFRESH"
- - `exp`: Expiration time (default: 7 days for cookies, 1 hour for transient tokens)
+### 1️⃣ Set up Environment and Server
-You can extend the tokens to include additional user data by modifying the token creation functions:
+To get started with Auth Kit, you'll need to set up your environment and server configuration:
```typescript
-// Example: Including email in session tokens
-async function createSessionToken(
- userId: string,
- email: string | null,
- secret: string,
- expiresIn: string = "15m"
-): Promise {
- const sessionId = crypto.randomUUID();
- return await new SignJWT({
- userId,
- sessionId,
- email
- })
- .setProtectedHeader({ alg: "HS256" })
- .setAudience("SESSION")
- .setExpirationTime(expiresIn)
- .sign(new TextEncoder().encode(secret));
-}
-```
-
-**Benefits of storing user data in JWTs:**
-- Reduces database lookups for common user information
-- Makes user data available on the client without additional API calls
-- Simplifies client-side state management
+// server.ts or worker.ts
+import { withAuth, AuthHooks } from "@open-game-collective/auth-kit/server";
+import { Env } from "./env";
-**Considerations:**
-- Only include non-sensitive data in tokens
-- Keep tokens reasonably sized (avoid large payloads)
-- Remember that JWT contents can be read (though not modified) by clients
-- Update tokens when user data changes
+// Define your auth hooks
+const authHooks: AuthHooks = {
+ // Required: Get user ID by email
+ getUserIdByEmail: async ({ email, env }) => {
+ // Example with Cloudflare KV
+ return await env.AUTH_KV.get(`email:${email}`);
+ },
+
+ // Required: Store verification code
+ storeVerificationCode: async ({ email, code, expiresAt, env }) => {
+ await env.AUTH_KV.put(
+ `verification:${email}`,
+ JSON.stringify({ code, expiresAt: expiresAt.toISOString() }),
+ { expirationTtl: 900 } // 15 minutes
+ );
+ },
+
+ // Required: Verify the code
+ verifyVerificationCode: async ({ email, code, env }) => {
+ const storedData = await env.AUTH_KV.get(`verification:${email}`);
+ if (!storedData) return false;
+
+ const { code: storedCode, expiresAt } = JSON.parse(storedData);
+ return storedCode === code && new Date(expiresAt) > new Date();
+ },
+
+ // Required: Send verification code to user
+ sendVerificationCode: async ({ email, code, env }) => {
+ // Integrate with your email service
+ console.log(`Sending code ${code} to ${email}`);
+ // Example: await env.EMAIL_SERVICE.send(email, `Your verification code: ${code}`);
+ }
+};
-The Auth Kit client automatically extracts and provides this data to your application through the auth state:
+// Create the auth middleware
+const authMiddleware = withAuth(
+ async (request, env, { userId, sessionId, sessionToken }) => {
+ // This handler runs for non-auth routes
+ const url = new URL(request.url);
+
+ // Example of a protected route
+ if (url.pathname === '/dashboard') {
+ return new Response(`Welcome to your dashboard, user ${userId}!`);
+ }
+
+ // Default response for other routes
+ return new Response('Hello World');
+ },
+ {
+ hooks: authHooks,
+ useTopLevelDomain: true, // Optional: Use top-level domain for cookies
+ basePath: "/auth" // Optional: Base path for auth routes
+ }
+);
-```typescript
-const { userId, email } = authClient.getState();
+// Export the worker handler
+export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ return authMiddleware(request, env, ctx);
+ }
+};
```
-### Authentication State
+For Cloudflare Workers, configure your `wrangler.toml`:
-Auth Kit maintains a core state object that represents the current user's authentication status. This state is accessible through the client and can be subscribed to for real-time updates.
+```toml
+name = "auth-kit-example"
+main = "src/index.ts"
+compatibility_date = "2024-03-25"
-The `AuthState` type is defined as:
+[[kv_namespaces]]
+binding = "AUTH_KV"
+id = "your-kv-namespace-id"
-```typescript
-/**
- * The authentication state object that represents the current user's session.
- * This is the core state object used throughout the auth system.
- */
-export type AuthState = {
- /**
- * The unique identifier for the current user.
- * For anonymous users, this will be a randomly generated ID.
- * For verified users, this will be their permanent user ID.
- */
- userId: string;
-
- /**
- * The JWT session token used for authenticated requests.
- * This token has a short expiration (typically 15 minutes) and is
- * automatically refreshed using the refresh token when needed.
- */
- sessionToken: string | null;
-
- /**
- * The user's verified email address, if they have completed verification.
- * Will be null for anonymous users or users who haven't verified their email.
- * The presence of an email indicates the user is verified.
- */
- email: string | null;
-
- /**
- * Indicates if an authentication operation is currently in progress.
- * Used to show loading states in the UI during auth operations.
- */
- isLoading: boolean;
-
- /**
- * Any error that occurred during the last authentication operation.
- * Will be null if no error occurred.
- */
- error: string | null;
-};
+[vars]
+# Environment variables
+
+# Secrets (use `wrangler secret put AUTH_SECRET` to set)
+# - AUTH_SECRET
```
-#### Working with Authentication State
+### 2️⃣ Access Auth in React Router Routes
-You can access the current state at any time:
+If you're using React Router or Remix, you can access authentication information in your routes:
-```typescript
-const state = authClient.getState();
-console.log(`User ID: ${state.userId}`);
-console.log(`Is verified: ${Boolean(state.email)}`);
-```
-
-For reactive applications, you can subscribe to state changes:
+```tsx
+// app/routes/dashboard.tsx (Remix example)
+import { useLoaderData } from "@remix-run/react";
+import { json, LoaderFunctionArgs, redirect } from "@remix-run/cloudflare";
-```typescript
-const unsubscribe = authClient.subscribe((state) => {
- console.log('Auth state updated:', state);
+// Define loader function to access auth info
+export async function loader({ context }: LoaderFunctionArgs) {
+ // Auth info is passed from the Auth Kit middleware
+ const { userId, sessionId, sessionToken } = context;
- if (state.email) {
- // User is verified
- showVerifiedUI();
- } else {
- // User is anonymous
- showAnonymousUI();
+ if (!userId) {
+ // Redirect to login if not authenticated
+ return redirect("/login");
}
- if (state.isLoading) {
- // Show loading indicator
- showLoadingSpinner();
- }
+ // Fetch user data using the userId
+ const userData = await context.env.AUTH_KV.get(`user:${userId}`);
+ const user = userData ? JSON.parse(userData) : null;
- if (state.error) {
- // Show error message
- showErrorNotification(state.error);
- }
-});
-
-// Later, when you no longer need updates:
-unsubscribe();
-```
-
-#### React Integration
-
-For React applications, Auth Kit provides components that automatically respond to state changes:
-
-```jsx
-import { createAuthContext } from '@open-game-collective/auth-kit/react';
-
-const AuthContext = createAuthContext();
-
-function App() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ return json({ user });
}
-```
-
-You can also use the `useSelector` hook to access specific parts of the state:
-```jsx
-function UserGreeting() {
- const email = AuthContext.useSelector(state => state.email);
+export default function Dashboard() {
+ const { user } = useLoaderData();
return (
-
);
}
```
-This setup provides:
+Create a hook to access auth state in your components:
-1. **Server-side Authentication**:
- - The `withAuth` middleware automatically handles authentication for all routes
- - Auth state is passed to loaders and actions via the context
- - Protected routes can check auth state and redirect if needed
+```tsx
+// src/hooks/useAuth.ts
+import { useEffect } from "react";
+import { AuthContext } from "../auth";
-2. **Client-side Integration**:
- - Auth state is hydrated from the server via the root loader
- - The auth client is created once and provided to all components
- - Components can access auth state via hooks and conditional components
+export function useAuth() {
+ const client = AuthContext.useClient();
+ const state = AuthContext.useSelector(state => state);
+
+ useEffect(() => {
+ // Refresh the session when the component mounts
+ if (client) {
+ client.refresh().catch(err => {
+ console.error("Failed to refresh session:", err);
+ });
+ }
+ }, [client]);
+
+ const logout = async () => {
+ if (client) {
+ await client.logout();
+ window.location.href = "/login";
+ }
+ };
+
+ return {
+ isAuthenticated: !!state.userId,
+ isLoading: state.isLoading,
+ error: state.error,
+ email: state.email,
+ userId: state.userId,
+ logout
+ };
+}
+```
-3. **Form Handling**:
- - React Router's Form component works with auth-protected actions
- - Client-side auth state updates automatically after form submissions
- - Loading states are handled via the auth context
+Now you can use this hook in your components:
-4. **Navigation**:
- - Auth-based redirects work both server-side and client-side
- - After verification, users are redirected to protected routes
- - After logout, users are redirected to public routes
+```tsx
+// src/components/Header.tsx
+import { useAuth } from "../hooks/useAuth";
-You can use Auth Kit with:
-- Next.js: In API routes or server components
-- React Router: In loaders or actions
-- TanStack Router: In route handlers
-- Vite SSR: In server entry point
+export function Header() {
+ const { isAuthenticated, email, logout } = useAuth();
+
+ return (
+
+
+
+ );
+}
+```
-The only requirement is implementing the auth hooks for your chosen storage and email delivery solutions.
+## Architecture
-### Mobile Applications (React Native)
+Auth Kit is designed with a modular architecture that separates concerns and provides flexibility for different use cases.
-For mobile applications, you'll need to explicitly manage user creation and token storage. For enhanced security, we recommend using biometric authentication to protect the refresh token:
+### Middleware Architecture
+
+Auth Kit provides a flexible middleware architecture that separates authentication middleware from route handlers. This is particularly useful for environments like Cloudflare Workers where middleware and route handling need to be distinct.
+
+#### Key Components
+
+- **`createAuthRouter`**: Handles auth-specific routes like `/auth/*`
+- **`withAuth`**: Creates middleware that applies authentication to your application handler
+
+#### Integration with Cloudflare Workers
+
+Here's how to integrate Auth Kit with Cloudflare Workers and Remix:
```typescript
-// app/auth.ts
-import { createAnonymousUser, createAuthClient } from "@open-game-collective/auth-kit/client";
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import * as LocalAuthentication from 'expo-local-authentication';
-import * as SecureStore from 'expo-secure-store';
-
-// Keys for different storage mechanisms
-const AUTH_KEYS = {
- // Regular storage for non-sensitive data
- USER_ID: 'auth_user_id',
- SESSION_TOKEN: 'auth_session_token',
-
- // Secure storage for sensitive data
- REFRESH_TOKEN: 'auth_refresh_token'
-} as const;
-
-// Check if biometric authentication is available
-async function isBiometricAvailable() {
- const compatible = await LocalAuthentication.hasHardwareAsync();
- const enrolled = await LocalAuthentication.isEnrolledAsync();
- return compatible && enrolled;
-}
+import { AuthHooks, withAuth } from "@open-game-collective/auth-kit/server";
+import { createRequestHandler, logDevReady } from "@remix-run/cloudflare";
+import * as build from "@remix-run/dev/server-build";
-// Store refresh token with biometric protection if available
-async function storeRefreshToken(token: string) {
- if (await isBiometricAvailable()) {
- // Use biometric authentication before storing the token
- const result = await LocalAuthentication.authenticateAsync({
- promptMessage: 'Authenticate to secure your session',
- fallbackLabel: 'Use passcode'
- });
-
- if (result.success) {
- // Store in secure storage after biometric authentication
- await SecureStore.setItemAsync(AUTH_KEYS.REFRESH_TOKEN, token);
- return true;
- } else {
- console.warn('Biometric authentication failed, using fallback storage');
- // Fallback to regular secure storage
- await AsyncStorage.setItem(AUTH_KEYS.REFRESH_TOKEN, token);
- return false;
- }
- } else {
- // Fallback to regular secure storage if biometrics not available
- await AsyncStorage.setItem(AUTH_KEYS.REFRESH_TOKEN, token);
- return false;
- }
-}
+// Create the Remix request handler
+const handleRemixRequest = createRequestHandler(build);
-// Retrieve refresh token, requiring biometric auth if it was stored that way
-async function getRefreshToken() {
- if (await isBiometricAvailable()) {
- try {
- // Try to get from secure storage first (requires biometrics on some devices)
- return await SecureStore.getItemAsync(AUTH_KEYS.REFRESH_TOKEN);
- } catch (error) {
- // Fallback to AsyncStorage
- return await AsyncStorage.getItem(AUTH_KEYS.REFRESH_TOKEN);
- }
- } else {
- // Use regular storage if biometrics not available
- return await AsyncStorage.getItem(AUTH_KEYS.REFRESH_TOKEN);
- }
+if (process.env.NODE_ENV === "development") {
+ logDevReady(build);
}
-async function clearAuthTokens() {
- await Promise.all([
- AsyncStorage.removeItem(AUTH_KEYS.USER_ID),
- AsyncStorage.removeItem(AUTH_KEYS.SESSION_TOKEN),
- // Clear from both storage mechanisms
- AsyncStorage.removeItem(AUTH_KEYS.REFRESH_TOKEN),
- SecureStore.deleteItemAsync(AUTH_KEYS.REFRESH_TOKEN)
- ]);
-}
+// Define auth hooks
+const authHooks: AuthHooks = {
+ getUserIdByEmail: async ({ email, env }) => {
+ return await env.KV_STORAGE.get(`email:${email}`);
+ },
+ // ... other hooks implementation
+};
-export async function initializeAuth() {
- // Try to load existing tokens
- const [userId, sessionToken, refreshToken] = await Promise.all([
- AsyncStorage.getItem(AUTH_KEYS.USER_ID),
- AsyncStorage.getItem(AUTH_KEYS.SESSION_TOKEN),
- getRefreshToken() // Use our helper that handles biometric auth
- ]);
-
- // If we have existing tokens, create client with them
- if (userId && sessionToken) {
- return createAuthClient({
- host: "your-worker.workers.dev",
+// Create the auth middleware
+const authMiddleware = withAuth(
+ async (request, env, { userId, sessionId, sessionToken }) => {
+ // This handler runs for non-auth routes
+ // Auth routes like /auth/* are handled automatically by the middleware
+
+ // Pass auth info to Remix
+ return handleRemixRequest(request, {
+ env,
userId,
+ sessionId,
sessionToken,
- refreshToken // Include refresh token for mobile
});
+ },
+ {
+ hooks: authHooks,
+ useTopLevelDomain: true,
+ basePath: "/auth" // This tells withAuth to handle /auth/* routes internally
}
+);
- // Otherwise create a new anonymous user with longer refresh token for mobile
- const tokens = await createAnonymousUser({
- host: "your-worker.workers.dev",
- refreshTokenExpiresIn: '30d', // Longer refresh token for mobile
- sessionTokenExpiresIn: '1h' // Longer session token for mobile
- });
-
- // Store the tokens
- await Promise.all([
- AsyncStorage.setItem(AUTH_KEYS.USER_ID, tokens.userId),
- AsyncStorage.setItem(AUTH_KEYS.SESSION_TOKEN, tokens.sessionToken),
- storeRefreshToken(tokens.refreshToken) // Use our helper for biometric protection
- ]);
-
- // Create and return the client
- return createAuthClient({
- host: "your-worker.workers.dev",
- userId: tokens.userId,
- sessionToken: tokens.sessionToken,
- refreshToken: tokens.refreshToken // Include refresh token for mobile
- });
-}
+// Main worker entry point
+export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ // All requests go through the auth middleware
+ // - Auth routes like /auth/* are handled automatically
+ // - Other routes get authentication and are passed to the Remix request handler
+ return authMiddleware(request, env, ctx);
+ }
+};
+```
-// App.tsx
-import { AuthContext } from "./auth.context";
-import { useState, useEffect, useCallback } from "react";
-import { Button } from "react-native";
-import { NavigationContainer } from "@react-navigation/native";
+This pattern provides several benefits:
-export default function App() {
- const [client, setClient] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [isLoggingOut, setIsLoggingOut] = useState(false);
+1. **Simplified Integration**: The `withAuth` function handles both auth routes and application routes
+2. **Clear Separation of Concerns**: Auth routes are handled internally by the middleware
+3. **Flexibility**: You can use this pattern with any framework or custom request handler
+4. **Performance**: Auth routes are handled efficiently by the middleware
- useEffect(() => {
- initializeAuth()
- .then(setClient)
- .finally(() => setIsLoading(false));
- }, []);
+#### Alternative: Using a Separate Auth Router
- const handleLogout = useCallback(async () => {
- if (!client || isLoggingOut) return;
-
- // Immediately set logging out state and clear client
- setIsLoggingOut(true);
- setClient(null);
+If you need more control over auth routes, you can also use a separate router:
- try {
- // Call client logout to clear server-side session
- await client.logout();
-
- // Clear stored tokens
- await clearAuthTokens();
-
- // Create new anonymous session
- const newClient = await initializeAuth();
- setClient(newClient);
- } finally {
- setIsLoggingOut(false);
- }
- }, [client, isLoggingOut]);
+```typescript
+import { AuthHooks, createAuthRouter, withAuth } from "@open-game-collective/auth-kit/server";
+import { createRequestHandler, logDevReady } from "@remix-run/cloudflare";
+import * as build from "@remix-run/dev/server-build";
- if (isLoading || isLoggingOut || !client) {
- return ;
- }
+// Create the Remix request handler
+const handleRemixRequest = createRequestHandler(build);
- return (
-
-
-
-
-
-
- );
+if (process.env.NODE_ENV === "development") {
+ logDevReady(build);
}
-// Usage in components
-function ProfileScreen() {
- const client = AuthContext.useClient();
- const email = AuthContext.useSelector(state => state.email);
+// Define auth hooks
+const authHooks: AuthHooks = {
+ getUserIdByEmail: async ({ email, env }) => {
+ return await env.KV_STORAGE.get(`email:${email}`);
+ },
+ // ... other hooks implementation
+};
- const verifyEmail = async () => {
- await client.requestCode('user@example.com');
- // Show verification code input...
- };
+// Create the auth router for handling auth routes
+const authRouter = createAuthRouter({
+ hooks: authHooks,
+ useTopLevelDomain: true,
+ basePath: "/auth"
+});
- return (
-
- {!email ? (
-
- ) : (
- Welcome back, {email}!
- )}
-
- );
-}
-```
+// Define your application handler function
+const handleAppRequest = async (
+ request: Request,
+ env: Env,
+ authInfo: { userId: string, sessionId: string, sessionToken: string }
+) => {
+ const { userId, sessionId, sessionToken } = authInfo;
+
+ // Pass auth info to Remix
+ return await handleRemixRequest(request, {
+ env,
+ userId,
+ sessionId,
+ sessionToken,
+ });
+};
-This implementation provides several security enhancements:
-
-1. **Biometric Authentication**: Uses device biometrics (fingerprint/face recognition) to protect the refresh token
-2. **Secure Storage Tiers**:
- - Regular tokens (session token, user ID) in AsyncStorage
- - Sensitive tokens (refresh token) in SecureStore with biometric protection
-3. **Graceful Fallbacks**: Falls back to regular secure storage if biometrics aren't available
-4. **Token Separation**: Keeps session and refresh tokens separate for better security
-5. **Longer-lived Tokens**: Uses longer expiration times for mobile to reduce authentication frequency
-6. **Proper Cleanup**: Ensures tokens are removed from all storage locations on logout
-
-### Mobile-to-Web Authentication
-
-Auth Kit provides a secure way to authenticate mobile app users in web views using signed JWTs. This is useful for scenarios where you want to:
-- Open authenticated web content from your mobile app
-- Share authentication state between mobile and web
-- Provide a hybrid mobile-web experience
-
-Here's how the flow works:
-
-1. **Mobile App**: Generate a signed JWT auth code
- ```typescript
- // Mobile App: Request a signed JWT auth code
- const { code, expiresIn } = await client.getWebAuthCode();
- ```
- The server:
- - Verifies the mobile session token
- - Creates a signed JWT containing the user's ID
- - Sets a short expiration (5 minutes)
- - Signs it with the same secret used for other tokens
-
-2. **Open Web View**: Use the JWT to authenticate
- ```typescript
- // Option 1: Using Expo WebBrowser
- import * as WebBrowser from 'expo-web-browser';
- await WebBrowser.openAuthSessionAsync(
- `https://your-web-app.com?code=${code}`
- );
-
- // Option 2: Using React Native's Linking
- import { Linking } from 'react-native';
- await Linking.openURL(
- `https://your-web-app.com?code=${code}`
- );
-
- // Option 3: Using React Native WebView
- import { WebView } from 'react-native-webview';
- return (
-
- );
- ```
-
-3. **Server Middleware**: Automatic JWT verification
- The `withAuth` middleware automatically:
- 1. Detects the JWT auth code in the URL
- 2. Verifies the JWT signature and expiration
- 3. Extracts the user ID from the verified JWT
- 4. Creates new web session tokens with the same user ID
- 5. Sets HTTP-only cookies for the web session
- 6. Redirects to remove the code from URL
-
-Security features:
-- JWTs are cryptographically signed
-- Short expiration (5 minutes)
-- Audience claim verification ("WEB_AUTH")
-- No server-side storage needed
-- All communication requires HTTPS
-- Web sessions use HTTP-only cookies
-- Mobile app verifies web app origin
-- Session tokens are never exposed in URLs
-
-This approach is more secure than OAuth for first-party applications because:
-- No need for complex OAuth flows
-- Direct session transfer using signed JWTs
-- No server-side storage required
-- Reduced attack surface (no callback URLs)
-- Better UX (no consent screens)
-- Same user ID maintained across platforms
-
-Example JWT verification:
-```typescript
-// Inside withAuth middleware
-const webAuthCode = url.searchParams.get('code');
-if (webAuthCode) {
- try {
- // Verify the JWT signature and claims
- const verified = await jwtVerify(
- webAuthCode,
- new TextEncoder().encode(env.AUTH_SECRET),
- { audience: "WEB_AUTH" }
- );
+// Create the auth handler using withAuth
+const handler = withAuth(handleAppRequest, {
+ hooks: authHooks,
+ useTopLevelDomain: true
+});
+
+// Main worker entry point
+export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ const url = new URL(request.url);
- const payload = verified.payload as { userId: string };
- if (payload.userId) {
- // Create new web session with same user ID
- const sessionId = crypto.randomUUID();
- const newSessionToken = await createSessionToken(
- payload.userId, // Same user ID as mobile
- env.AUTH_SECRET
- );
- const newRefreshToken = await createRefreshToken(
- payload.userId,
- env.AUTH_SECRET
- );
-
- // Redirect and set cookies...
+ // Handle auth routes with the auth router
+ if (url.pathname.startsWith('/auth/')) {
+ return authRouter(request, env, ctx);
}
- } catch (error) {
- // JWT verification failed
- console.error('Failed to verify web auth code:', error);
+
+ // Handle all other routes with the auth handler, which passes them to the Remix request handler
+ return handler(request, env, ctx);
+ }
+};
+```
+
+This approach gives you more control over how auth routes are handled, which can be useful in specific scenarios like:
+- When you need to apply different middleware to auth routes
+- When you want to handle auth routes at the edge but process application routes in a Durable Object
+- When you need to customize the auth route handling beyond what `withAuth` provides
+
+#### Example with Durable Objects
+
+If you're using Durable Objects, you can adapt the pattern like this:
+
+```typescript
+import { AuthHooks, withAuth } from "@open-game-collective/auth-kit/server";
+import { createRequestHandler } from "@remix-run/cloudflare";
+import * as build from "@remix-run/dev/server-build";
+import { DurableObject } from "cloudflare:workers";
+
+// Create the Remix request handler
+const handleRemixRequest = createRequestHandler(build);
+
+// Define auth hooks
+const authHooks: AuthHooks = {
+ // ... hooks implementation
+};
+
+// Create the auth middleware for the Durable Object
+const authMiddleware = withAuth(
+ async (request, env, { userId, sessionId, sessionToken }) => {
+ // Pass auth info to Remix
+ return handleRemixRequest(request, {
+ env,
+ userId,
+ sessionId,
+ sessionToken,
+ });
+ },
+ {
+ hooks: authHooks,
+ useTopLevelDomain: true,
+ basePath: "/auth" // This tells withAuth to handle /auth/* routes internally
+ }
+);
+
+// Durable Object implementation
+export class AppDO extends DurableObject {
+ async fetch(request: Request): Promise {
+ // No need to check for /auth/ paths - withAuth handles that internally
+ return authMiddleware(request, this.env);
}
}
+
+// Main worker entry point
+export default {
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
+ // For all routes, use the Durable Object
+ const id = env.APP_DO.idFromName("default");
+ const appDO = env.APP_DO.get(id);
+ return appDO.fetch(request);
+ }
+};
```
-The web session maintains the same user identity as the mobile app while using its own session tokens, allowing for independent session management on each platform.
+For more detailed examples, including provider/consumer implementations, see the documentation.
-## Architecture
+## Account Linking
-Auth Kit is comprised of three core components:
+Auth Kit provides a robust account linking system that allows applications to connect user accounts across the Open Game ecosystem. This enables rich cross-application features such as push notifications, achievement sharing, and synchronized experiences, while maintaining each application's independent authentication system.
-1. **Server Middleware (`@open-game-collective/auth-kit/server`)**
- - Handles all `/auth/*` routes automatically.
- - Manages JWT-based session tokens (15 minutes) and refresh tokens (7 days).
- - Creates anonymous users when no valid session exists.
- - Supplies `userId` and `sessionId` to your React Router loaders.
+### Provider-Consumer Model
-2. **Auth Client (`@open-game-collective/auth-kit/client`)**
- - Manages client-side auth state.
- - Automatically refreshes tokens.
- - Provides methods for email verification and logout.
- - Supports state subscriptions and pub/sub updates.
+Account linking follows a provider-consumer model:
-3. **React Integration (`@open-game-collective/auth-kit/react`)**
- - Offers hooks for accessing auth state.
- - Provides conditional components for loading, authentication, and verification states.
- - Leverages Suspense for efficient UI updates.
+- **Provider** (e.g., OpenGame): The central identity provider that manages user accounts
+- **Consumer** (e.g., Game applications): Applications that integrate with the provider for feature sharing
-Auth Kit is designed to be deployed with your application server. The auth middleware is integrated into your server, handling authentication for all routes.
+Account linking is not about authentication delegation, but rather about enabling cross-application features such as:
+
+- Push notifications from the provider app for events in consumer apps
+- Profile and achievement sharing across applications
+- Synchronized preferences and settings
+- Cross-application rewards and progression
+- Unified social features and friend connections
+
+Each application maintains its own authentication system, but linking accounts allows for a richer, connected user experience across the ecosystem.
+
+### Account Linking Flow
+
+The following diagram illustrates how accounts are linked between the provider (OpenGame) and consumer applications (games), enabling cross-application features while maintaining separate authentication systems:
```mermaid
sequenceDiagram
- participant B as Browser
- participant S as Server
- participant D as Database
-
- B->>S: Request /app
- S->>S: Auth Middleware
- S->>D: Check Session
- D->>S: Session Data
- S->>B: Response with Auth State
+ participant User
+ participant OGApp as Provider App
+ participant GameApp as Consumer App
+ participant ProviderAuth as Provider Auth API
+ participant ConsumerAuth as Consumer Auth API
+
+ User->>OGApp: Initiates account linking
+ OGApp->>ProviderAuth: Requests link token
+ ProviderAuth->>OGApp: Returns link token
+ OGApp->>User: Displays link URL/QR code
+ User->>GameApp: Opens link URL
+ GameApp->>ConsumerAuth: Verifies link token
+ ConsumerAuth->>ProviderAuth: Validates token
+ ProviderAuth->>ConsumerAuth: Confirms token validity
+ GameApp->>User: Requests confirmation
+ User->>GameApp: Confirms linking
+ GameApp->>ConsumerAuth: Confirms link
+ ConsumerAuth->>ProviderAuth: Stores account link
+ ProviderAuth->>ConsumerAuth: Confirms success
+ GameApp->>User: Shows success message
+ Note over User, ConsumerAuth: After linking, cross-app features are enabled
```
-### Auth Middleware Setup
+Once accounts are linked, the provider application can send push notifications about events in the consumer application, share profile information between applications, and enable other cross-application features - all while each application maintains its own independent authentication system.
+
+### Implementation
-The auth middleware is the core of Auth Kit. It handles:
-1. Session validation and renewal
-2. Anonymous user creation
-3. JWT verification
-4. Cookie management
+Auth Kit provides specialized APIs for both providers and consumers:
+
+#### Provider Implementation
```typescript
-// server.ts
-import { withAuth } from "@open-game-collective/auth-kit/server";
+// Server-side setup
+import { createProviderAuthRouter } from "@open-game-collective/auth-kit/provider/server";
-// Example showing conditional logging based on NODE_ENV
-const handler = withAuth(async (request, env, { userId, sessionId }) => {
- // Conditionally log auth information in development mode
- if (process.env.NODE_ENV === 'development') {
- console.log(`Auth request from user: ${userId}`);
- console.log(`Session ID: ${sessionId}`);
- console.log(`Request path: ${new URL(request.url).pathname}`);
- }
-
- // Your application logic here
- const url = new URL(request.url);
-
- if (url.pathname === '/api/protected-data') {
- // This route is automatically protected by auth middleware
- return new Response(JSON.stringify({
- data: 'This is protected data',
- userId
- }));
- }
-
- // Serve your application
- return fetch(request);
-}, {
+const providerRouter = createProviderAuthRouter({
hooks: {
- // Your auth hooks implementation
- getUserIdByEmail: async ({ email }) => {
- // In development, log email verification attempts
- if (process.env.NODE_ENV === 'development') {
- console.log(`Looking up user ID for email: ${email}`);
- }
- return db.getUserIdByEmail(email);
- },
- // Other required hooks...
- }
+ // Base auth hooks
+ getUserIdByEmail: async ({ email, env }) => { /* ... */ },
+ // ... other base hooks
+
+ // Provider-specific hooks
+ getGameIdFromApiKey: async ({ apiKey, env }) => { /* ... */ },
+ storeAccountLink: async ({ openGameUserId, gameId, gameUserId, env }) => { /* ... */ },
+ getLinkedAccounts: async ({ openGameUserId, env }) => { /* ... */ },
+ removeAccountLink: async ({ openGameUserId, gameId, env }) => { /* ... */ },
+ },
+ useTopLevelDomain: true, // Optional
+ basePath: "/auth" // Optional, defaults to "/auth"
});
-export default {
- fetch: handler
-};
+// Client-side implementation
+import { createProviderAuthClient } from "@open-game-collective/auth-kit/provider/client";
+import { createProviderAuthContext } from "@open-game-collective/auth-kit/provider/react";
+
+const ProviderAuthContext = createProviderAuthContext();
+const providerClient = createProviderAuthClient({
+ host: "your-api.example.com",
+ userId: "provider-123",
+ sessionToken: "jwt-token"
+});
+
+function AccountLinkingUI() {
+ return (
+
+
+