Understanding Authentication State Management with MobX
Authentication is one of the most critical aspects of modern web applications. Managing authentication state—keeping track of logged-in users, tokens, session data, and permissions—can quickly become complex. MobX provides an elegant, reactive state management solution that makes authentication flows predictable, maintainable, and performant.
At its core, MobX uses the observable pattern: when observable data changes, any React component or derived computation that depends on it automatically updates. Applied to authentication, this means your UI instantly reflects login/logout events, token expiry, and permission changes without manual wiring or prop drilling.
Why MobX for Authentication?
- Reactive by default — Components automatically re-render when auth state changes, eliminating stale UI states
- Centralized auth store — All authentication logic lives in a single, testable class
- Type-safe observables — TypeScript support ensures tokens, user objects, and session data are properly typed
- Decoupled from UI — Auth logic works independently of React, making it portable across frameworks
- Computed properties — Derived values like
isAuthenticatedoruserRoleare automatically cached and recalculated
Setting Up a MobX Authentication Store
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Before diving into specific authentication strategies, let's establish a foundational store structure. We'll use MobX 6 with makeAutoObservable, which automatically applies observable, action, and computed decorators to class properties.
// src/stores/AuthStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
import { makePersistable } from 'mobx-persist-store'; // for persistence across reloads
export interface User {
id: string;
email: string;
name: string;
roles: string[];
avatar?: string;
}
export class AuthStore {
// Observable state
user: User | null = null;
accessToken: string | null = null;
refreshToken: string | null = null;
isAuthenticated: boolean = false;
isLoading: boolean = false;
error: string | null = null;
sessionExpiresAt: number | null = null; // Unix timestamp
constructor() {
makeAutoObservable(this);
// Optional: persist tokens across browser refreshes
makePersistable(this, {
name: 'AuthStore',
properties: ['accessToken', 'refreshToken', 'user', 'sessionExpiresAt'],
storage: window.localStorage,
});
}
// Computed getter — automatically recalculated when dependencies change
get isTokenExpired(): boolean {
if (!this.sessionExpiresAt) return true;
return Date.now() >= this.sessionExpiresAt;
}
get userRole(): string | null {
return this.user?.roles?.[0] ?? null;
}
get isAdmin(): boolean {
return this.user?.roles?.includes('admin') ?? false;
}
// Actions — methods that modify observable state
setAuthData(user: User, accessToken: string, refreshToken: string, expiresIn: number) {
this.user = user;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.isAuthenticated = true;
this.error = null;
this.sessionExpiresAt = Date.now() + expiresIn * 1000;
}
clearAuthData() {
this.user = null;
this.accessToken = null;
this.refreshToken = null;
this.isAuthenticated = false;
this.sessionExpiresAt = null;
this.error = null;
}
setLoading(loading: boolean) {
this.isLoading = loading;
}
setError(error: string | null) {
this.error = error;
}
}
This store forms the backbone of all authentication strategies. The observable properties track every piece of auth state, computed getters derive useful information, and actions provide controlled mutation points.
JWT Authentication with MobX
JSON Web Tokens (JWT) are the dominant stateless authentication mechanism. A JWT is a compact, URL-safe token that carries encoded claims. The typical flow involves receiving an access token and optionally a refresh token after successful login, then attaching the access token to API requests via the Authorization header.
JWT Login Flow
The login action sends credentials to the server, receives tokens, and updates the store. A crucial aspect is token refresh logic to handle expired access tokens seamlessly.
// src/stores/AuthStore.ts — JWT-specific methods added to our base store
import axios from 'axios';
// Extend AuthStore with JWT methods
export class AuthStore {
// ... previous properties remain the same
/**
* Login with email/password credentials.
* On success, stores JWT tokens and user data.
*/
async login(email: string, password: string) {
this.setLoading(true);
this.setError(null);
try {
const response = await axios.post('/api/auth/login', {
email,
password,
});
const { user, accessToken, refreshToken, expiresIn } = response.data;
// runInAction ensures all changes are batched in a single reaction
runInAction(() => {
this.setAuthData(user, accessToken, refreshToken, expiresIn);
this.setLoading(false);
});
return true;
} catch (err: any) {
runInAction(() => {
this.setError(err.response?.data?.message || 'Login failed');
this.setLoading(false);
});
return false;
}
}
/**
* Register a new user account.
*/
async register(email: string, password: string, name: string) {
this.setLoading(true);
this.setError(null);
try {
const response = await axios.post('/api/auth/register', {
email,
password,
name,
});
const { user, accessToken, refreshToken, expiresIn } = response.data;
runInAction(() => {
this.setAuthData(user, accessToken, refreshToken, expiresIn);
this.setLoading(false);
});
return true;
} catch (err: any) {
runInAction(() => {
this.setError(err.response?.data?.message || 'Registration failed');
this.setLoading(false);
});
return false;
}
}
/**
* Refresh the access token using the stored refresh token.
* Call this when a 401 response indicates the access token has expired.
*/
async refreshAccessToken() {
if (!this.refreshToken) {
this.clearAuthData();
return false;
}
try {
const response = await axios.post('/api/auth/refresh', {
refreshToken: this.refreshToken,
});
const { accessToken, refreshToken: newRefreshToken, expiresIn } = response.data;
runInAction(() => {
this.accessToken = accessToken;
this.refreshToken = newRefreshToken || this.refreshToken;
this.sessionExpiresAt = Date.now() + expiresIn * 1000;
});
return true;
} catch (err) {
runInAction(() => {
this.clearAuthData();
});
return false;
}
}
/**
* Logout — invalidates tokens on the server and clears local state.
*/
async logout() {
this.setLoading(true);
try {
// Attempt server-side token invalidation
if (this.refreshToken) {
await axios.post('/api/auth/logout', {
refreshToken: this.refreshToken,
});
}
} catch (err) {
// Even if server call fails, clear local state
console.warn('Server logout failed, clearing local state');
} finally {
runInAction(() => {
this.clearAuthData();
this.setLoading(false);
});
}
}
}
Axios Interceptor for Automatic Token Refresh
A powerful pattern is intercepting HTTP requests to attach the JWT and handle 401 responses automatically. This keeps the refresh logic transparent to the rest of the application.
// src/api/httpClient.ts
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { authStore } from '../stores';
const httpClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 15000,
});
// Flag to prevent infinite refresh loops
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string | null) => void;
reject: (error: AxiosError) => void;
}> = [];
const processQueue = (error: AxiosError | null, token: string | null = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve(token);
}
});
failedQueue = [];
};
// Request interceptor: attach JWT to every outgoing request
httpClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = authStore.accessToken;
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor: handle 401 and refresh token
httpClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Only handle 401 errors that haven't been retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
// If already refreshing, queue this request
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: (token: string | null) => {
if (token && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${token}`;
}
resolve(httpClient(originalRequest));
},
reject,
});
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshed = await authStore.refreshAccessToken();
if (refreshed && authStore.accessToken) {
// Update the original request with new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${authStore.accessToken}`;
}
// Process queued requests with new token
processQueue(null, authStore.accessToken);
return httpClient(originalRequest);
} else {
// Refresh failed — force logout
processQueue(error, null);
authStore.clearAuthData();
// Optionally redirect to login
window.location.href = '/login';
return Promise.reject(error);
}
} catch (refreshError) {
processQueue(error, null);
authStore.clearAuthData();
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
export default httpClient;
React Component Integration with JWT Auth
React components observe the MobX store using the observer wrapper from mobx-react-lite. Here's a complete login form and protected route example.
// src/components/LoginForm.tsx
import React, { useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useStore } from '../stores/StoreContext';
const LoginForm: React.FC = observer(() => {
const { authStore } = useStore();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const success = await authStore.login(email, password);
if (success) {
// Navigation will happen reactively based on isAuthenticated
}
};
return (
<form onSubmit={handleSubmit} className="auth-form">
<h2>Sign In</h2>
{authStore.error && (
<div className="error-banner">{authStore.error}</div>
)}
<label>
Email
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={authStore.isLoading}
required
/>
</label>
<label>
Password
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={authStore.isLoading}
required
/>
</label>
<button type="submit" disabled={authStore.isLoading}>
{authStore.isLoading ? 'Signing in...' : 'Sign In'}
</button>
</form>
);
});
export default LoginForm;
// src/components/ProtectedRoute.tsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { Navigate, Outlet } from 'react-router-dom';
import { useStore } from '../stores/StoreContext';
interface ProtectedRouteProps {
requiredRole?: string;
children?: React.ReactNode;
}
const ProtectedRoute: React.FC = observer(({ requiredRole, children }) => {
const { authStore } = useStore();
// Show loading spinner while checking auth state
if (authStore.isLoading) {
return <div className="auth-loading">Verifying credentials...</div>;
}
// Redirect to login if not authenticated
if (!authStore.isAuthenticated || authStore.isTokenExpired) {
return <Navigate to="/login" replace />;
}
// Check role-based access
if (requiredRole && authStore.userRole !== requiredRole && !authStore.isAdmin) {
return <Navigate to="/unauthorized" replace />;
}
return children ? <>{children}</> : <Outlet />;
});
export default ProtectedRoute;
Session-Based Authentication with MobX
Session-based authentication relies on the server maintaining user session state, typically via a session cookie. Unlike JWT, the client doesn't store a cryptographic token—instead, a session ID is stored in an HTTP-only cookie that's automatically sent with requests.
Session Auth Store Adaptation
The MobX store for session auth is simpler since token management moves to the server side. However, we still need to track the authenticated user and session status.
// src/stores/SessionAuthStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
import axios from 'axios';
interface SessionUser {
id: string;
email: string;
name: string;
roles: string[];
sessionId: string;
lastActivity: string;
}
export class SessionAuthStore {
user: SessionUser | null = null;
isAuthenticated: boolean = false;
isLoading: boolean = false;
error: string | null = null;
sessionTimeout: number | null = null; // milliseconds until session expires
// CSRF token for secure form submissions
csrfToken: string | null = null;
constructor() {
makeAutoObservable(this);
}
get isSessionActive(): boolean {
return this.isAuthenticated && this.user !== null;
}
get userDisplayName(): string {
return this.user?.name ?? 'Guest';
}
/**
* Fetch current session on app initialization.
* The server checks the session cookie and returns user data if valid.
*/
async fetchSession() {
this.setLoading(true);
try {
// Credentials: 'include' ensures cookies are sent
const response = await axios.get('/api/auth/session', {
withCredentials: true,
});
if (response.data.user) {
runInAction(() => {
this.user = response.data.user;
this.isAuthenticated = true;
this.sessionTimeout = response.data.timeout;
this.csrfToken = response.data.csrfToken;
this.isLoading = false;
});
} else {
runInAction(() => {
this.clearSession();
this.isLoading = false;
});
}
} catch (err) {
runInAction(() => {
this.clearSession();
this.isLoading = false;
});
}
}
/**
* Login with session-based credentials.
* The server sets an HTTP-only session cookie on success.
*/
async login(email: string, password: string) {
this.setLoading(true);
this.setError(null);
try {
const response = await axios.post(
'/api/auth/login',
{ email, password },
{ withCredentials: true }
);
runInAction(() => {
this.user = response.data.user;
this.isAuthenticated = true;
this.sessionTimeout = response.data.timeout;
this.csrfToken = response.data.csrfToken;
this.setLoading(false);
});
return true;
} catch (err: any) {
runInAction(() => {
this.setError(err.response?.data?.message || 'Login failed');
this.setLoading(false);
});
return false;
}
}
/**
* Logout — server destroys the session and clears the cookie.
*/
async logout() {
this.setLoading(true);
try {
await axios.post(
'/api/auth/logout',
{ csrfToken: this.csrfToken },
{ withCredentials: true }
);
} catch (err) {
console.warn('Logout request failed');
} finally {
runInAction(() => {
this.clearSession();
this.setLoading(false);
});
}
}
clearSession() {
this.user = null;
this.isAuthenticated = false;
this.sessionTimeout = null;
this.csrfToken = null;
this.error = null;
}
setLoading(loading: boolean) {
this.isLoading = loading;
}
setError(error: string | null) {
this.error = error;
}
}
Session Heartbeat / Keep-Alive Pattern
For long-lived sessions, implement a heartbeat mechanism to keep the session alive and detect when the user has been logged out on another tab.
// src/stores/SessionAuthStore.ts — add heartbeat logic
export class SessionAuthStore {
// ... previous code
private heartbeatInterval: ReturnType | null = null;
private readonly HEARTBEAT_INTERVAL_MS = 60000; // 1 minute
/**
* Start periodic session checks.
* Call this after successful login or session fetch.
*/
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = setInterval(async () => {
try {
const response = await axios.get('/api/auth/heartbeat', {
withCredentials: true,
});
if (response.data.valid) {
runInAction(() => {
this.sessionTimeout = response.data.timeout;
if (response.data.csrfToken) {
this.csrfToken = response.data.csrfToken;
}
});
} else {
runInAction(() => {
this.clearSession();
});
this.stopHeartbeat();
}
} catch (err) {
// Session expired or network error
runInAction(() => {
this.clearSession();
});
this.stopHeartbeat();
}
}, this.HEARTBEAT_INTERVAL_MS);
}
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* Cross-tab synchronization using BroadcastChannel API.
* When the user logs out in one tab, all other tabs are notified.
*/
private broadcastChannel: BroadcastChannel | null = null;
initCrossTabSync() {
this.broadcastChannel = new BroadcastChannel('auth-sync');
this.broadcastChannel.onmessage = (event) => {
if (event.data.type === 'LOGOUT') {
runInAction(() => {
this.clearSession();
});
} else if (event.data.type === 'LOGIN') {
// Refetch session rather than trusting the broadcast payload
this.fetchSession();
}
};
}
broadcastAuthChange(type: 'LOGIN' | 'LOGOUT') {
this.broadcastChannel?.postMessage({ type });
}
}
OAuth Integration with MobX
OAuth 2.0 is the standard for delegated authorization, commonly used for "Sign in with Google/GitHub/Microsoft" flows. The Authorization Code flow with PKCE is recommended for single-page applications. MobX excels here because OAuth flows involve multiple asynchronous steps with distinct state transitions.
OAuth Flow State Machine
An OAuth login goes through several stages: initiating the request, redirecting to the provider, handling the callback, exchanging the code for tokens, and finally fetching user data. Modeling this as observable state makes the UI responsive at every step.
// src/stores/OAuthStore.ts
import { makeAutoObservable, runInAction } from 'mobx';
import axios from 'axios';
import { AuthStore } from './AuthStore';
type OAuthProvider = 'google' | 'github' | 'microsoft' | 'apple';
interface OAuthConfig {
provider: OAuthProvider;
clientId: string;
redirectUri: string;
scope: string;
authorizationUrl: string;
tokenUrl: string;
userInfoUrl: string;
}
export class OAuthStore {
// OAuth-specific observable state
currentProvider: OAuthProvider | null = null;
oauthStep: 'idle' | 'redirecting' | 'exchanging' | 'fetching-user' | 'complete' | 'error' = 'idle';
oauthError: string | null = null;
// PKCE code verifier and state (for CSRF protection)
private codeVerifier: string | null = null;
private oauthState: string | null = null;
// Reference to the main auth store for final token storage
private authStore: AuthStore;
constructor(authStore: AuthStore) {
makeAutoObservable(this);
this.authStore = authStore;
}
// Provider configurations
private providers: Record = {
google: {
provider: 'google',
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
redirectUri: `${window.location.origin}/oauth/callback/google`,
scope: 'openid email profile',
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo',
},
github: {
provider: 'github',
clientId: import.meta.env.VITE_GITHUB_CLIENT_ID,
redirectUri: `${window.location.origin}/oauth/callback/github`,
scope: 'user:email read:user',
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
},
// ... microsoft, apple configurations follow the same pattern
};
/**
* Generate a cryptographically random string for PKCE.
* Uses crypto.subtle API available in modern browsers.
*/
private async generateCodeVerifier(): Promise {
const array = new Uint8Array(64);
crypto.getRandomValues(array);
const base64 = btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return base64.substring(0, 128);
}
/**
* Create a SHA-256 code challenge from the verifier.
*/
private async generateCodeChallenge(verifier: string): Promise {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const base64 = btoa(String.fromCharCode(...hashArray))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return base64;
}
/**
* Initiate OAuth login for a provider.
* Generates PKCE challenge, stores verifier, and redirects.
*/
async initiateOAuthLogin(provider: OAuthProvider) {
const config = this.providers[provider];
if (!config) {
this.oauthStep = 'error';
this.oauthError = `Unknown provider: ${provider}`;
return;
}
runInAction(() => {
this.currentProvider = provider;
this.oauthStep = 'redirecting';
this.oauthError = null;
});
try {
// Generate PKCE values
this.codeVerifier = await this.generateCodeVerifier();
const codeChallenge = await this.generateCodeChallenge(this.codeVerifier);
// Generate state parameter for CSRF protection
this.oauthState = crypto.randomUUID();
// Store state in sessionStorage for callback verification
sessionStorage.setItem('oauth_state', this.oauthState);
sessionStorage.setItem('oauth_verifier', this.codeVerifier);
sessionStorage.setItem('oauth_provider', provider);
// Build authorization URL
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectUri,
response_type: 'code',
scope: config.scope,
state: this.oauthState,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
access_type: 'offline', // for refresh tokens (Google-specific)
prompt: 'consent',
});
// Redirect to the provider's authorization endpoint
window.location.href = `${config.authorizationUrl}?${params.toString()}`;
} catch (err) {
runInAction(() => {
this.oauthStep = 'error';
this.oauthError = 'Failed to initiate OAuth flow';
});
}
}
/**
* Handle the OAuth callback.
* Extracts authorization code from URL, verifies state, and exchanges for tokens.
*/
async handleOAuthCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const error = urlParams.get('error');
// Check for provider-reported errors
if (error) {
runInAction(() => {
this.oauthStep = 'error';
this.oauthError = `OAuth error: ${error}`;
});
return false;
}
// Verify state parameter to prevent CSRF
const storedState = sessionStorage.getItem('oauth_state');
if (!code || !state || state !== storedState) {
runInAction(() => {
this.oauthStep = 'error';
this.oauthError = 'Invalid OAuth callback — state mismatch';
});
return false;
}
// Retrieve stored PKCE verifier and provider
const verifier = sessionStorage.getItem('oauth_verifier');
const provider = sessionStorage.getItem('oauth_provider') as OAuthProvider;
if (!verifier || !provider) {
runInAction(() => {
this.oauthStep = 'error';
this.oauthError = 'Missing PKCE verifier — possible replay attack';
});
return false;
}
runInAction(() => {
this.oauthStep = 'exchanging';
this.currentProvider = provider;
});
const config = this.providers[provider];
try {
// Exchange authorization code for tokens
const tokenResponse = await axios.post(
config.tokenUrl,
new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
code_verifier: verifier,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
const { access_token, refresh_token, expires_in } = tokenResponse.data;
runInAction(() => {
this.oauthStep = 'fetching-user';
});
// Fetch user info from the provider
const userResponse = await axios.get(config.userInfoUrl, {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
// Normalize user data to your application's User shape
const normalizedUser = this.normalizeUserData(provider, userResponse.data);
runInAction(() => {
// Store in the main auth store
this.authStore.setAuthData(
normalizedUser,
access_token,
refresh_token,
expires_in
);
this.oauthStep = 'complete';
});
// Clean up OAuth artifacts
sessionStorage.removeItem('oauth_state');
sessionStorage.removeItem('oauth_verifier');
sessionStorage.removeItem('oauth_provider');
// Clean URL
window.history.replaceState({}, document.title, window.location.pathname);
return true;
} catch (err: any) {
runInAction(() => {
this.oauthStep = 'error';
this.oauthError = err.response?.data?.error_description || 'Token exchange failed';
});
return false;
}
}
/**
* Normalize provider-specific user data to your application's User interface.
*/
private normalizeUserData(provider: OAuthProvider, raw: any): import('./AuthStore').User {
switch (provider) {
case 'google':
return {
id: raw.sub,
email: raw.email,
name: raw.name,
roles: ['user'], // Default role for OAuth users
avatar: raw.picture,
};
case 'github':
return {
id: raw.id.toString(),
email: raw.email || `${raw.login}@github.com`,
name: raw.name || raw.login,
roles: ['user'],
avatar: raw.avatar_url,
};
// ... microsoft, apple normalization
default:
throw new Error(`Unknown provider: ${provider}`);
}
}
/**
* Reset OAuth-specific state back to idle.
*/
resetOAuth() {
runInAction(() => {
this.currentProvider = null;
this.oauthStep = 'idle';
this.oauthError = null;
});
}
}
OAuth Callback Handler Component
This component lives at the callback route and processes the OAuth response on mount.
// src/components/OAuthCallback.tsx
import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { useNavigate } from 'react-router-dom';
import { useStore } from '../stores/StoreContext';
const OAuthCallback: React.FC = observer(() => {
const { oauthStore } = useStore();
const navigate = useNavigate();
useEffect(() => {
const processCallback = async () => {
const success = await oauthStore.handleOAuthCallback();
if (success) {
navigate('/dashboard', { replace: true });
} else {
// Stay on callback page to show error, or redirect to login
setTimeout(() => {
navigate('/login', { replace: true });
oauthStore.resetOAuth();
}, 3000);
}
};
processCallback();
}, []);
return (
<div className="oauth-callback">
{oauthStore.oauthStep === 'exchanging' && (
<div className="oauth-status">
<h2>Completing Authentication</h2>
<p>Exchanging authorization code for secure tokens...</p>
</div>
)}
{oauthStore.oauthStep === 'fetching-user' && (
<div className="oauth-status">
<h2>Almost There</h2>
<p>Retrieving your profile from {oauthStore.currentProvider}...</p>
</div>
)}
{oauthStore.oauthStep === 'error' && (
<div className="oauth-error">
<h2>Authentication Failed</h2>
<p>{oauthStore.oauthError}</p>
<p>Redirecting to login page...</p>
</div>
)}
</div>
);
});
export default OAuthCallback;
OAuth Provider Buttons
// src/components/OAuthButtons.tsx
import React from 'react';
import { observer } from 'mobx-react-lite';
import { useStore } from '../stores/StoreContext';
const OAuthButtons: React.FC = observer(() => {
const { oauthStore } = useStore();
const providers = [
{ id: 'google' as const, label: 'Google', icon: '🔵', color: '#4285F4' },
{ id: 'github' as const, label: 'GitHub', icon: '🐙', color: '#24292e' },
{ id: 'microsoft' as const, label: 'Microsoft', icon: '🔷', color: '#00A4EF' },
];
return (
<div className="oauth-providers">
<p className="divider">Or sign in with</p>
<div className="provider-buttons">
{providers.map(({ id, label, icon, color }) => (
<button
key={id}
className="oauth-btn"
style={{ borderColor: color, color }}
onClick={() => oauthStore.initiateOAuthLogin(id)}
disabled={oauthStore.oauthStep !== 'idle'}
>
<span className="provider-icon">{icon}</span>
{label}
</button>
))}
</div>
{oauthStore.oauthStep === 'redirecting' && (
<p className="redirecting-notice">
Redirecting to {oauthStore.currentProvider}...
</p>
)}
</div>
);
});
export default OAuthButtons;
Combining All Three Strategies in a Unified Store
In a real application, you might support multiple authentication methods simultaneously. Here's how to orchestrate JWT, session, and OAuth in a single cohesive store.
// src/stores/UnifiedAuthStore.ts
import { makeAutoObservable,