This comprehensive guide provides detailed instructions for setting up and using OpenID Connect authentication in your TanStack Start application. This implementation provides secure, industry-standard authentication with PKCE (Proof Key for Code Exchange), automatic token refresh, and server-side session management.
- Overview
- Architecture
- Configuration
- OIDC Provider Setup
- Installation & Setup
- Authentication Flow
- Usage Guide
- API Reference
- Security Features
- Session Management
- Error Handling
- Production Deployment
- Testing
- Troubleshooting
- Advanced Configuration
- File Structure
OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0. It provides a standardized way for applications to verify user identity and obtain basic profile information. This implementation uses the Authorization Code Flow with PKCE, which is the recommended flow for public clients (like single-page applications).
- ✅ PKCE (Proof Key for Code Exchange): Enhanced security for public clients
- ✅ Automatic Token Refresh: Tokens are refreshed proactively before expiration
- ✅ Server-Side Session Management: Secure session storage using TanStack Start's built-in session handling
- ✅ State Parameter Validation: CSRF protection through state parameter verification
- ✅ RP-Initiated Logout: Proper logout flow with token revocation
- ✅ Type-Safe Implementation: Full TypeScript support throughout
- ✅ Error Handling: Comprehensive error handling with user-friendly messages
- ✅ Test Mode Support: Built-in test mode for development and E2E testing
This implementation works with any OIDC-compliant provider, including:
- Auth0
- Keycloak
- Azure AD / Microsoft Entra ID
- Google Identity Platform
- Okta
- AWS Cognito
- IdentityServer
- ABP Framework (with OpenIddict)
- Any other OIDC-compliant provider
┌─────────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ AuthProvider │ │ ProtectedRoute│ │ useAuth Hook │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼─────────────────┼─────────────────┼─────────────┘
│ │ │
└─────────────────┼─────────────────┘
│
┌─────────────────▼─────────────────┐
│ TanStack Start Server │
│ ┌─────────────────────────────┐ │
│ │ Auth Routes (API) │ │
│ │ - /auth/login │ │
│ │ - /auth/callback │ │
│ │ - /auth/logout │ │
│ │ - /auth/me │ │
│ └──────────┬──────────────────┘ │
│ │ │
│ ┌──────────▼──────────────────┐ │
│ │ Auth Infrastructure │ │
│ │ - oidc.ts (OIDC client) │ │
│ │ - auth-server.ts (session) │ │
│ │ - session.ts (session mgmt) │ │
│ └──────────┬──────────────────┘ │
└─────────────┼──────────────────────┘
│
┌─────────────▼──────────────┐
│ OIDC Provider │
│ (Auth0, Keycloak, etc.) │
└────────────────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Server │ │ OIDC │
│ │ │ │ │ Provider │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. GET /auth/login │ │
├──────────────────────────────>│ │
│ │ 2. Generate PKCE verifier │
│ │ & state │
│ │ │
│ 3. Return auth URL │ │
│<──────────────────────────────┤ │
│ │ │
│ 4. Redirect to OIDC │ │
├───────────────────────────────────────────────────────────────>│
│ │ │
│ │ 5. User authenticates │
│ │ │
│ 6. Redirect with code │ │
│<───────────────────────────────────────────────────────────────┤
│ │ │
│ 7. GET /auth/callback?code= │ │
├──────────────────────────────>│ │
│ │ 8. Exchange code for tokens │
│ ├──────────────────────────────>│
│ │ │
│ │ 9. Return tokens │
│ │<──────────────────────────────┤
│ │ │
│ │ 10. Create session │
│ │ │
│ 11. Redirect to /dashboard │ │
│<──────────────────────────────┤ │
│ │ │
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Server │ │ OIDC │
│ │ │ │ │ Provider │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. GET /auth/me │ │
├──────────────────────────────>│ │
│ │ 2. Check token expiration │
│ │ (15 min before expiry) │
│ │ │
│ │ 3. Refresh token needed? │
│ │ Yes ───────────────────┐ │
│ │ │ │
│ │ 4. Refresh access token │ │
│ ├───────────────────────────>│ │
│ │ │ │
│ │ 5. Return new tokens │ │
│ │<───────────────────────────┤ │
│ │ │ │
│ │ 6. Update session │ │
│ │<───────────────────────────┘ │
│ │ │
│ 7. Return user data │ │
│<──────────────────────────────┤ │
│ │ │
Create a .env file in the root of your project with the following variables:
# ============================================
# OIDC Provider Configuration
# ============================================
# OIDC Provider Issuer URL
# This is the base URL of your OIDC provider's discovery endpoint
# Examples:
# - Auth0: https://your-tenant.auth0.com
# - Keycloak: https://keycloak.example.com/realms/your-realm
# - Azure AD: https://login.microsoftonline.com/your-tenant-id
# - ABP: https://your-abp-backend.com
VITE_OIDC_ISSUER=https://your-oidc-provider.com
# OIDC Client ID
# The client identifier registered with your OIDC provider
VITE_OIDC_CLIENT_ID=your-client-id
# OIDC Client Secret (Optional for public clients)
# Required for confidential clients, optional for public clients
# If using PKCE (which this implementation does), this can be omitted for public clients
VITE_OIDC_CLIENT_SECRET=your-client-secret
# ============================================
# Application URLs
# ============================================
# Base URL of your application
# Development: http://localhost:3000
# Production: https://your-domain.com
VITE_BASE_URL=http://localhost:3000
# OIDC Redirect URI
# Must match exactly what's configured in your OIDC provider
# This is where users are redirected after authentication
VITE_OIDC_REDIRECT_URI=http://localhost:3000/auth/callback
# ============================================
# Session Configuration
# ============================================
# Session Secret
# IMPORTANT: Use a strong, randomly generated secret in production
# Generate with: openssl rand -base64 32
# This is used to encrypt session cookies
VITE_SESSION_SECRET=your-super-secret-key-change-this-in-production
# ============================================
# Optional Configuration
# ============================================
# OIDC Scopes (comma-separated)
# Default: openid,profile,email,offline_access,AbpTemplate
# Customize based on your provider's requirements
VITE_OIDC_SCOPES=openid,profile,email,offline_access,AbpTemplateThe application uses constants defined in src/infrastructure/constants.ts. These constants are automatically loaded from environment variables with sensible defaults:
export const OIDC_CONSTANTS = {
ISSUER: import.meta.env.VITE_OIDC_ISSUER || "https://your-oidc-provider.com",
CLIENT_ID: import.meta.env.VITE_OIDC_CLIENT_ID || "your-client-id",
CLIENT_SECRET: import.meta.env.VITE_OIDC_CLIENT_SECRET || "your-client-secret",
BASE_URL: import.meta.env.VITE_BASE_URL || "http://localhost:3000",
REDIRECT_URI: import.meta.env.VITE_OIDC_REDIRECT_URI || "http://localhost:3000/auth/callback",
SESSION_SECRET: import.meta.env.VITE_SESSION_SECRET || "your-super-secret-key-change-this-in-production",
SESSION_COOKIE_NAME: "tanstack-oidc-session",
SCOPES: ["openid", "profile", "email", "offline_access", "AbpTemplate"],
RESPONSE_TYPE: "code",
GRANT_TYPE: "authorization_code",
} as const;The application validates OIDC configuration on startup. If required values are missing or contain placeholder values, you'll see clear error messages:
- Missing Issuer: "OIDC configuration not properly set. Please configure VITE_OIDC_ISSUER"
- Missing Client ID: "OIDC configuration not properly set. Please configure VITE_OIDC_CLIENT_ID"
- Placeholder Client Secret: Warning (not an error, as it's optional for public clients)
Regardless of which OIDC provider you use, you need to configure the following:
In your OIDC provider's admin console:
- Navigate to Applications/Clients section
- Create a new application/client
- Note the Client ID and Client Secret (if required)
Add the following redirect URI(s):
- Development:
http://localhost:3000/auth/callback - Production:
https://your-domain.com/auth/callback
Important: The redirect URI must match exactly (including protocol, domain, port, and path).
Enable the following grant types:
- ✅ Authorization Code
- ✅ Refresh Token (for automatic token refresh)
Ensure these scopes are available:
openid(required)profile(for user profile information)email(for user email)offline_access(for refresh tokens)
Enable PKCE (Proof Key for Code Exchange) for enhanced security. Most modern OIDC providers support this.
-
Create Application:
- Go to Applications → Create Application
- Choose "Single Page Web Applications"
- Note the Domain, Client ID, and Client Secret
-
Configure URLs:
- Allowed Callback URLs:
http://localhost:3000/auth/callback - Allowed Logout URLs:
http://localhost:3000 - Allowed Web Origins:
http://localhost:3000
- Allowed Callback URLs:
-
Advanced Settings:
- OAuth → Grant Types: Enable "Authorization Code" and "Refresh Token"
- OAuth → OIDC Conformant: Enable
- OAuth → PKCE: Enable
-
Environment Variables:
VITE_OIDC_ISSUER=https://your-tenant.auth0.com VITE_OIDC_CLIENT_ID=your-auth0-client-id VITE_OIDC_CLIENT_SECRET=your-auth0-client-secret
-
Create Client:
- Go to Clients → Create
- Client ID:
your-client-id - Client Protocol:
openid-connect - Access Type:
public(for PKCE) orconfidential
-
Configure URLs:
- Valid Redirect URIs:
http://localhost:3000/auth/callback - Web Origins:
http://localhost:3000 - Base URL:
http://localhost:3000
- Valid Redirect URIs:
-
Configure Capabilities:
- Standard Flow Enabled: ✅
- Direct Access Grants Enabled: ✅ (optional)
- PKCE Code Challenge Method:
S256
-
Environment Variables:
VITE_OIDC_ISSUER=https://keycloak.example.com/realms/your-realm VITE_OIDC_CLIENT_ID=your-client-id VITE_OIDC_CLIENT_SECRET=your-client-secret # Only if confidential client
-
Register Application:
- Go to Azure Portal → Azure Active Directory → App registrations
- New registration
- Name: Your app name
- Supported account types: Choose appropriate
- Redirect URI:
http://localhost:3000/auth/callback(SPA platform)
-
Configure Authentication:
- Platform configurations → Single-page application
- Redirect URIs:
http://localhost:3000/auth/callback - Implicit grant and hybrid flows: Enable "ID tokens" and "Access tokens"
-
API Permissions:
- Microsoft Graph → Delegated permissions:
openidprofileemailoffline_access
- Microsoft Graph → Delegated permissions:
-
Environment Variables:
VITE_OIDC_ISSUER=https://login.microsoftonline.com/your-tenant-id/v2.0 VITE_OIDC_CLIENT_ID=your-azure-app-id # Client secret not needed for SPA apps with PKCE
-
Configure OpenIddict:
- In your ABP backend, configure OpenIddict application
- Client Type:
Public(for PKCE) - Consent Type:
Implicit - Permissions: Enable
GrantTypes.AuthorizationCodeandGrantTypes.RefreshToken
-
Configure Redirect URIs:
- Redirect URIs:
http://localhost:3000/auth/callback - Post Logout Redirect URIs:
http://localhost:3000
- Redirect URIs:
-
Scopes:
- Ensure scopes include:
openid,profile,email,offline_access,AbpTemplate
- Ensure scopes include:
-
Environment Variables:
VITE_OIDC_ISSUER=https://your-abp-backend.com VITE_OIDC_CLIENT_ID=your-abp-client-id # No client secret needed for public clients
- Node.js 18+ and pnpm (or npm/yarn)
- TanStack Start application
- OIDC provider account and configuration
The required dependencies should already be installed. If not, ensure these are in your package.json:
{
"dependencies": {
"@tanstack/react-query": "^5.x",
"@tanstack/react-router": "^1.x",
"@tanstack/react-start": "^1.x",
"openid-client": "^5.x"
}
}- Copy the example environment variables (see Configuration section)
- Update values with your OIDC provider details
- Generate a strong session secret:
openssl rand -base64 32
Start the development server:
pnpm devCheck the console for any configuration errors. The application will validate OIDC configuration on first use.
- Navigate to
http://localhost:3000 - Click login or navigate to a protected route
- You should be redirected to your OIDC provider
- After authentication, you'll be redirected back to
/dashboard
When a user clicks login or accesses a protected route:
// Client-side: useAuth hook
const { login } = useAuth();
await login(); // Calls GET /auth/loginServer-side (/auth/login route):
- Generates PKCE code verifier and challenge
- Generates random state parameter (CSRF protection)
- Stores state and code verifier in session
- Builds authorization URL with:
client_id: Your OIDC client IDredirect_uri: Your callback URLresponse_type:code(Authorization Code flow)scope: Requested scopesstate: Random state for CSRF protectioncode_challenge: PKCE code challengecode_challenge_method:S256
- Returns authorization URL to client
- Client redirects browser to OIDC provider
- User authenticates (login, MFA, etc.)
- OIDC provider validates credentials
- OIDC provider generates authorization code
After authentication, OIDC provider redirects to:
GET /auth/callback?code=AUTHORIZATION_CODE&state=STATE_PARAMETERServer-side (/auth/callback route):
- Extracts
codeandstatefrom URL parameters - Retrieves stored
stateandcodeVerifierfrom session - Validates
stateparameter (CSRF protection) - Exchanges authorization code for tokens:
- Sends
code,code_verifier,redirect_urito token endpoint - Receives
access_token,id_token,refresh_token
- Sends
- Creates user session:
- Extracts user claims from ID token
- Optionally fetches additional user info
- Stores tokens securely in server-side session
- Clears temporary OIDC session data
- Redirects to
/dashboard(or original destination)
- Session stored server-side using encrypted cookies
- Contains:
user,accessToken,refreshToken,idToken,expiresAt - Tokens never exposed to client-side JavaScript
- Automatic token refresh (15 minutes before expiration)
When accessing protected routes or calling /auth/me:
- Server checks token expiration
- If token expires within 15 minutes:
- Uses refresh token to get new access token
- Updates session with new tokens
- Returns fresh user data
When user clicks logout:
// Client-side
const { logout } = useAuth();
await logout(); // Calls GET /auth/logoutServer-side (/auth/logout route):
- Revokes access and refresh tokens (if supported)
- Builds end session URL (RP-initiated logout)
- Clears server-side session
- Redirects to OIDC provider's end session endpoint
- OIDC provider clears its session
- Redirects back to application home page
Use the ProtectedRoute component to wrap routes that require authentication:
// src/routes/dashboard.tsx
import { createFileRoute } from "@tanstack/react-router";
import { ProtectedRoute } from "@/features/auth/components/protected-route";
import Dashboard from "@/features/dashboard/components/dashboard";
export const Route = createFileRoute("/dashboard")({
component: () => (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
),
});Custom Loading State:
<ProtectedRoute
loading={
<div className="flex items-center justify-center min-h-screen">
<Spinner />
<p>Authenticating...</p>
</div>
}
>
<Dashboard />
</ProtectedRoute>Custom Fallback (Not Authenticated):
<ProtectedRoute
fallback={
<div>
<h2>Please log in</h2>
<button onClick={login}>Login</button>
</div>
}
>
<Dashboard />
</ProtectedRoute>Provides login, logout, and error management:
import { useAuth } from "@/features/auth/hooks/use-auth";
function MyComponent() {
const { login, logout, clearError, authState } = useAuth();
const { user, isAuthenticated, isLoading, error } = authState;
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={clearError}>Dismiss</button>
</div>
);
}
return (
<div>
{isAuthenticated ? (
<div>
<p>Welcome, {user?.name}!</p>
<p>Email: {user?.email}</p>
<button onClick={logout}>Logout</button>
</div>
) : (
<button onClick={login}>Login</button>
)}
</div>
);
}Lightweight hook for reading authentication state:
import { useAuthState } from "@/features/auth/hooks/use-auth";
function UserProfile() {
const { user, isAuthenticated, isLoading } = useAuthState();
if (isLoading) return <div>Loading...</div>;
if (!isAuthenticated) return <div>Not authenticated</div>;
return (
<div>
<h1>{user?.name}</h1>
<p>{user?.email}</p>
<p>Roles: {user?.roles?.join(", ")}</p>
</div>
);
}Combines auth state and functions in a single hook:
import { useAuthCombined } from "@/features/auth/hooks/use-auth";
function AuthButton() {
const { user, isAuthenticated, login, logout } = useAuthCombined();
return isAuthenticated ? (
<div>
<span>Hello, {user?.name}</span>
<button onClick={logout}>Logout</button>
</div>
) : (
<button onClick={login}>Login</button>
);
}User information is available through the auth hooks:
const { user } = useAuthState();
// Available user properties:
user?.sub // Subject (unique user identifier)
user?.name // Full name
user?.email // Email address
user?.email_verified // Email verification status
user?.picture // Profile picture URL
user?.preferred_username // Preferred username
user?.given_name // First name
user?.family_name // Last name
user?.roles // Array of user roles
user?.updated_at // Last update timestampCheck user roles for conditional rendering:
import { useAuthState } from "@/features/auth/hooks/use-auth";
function AdminPanel() {
const { user } = useAuthState();
const isAdmin = user?.roles?.includes("admin");
if (!isAdmin) {
return <div>Access denied. Admin role required.</div>;
}
return <div>Admin content here</div>;
}function Navigation() {
const { isAuthenticated, user } = useAuthState();
return (
<nav>
<Link to="/">Home</Link>
{isAuthenticated ? (
<>
<Link to="/dashboard">Dashboard</Link>
<Link to="/profile">Profile</Link>
<span>Welcome, {user?.name}</span>
</>
) : (
<button onClick={login}>Login</button>
)}
</nav>
);
}import { useNavigate } from "@tanstack/react-router";
import { useAuth } from "@/features/auth/hooks/use-auth";
function LoginButton() {
const { login } = useAuth();
const navigate = useNavigate();
const handleLogin = async () => {
try {
await login();
// User will be redirected to OIDC provider
// After callback, they'll be redirected to /dashboard
} catch (error) {
console.error("Login failed:", error);
}
};
return <button onClick={handleLogin}>Login</button>;
}Initiates the OIDC login flow.
Response:
{
"authUrl": "https://oidc-provider.com/authorize?..."
}Usage:
const response = await fetch("/auth/login");
const { authUrl } = await response.json();
window.location.href = authUrl;Handles the OIDC callback after authentication. This is called automatically by the OIDC provider.
Query Parameters:
code: Authorization code from OIDC providerstate: State parameter for CSRF protectionerror: Error code (if authentication failed)
Response: HTTP 302 redirect to /dashboard (or error page)
Logs out the user and initiates RP-initiated logout.
Response: HTTP 302 redirect to OIDC provider's end session endpoint, then back to home page
Usage:
window.location.href = "/auth/logout";Returns the current user's information and session status.
Response:
{
"user": {
"sub": "user-id",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"roles": ["user", "admin"]
},
"expiresAt": 1234567890000
}Error Response (401/403):
{
"user": null
}Usage:
const response = await fetch("/auth/me");
const { user, expiresAt } = await response.json();Retrieves OIDC configuration through discovery.
import { getOIDCConfig } from "@/infrastructure/auth/oidc";
const config = await getOIDCConfig();Generates authorization URL for login.
import { getAuthUrl } from "@/infrastructure/auth/oidc";
const { url, state, codeVerifier } = await getAuthUrl();Exchanges authorization code for tokens.
import { exchangeCodeForTokens } from "@/infrastructure/auth/oidc";
const tokenSet = await exchangeCodeForTokens(
new URL(request.url),
codeVerifier,
state
);Refreshes an access token using a refresh token.
import { refreshToken } from "@/infrastructure/auth/oidc";
const newTokenSet = await refreshToken(refreshToken);Retrieves the current user session (with automatic token refresh).
import { getUserSession } from "@/infrastructure/auth/auth-server";
const session = await getUserSession();
if (session) {
console.log(session.user);
console.log(session.accessToken);
}Creates a new user session from token response.
import { createSession } from "@/infrastructure/auth/auth-server";
const session = await createSession(tokenSet);Performs complete logout including token revocation.
import { performLogout } from "@/infrastructure/auth/auth-server";
const { endSessionUrl } = await performLogout();PKCE enhances security for public clients by:
- Code Verifier: Random cryptographically random string generated by client
- Code Challenge: SHA256 hash of code verifier
- Code Challenge Method:
S256(SHA256)
Flow:
- Client generates code verifier and challenge
- Sends challenge in authorization request
- Sends verifier in token exchange
- Server validates challenge matches verifier
This prevents authorization code interception attacks.
The state parameter prevents CSRF attacks:
- Generation: Random state generated on login
- Storage: Stored in server-side session
- Validation: State from callback must match stored state
- One-time Use: State is cleared after validation
- Server-Side Storage: Tokens stored in encrypted server-side sessions
- HttpOnly Cookies: Session cookies are HttpOnly (not accessible to JavaScript)
- Secure Cookies: Cookies marked as Secure in production (HTTPS only)
- SameSite: Cookies use
LaxSameSite policy - Encryption: Session data encrypted with strong secret
- No Client Exposure: Access tokens never exposed to client-side JavaScript
- Automatic Refresh: Tokens refreshed proactively before expiration
- Token Revocation: Tokens revoked on logout (if supported by provider)
- Expiration Handling: Expired tokens trigger automatic refresh or logout
- No Information Leakage: Error messages don't expose sensitive information
- User-Friendly Messages: Errors translated to user-friendly messages
- Logging: Server-side errors logged for debugging (not exposed to client)
Sessions are stored server-side with the following structure:
interface SessionData {
user: User; // User information from ID token
accessToken: string; // JWT access token
refreshToken?: string; // Refresh token (if available)
idToken?: string; // ID token
expiresAt: number; // Token expiration timestamp (ms)
}- Creation: Session created after successful token exchange
- Access: Session retrieved on each authenticated request
- Refresh: Tokens refreshed automatically 15 minutes before expiration
- Expiration: Session cleared if refresh fails or tokens expire
- Logout: Session cleared on explicit logout
Tokens are refreshed proactively:
- Threshold: 15 minutes before expiration (
TOKEN_REFRESH_THRESHOLD) - Automatic: Happens transparently on session access
- Locking: Refresh lock prevents concurrent refresh attempts
- Fallback: If refresh fails, session is cleared
Sessions use TanStack Start's built-in session management:
- Cookie-Based: Sessions stored in encrypted cookies
- Server-Side: Cookie contains encrypted session data
- Configuration: Configurable cookie options (secure, httpOnly, sameSite)
Error: "OIDC configuration not properly set"
- Cause: Missing or invalid OIDC configuration
- Solution: Check environment variables
Error: "OIDC configuration discovery failed"
- Cause: Cannot reach OIDC provider or invalid issuer URL
- Solution: Verify
VITE_OIDC_ISSUERis correct and accessible
Error: "Invalid client credentials"
- Cause: Wrong client ID or secret
- Solution: Verify
VITE_OIDC_CLIENT_IDandVITE_OIDC_CLIENT_SECRET
Error: "Invalid authorization code or PKCE verifier"
- Cause: Code expired, already used, or verifier mismatch
- Solution: Retry login (new code will be generated)
Error: "Redirect URI mismatch"
- Cause: Redirect URI doesn't match provider configuration
- Solution: Verify
VITE_OIDC_REDIRECT_URImatches provider settings
Error: "State mismatch"
- Cause: CSRF protection detected invalid state
- Solution: Retry login (new state will be generated)
Error: "Network error connecting to OIDC provider"
- Cause: Cannot reach OIDC provider
- Solution: Check network connectivity and provider URL
Error: "Token refresh failed"
- Cause: Refresh token invalid or expired
- Solution: User needs to login again
Error: "Failed to retrieve user information"
- Cause: Cannot fetch user info from provider
- Solution: Check provider's user info endpoint
function MyComponent() {
const { authState, clearError } = useAuth();
const { error } = authState;
if (error) {
return (
<div className="error-container">
<p>Authentication Error: {error}</p>
<button onClick={clearError}>Dismiss</button>
<button onClick={login}>Retry Login</button>
</div>
);
}
return <div>Content</div>;
}After failed authentication, users are redirected with error query parameters:
?error=auth_failed: General authentication failure?error=config_error: Configuration error?error=invalid_client: Invalid client credentials?error=invalid_code: Invalid authorization code?error=network_error: Network connectivity issue?error=missing_params: Missing required parameters?error=invalid_session: Invalid session data?error=state_mismatch: State validation failed
Handle these in your components:
import { useSearch } from "@tanstack/react-router";
function HomePage() {
const { error } = useSearch({ from: "/" });
useEffect(() => {
if (error) {
// Handle error
console.error("Auth error:", error);
}
}, [error]);
return <div>Home</div>;
}- Update all environment variables for production
- Generate strong session secret (
openssl rand -base64 32) - Configure production OIDC provider
- Update redirect URIs in OIDC provider
- Enable HTTPS
- Configure secure cookies
- Test authentication flow
- Test token refresh
- Test logout flow
- Review error handling
- Set up monitoring and logging
# Production OIDC Provider
VITE_OIDC_ISSUER=https://your-production-oidc-provider.com
VITE_OIDC_CLIENT_ID=your-production-client-id
VITE_OIDC_CLIENT_SECRET=your-production-client-secret
# Production Application URLs
VITE_BASE_URL=https://your-domain.com
VITE_OIDC_REDIRECT_URI=https://your-domain.com/auth/callback
# Strong Session Secret (generate new one)
VITE_SESSION_SECRET=<generated-strong-secret>Update your OIDC provider with production URLs:
- Redirect URIs:
https://your-domain.com/auth/callback - Post Logout Redirect URIs:
https://your-domain.com - Allowed Origins:
https://your-domain.com - CORS: Configure CORS for your domain
HTTPS is required for OIDC in production:
- SSL Certificate: Obtain SSL certificate (Let's Encrypt, etc.)
- Secure Cookies: Cookies automatically marked as Secure in production
- HSTS: Consider enabling HSTS headers
In production, sessions are automatically configured with:
- Secure: Cookies only sent over HTTPS
- HttpOnly: Cookies not accessible to JavaScript
- SameSite:
Laxpolicy (CSRF protection)
Set up monitoring for:
- Authentication success/failure rates
- Token refresh failures
- Session expiration events
- Error rates by type
Configure logging for:
- Authentication events (login, logout)
- Token refresh events
- Error events (with sanitized data)
- Security events (state mismatches, etc.)
Important: Never log sensitive data (tokens, secrets, etc.)
The application includes a test mode for development and E2E testing:
Enable Test Mode:
// In browser console or test setup
localStorage.setItem("test-mode", "true");
// or
sessionStorage.setItem("test-mode", "true");Test Mode Behavior:
/auth/mereturns mock user data- No actual OIDC provider calls
- Useful for E2E tests and development
Test authentication hooks:
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AuthProvider, useAuth } from "@/features/auth/hooks/use-auth";
test("useAuth provides login function", async () => {
const queryClient = new QueryClient();
const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
<AuthProvider>{children}</AuthProvider>
</QueryClientProvider>
);
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.login).toBeDefined();
expect(typeof result.current.login).toBe("function");
});Example Playwright test:
import { test, expect } from "@playwright/test";
test("user can login and access protected route", async ({ page }) => {
// Enable test mode
await page.addInitScript(() => {
localStorage.setItem("test-mode", "true");
});
await page.goto("/");
// Login
await page.click("text=Login");
// In test mode, this should work without actual OIDC provider
// Check protected route
await page.goto("/dashboard");
await expect(page.locator("text=Dashboard")).toBeVisible();
});For testing without a real OIDC provider:
// Mock OIDC discovery
vi.mock("@/infrastructure/auth/oidc", () => ({
getOIDCConfig: vi.fn().mockResolvedValue({
authorization_endpoint: "https://mock-oidc.com/authorize",
token_endpoint: "https://mock-oidc.com/token",
}),
}));Symptoms:
- Error in console: "OIDC configuration discovery failed"
- Login button doesn't work
Causes:
- Invalid issuer URL
- Network connectivity issues
- OIDC provider not accessible
Solutions:
- Verify
VITE_OIDC_ISSUERis correct and accessible - Test issuer URL in browser:
https://your-issuer/.well-known/openid-configuration - Check network connectivity and firewall rules
- Verify OIDC provider is running and accessible
Symptoms:
- Redirected to home page with
?error=state_mismatch - Authentication fails after OIDC provider redirect
Causes:
- Session expired between login and callback
- Multiple login attempts
- Browser blocking cookies
Solutions:
- Clear browser cookies and retry
- Ensure cookies are enabled
- Check session cookie configuration
- Verify session secret is consistent
Symptoms:
- OIDC provider shows error: "redirect_uri_mismatch"
- Authentication fails at provider
Causes:
- Redirect URI doesn't match provider configuration
- Protocol mismatch (http vs https)
- Port mismatch
- Path mismatch
Solutions:
- Verify
VITE_OIDC_REDIRECT_URImatches exactly (including protocol, port, path) - Check OIDC provider configuration
- Ensure redirect URI is registered in provider
- For development, use
http://localhost:3000/auth/callbackexactly
Symptoms:
- User logged out unexpectedly
- "Token refresh failed" in logs
Causes:
- Refresh token expired
- Refresh token revoked
- Provider doesn't support refresh tokens
Solutions:
- Check if provider supports refresh tokens
- Verify
offline_accessscope is requested - Check token expiration times
- Implement proper error handling for refresh failures
Symptoms:
- User logged out on page refresh
- Session data not available
Causes:
- Session cookie not set
- Cookie domain/path issues
- Session secret changed
Solutions:
- Check browser cookies (DevTools → Application → Cookies)
- Verify session cookie is present
- Check cookie domain and path settings
- Ensure session secret is consistent
- Check SameSite cookie policy
Symptoms:
- Browser console shows CORS errors
- API calls fail
Causes:
- OIDC provider not configured for your domain
- Missing CORS headers
Solutions:
- Configure CORS in OIDC provider
- Add your domain to allowed origins
- Check CORS headers in network tab
Symptoms:
- Error: "Invalid client credentials"
- Token exchange fails
Causes:
- Wrong client ID
- Wrong client secret
- Client not found in provider
Solutions:
- Verify
VITE_OIDC_CLIENT_IDis correct - Verify
VITE_OIDC_CLIENT_SECRETis correct (if required) - Check client configuration in OIDC provider
- For public clients, ensure client secret is optional
Enable debug logging:
# Set environment variable
DEBUG=oidc:*
# Or in code
console.log("OIDC Config:", await getOIDCConfig());Use browser DevTools to debug:
- Network Tab: Check all OIDC-related requests
- Application Tab: Check cookies and session storage
- Console Tab: Check for JavaScript errors
- Issue: "Invalid audience"
- Solution: Configure audience in Auth0 application settings
- Issue: "Invalid client"
- Solution: Ensure client access type matches (public vs confidential)
- Issue: "Invalid scope"
- Solution: Verify API permissions are granted and admin consent given
Configure custom scopes:
VITE_OIDC_SCOPES=openid,profile,email,offline_access,custom_scopeModify token refresh threshold in src/infrastructure/constants.ts:
// Refresh 30 minutes before expiration (instead of 15)
export const TOKEN_REFRESH_THRESHOLD = 30 * 60 * 1000;Modify session cookie name in src/infrastructure/constants.ts:
SESSION_COOKIE_NAME: "my-custom-session-name",Modify user info extraction in src/infrastructure/auth/auth-server.ts:
// Custom role extraction
function extractRoles(userInfo: Record<string, unknown>): string[] {
// Your custom logic here
const roles = userInfo.custom_roles_field;
return Array.isArray(roles) ? roles : [];
}To support multiple OIDC providers, you can:
- Create provider-specific configuration
- Use environment variables to select provider
- Implement provider abstraction layer
src/
├── infrastructure/
│ ├── auth/
│ │ ├── oidc.ts # OIDC client functions
│ │ ├── auth-server.ts # Server-side auth utilities
│ │ └── session.ts # Session management
│ └── constants.ts # OIDC configuration constants
├── features/
│ └── auth/
│ ├── components/
│ │ └── protected-route.tsx # Route protection component
│ └── hooks/
│ └── use-auth.tsx # Authentication hooks and context
└── routes/
├── __root.tsx # Root route with AuthProvider
└── auth/
├── login.ts # Login API route
├── callback.ts # Callback handler route
├── logout.ts # Logout API route
└── me.ts # User info API route
Contains OIDC client functions:
getOIDCConfig(): OIDC configuration discoverygetAuthUrl(): Generate authorization URLexchangeCodeForTokens(): Exchange code for tokensrefreshToken(): Refresh access tokengetUserInfo(): Fetch user informationrevokeToken(): Revoke tokensgetEndSessionUrl(): Build logout URL
Server-side authentication utilities:
getUserSession(): Get current session (with auto-refresh)createSession(): Create new sessionperformLogout(): Complete logout flow
Session management:
SessionDatainterfacesessionUtils: Session CRUD operationsuseAppSession(): React hook for session access
Client-side authentication:
AuthProvider: Context provideruseAuth(): Main auth hookuseAuthState(): Auth state hookuseAuthCombined(): Combined hook
Route protection component:
- Wraps routes requiring authentication
- Shows loading state
- Shows login prompt if not authenticated
For issues, questions, or contributions:
- Check this documentation first
- Review troubleshooting section
- Check GitHub issues
- Review OIDC provider documentation
- Consult TanStack Start documentation
Last Updated: December 2025
Version: 1.0.0
Maintainer: Development Team