Master Front‑End Single Sign‑On (SSO) with React: Complete Guide & Code Samples
This comprehensive tutorial explains front‑end Single Sign‑On (SSO) concepts, covering cookie‑based, token‑based (JWT), and OAuth 2.0 implementations with detailed React code, session checking, token refresh, API interception, cross‑origin solutions, and security best practices.
Single Sign‑On (SSO) Front‑End Implementation Guide
Single Sign‑On (SSO) is an authentication mechanism that lets users access multiple applications with a single set of credentials. The following sections detail a complete front‑end SSO workflow.
1. SSO Architecture Overview
SSO Server : Central authentication service responsible for verifying user identity.
Client Applications : Individual apps that require user login.
User Browser : The interface through which users interact.
2. Cookie‑Based SSO Implementation
2.1 Login Flow Code
// Front‑end application entry component
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
user: null,
isLoading: true,
};
}
componentDidMount() {
// Check if user is already logged in
this.checkLoginStatus();
}
checkLoginStatus = async () => {
try {
// Call local verification API, check if there is a valid session
const response = await fetch('https://app1.example.com/api/auth/status', {
credentials: 'include' // important: include cross‑origin cookies
});
if (response.ok) {
const data = await response.json();
if (data.isAuthenticated) {
this.setState({
isAuthenticated: true,
user: data.user,
isLoading: false,
});
return;
}
}
// If not logged in, redirect to SSO login page
this.redirectToSSOLogin();
} catch (error) {
console.error('Failed to verify login status:', error);
this.setState({ isLoading: false });
}
};
redirectToSSOLogin = () => {
// Current app URL, used for redirect back after login
const currentUrl = encodeURIComponent(window.location.href);
// Redirect to SSO login page
window.location.href = `https://sso.example.com/login?redirect=${currentUrl}`;
};
render() {
const { isAuthenticated, user, isLoading } = this.state;
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <div>Redirecting to login page...</div>;
}
return (
<div>
<header>
<p>Welcome, {user.name}</p>
<button onClick={this.handleLogout}>Logout</button>
</header>
<main>{/* Application content */}</main>
</div>
);
}
handleLogout = async () => {
try {
await fetch('https://sso.example.com/logout', {
method: 'POST',
credentials: 'include',
});
// After logout, redirect to login page
window.location.href = 'https://sso.example.com/login';
} catch (error) {
console.error('Logout failed:', error);
}
};
}2.2 SSO Login Page Implementation
// SSO server login page component
class SSOLoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
error: null,
isLoading: false,
};
}
handleInputChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
handleSubmit = async (e) => {
e.preventDefault();
this.setState({ isLoading: true, error: null });
try {
const { username, password } = this.state;
const response = await fetch('https://sso.example.com/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'include',
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const { token } = await response.json();
// Redirect back to the application with the token
window.location.href = `${decodeURIComponent(callbackUrl)}?token=${token}`;
} catch (error) {
this.setState({ error: error.message, isLoading: false });
}
};
render() {
const { username, password, error, isLoading } = this.state;
return (
<div className="login-container">
<h2>Unified Login Platform</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input type="text" id="username" name="username" value={username} onChange={this.handleInputChange} required />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" value={password} onChange={this.handleInputChange} required />
</div>
<button type="submit" disabled={isLoading}>{isLoading ? 'Logging in...' : 'Login'}</button>
</form>
</div>
);
}
}3. Token‑Based SSO Implementation (JWT)
3.1 Front‑End Application Entry
// Front‑end application using JWT for SSO
class TokenBasedApp extends React.Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
user: null,
isLoading: true,
};
}
componentDidMount() {
// Check if URL contains a token parameter (redirected back from SSO server)
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
// Save token to localStorage
localStorage.setItem('auth_token', token);
// Remove token from URL
window.history.replaceState({}, document.title, window.location.pathname);
}
// Validate token
this.validateToken();
}
validateToken = async () => {
const token = localStorage.getItem('auth_token');
if (!token) {
this.setState({ isLoading: false });
this.redirectToSSOLogin();
return;
}
try {
const response = await fetch('https://app1.example.com/api/auth/validate', {
headers: { 'Authorization': `Bearer ${token}` },
});
if (response.ok) {
const userData = await response.json();
this.setState({ isAuthenticated: true, user: userData, isLoading: false });
} else {
// Token invalid, clear and redirect to login
localStorage.removeItem('auth_token');
this.setState({ isLoading: false });
this.redirectToSSOLogin();
}
} catch (error) {
console.error('Token validation failed:', error);
this.setState({ isLoading: false });
this.redirectToSSOLogin();
}
};
redirectToSSOLogin = () => {
const appId = 'app1';
const callbackUrl = encodeURIComponent(window.location.origin);
window.location.href = `https://sso.example.com/login?appId=${appId}&callback=${callbackUrl}`;
};
handleLogout = () => {
// Clear local token
localStorage.removeItem('auth_token');
const callbackUrl = encodeURIComponent(window.location.origin);
window.location.href = `https://sso.example.com/logout?callback=${callbackUrl}`;
};
render() {
const { isAuthenticated, user, isLoading } = this.state;
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return <div>Redirecting to login page...</div>;
}
return (
<div>
<header>
<p>Welcome, {user.name}</p>
<button onClick={this.handleLogout}>Logout</button>
</header>
<main>{/* Application content */}</main>
</div>
);
}
}3.2 JWT Login Page
// SSO server JWT login page component
class JWTLoginPage extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
error: null,
isLoading: false,
};
}
handleInputChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
handleSubmit = async (e) => {
e.preventDefault();
this.setState({ isLoading: true, error: null });
try {
const { username, password } = this.state;
const urlParams = new URLSearchParams(window.location.search);
const appId = urlParams.get('appId');
const callbackUrl = urlParams.get('callback');
if (!appId || !callbackUrl) {
throw new Error('Missing required parameters');
}
const response = await fetch('https://sso.example.com/api/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, appId }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Login failed');
}
const { token } = await response.json();
// Redirect back to the app with token
window.location.href = `${decodeURIComponent(callbackUrl)}?token=${token}`;
} catch (error) {
this.setState({ error: error.message, isLoading: false });
}
};
render() {
const { username, password, error, isLoading } = this.state;
return (
<div className="login-container">
<h2>Unified Login Platform</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="username">Username</label>
<input type="text" id="username" name="username" value={username} onChange={this.handleInputChange} required />
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<input type="password" id="password" name="password" value={password} onChange={this.handleInputChange} required />
</div>
<button type="submit" disabled={isLoading}>{isLoading ? 'Logging in...' : 'Login'}</button>
</form>
</div>
);
}
}4. OAuth 2.0 Based SSO
4.1 Front‑End OAuth Flow
// Front‑end application using OAuth 2.0 for SSO
class OAuthApp extends React.Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: false,
user: null,
isLoading: true,
};
// OAuth configuration
this.oauthConfig = {
clientId: 'your-client-id',
redirectUri: `${window.location.origin}/callback`,
authorizationEndpoint: 'https://sso.example.com/oauth/authorize',
tokenEndpoint: 'https://sso.example.com/oauth/token',
scope: 'profile email',
};
}
componentDidMount() {
if (window.location.pathname === '/callback') {
this.handleOAuthCallback();
} else {
this.checkAuthentication();
}
}
checkAuthentication = () => {
const accessToken = localStorage.getItem('access_token');
const tokenExpiry = localStorage.getItem('token_expiry');
// Check token existence and expiry
if (accessToken && tokenExpiry && new Date().getTime() < parseInt(tokenExpiry)) {
this.fetchUserInfo(accessToken);
} else {
if (accessToken) {
localStorage.removeItem('access_token');
localStorage.removeItem('token_expiry');
localStorage.removeItem('refresh_token');
}
this.setState({ isLoading: false });
}
};
fetchUserInfo = async (accessToken) => {
try {
const response = await fetch('https://sso.example.com/api/userinfo', {
headers: { 'Authorization': `Bearer ${accessToken}` },
});
if (response.ok) {
const userData = await response.json();
this.setState({ isAuthenticated: true, user: userData, isLoading: false });
} else {
this.setState({ isLoading: false });
this.initiateOAuthFlow();
}
} catch (error) {
console.error('Failed to fetch user info:', error);
this.setState({ isLoading: false });
}
};
handleOAuthCallback = async () => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const savedState = localStorage.getItem('oauth_state');
localStorage.removeItem('oauth_state');
if (!code || state !== savedState) {
this.setState({ isLoading: false, error: 'Invalid OAuth callback' });
return;
}
try {
const tokenResponse = await fetch(this.oauthConfig.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: this.oauthConfig.redirectUri,
client_id: this.oauthConfig.clientId,
}),
});
if (!tokenResponse.ok) {
throw new Error('Failed to obtain access token');
}
const tokenData = await tokenResponse.json();
// Save tokens
localStorage.setItem('access_token', tokenData.access_token);
localStorage.setItem('token_expiry', (new Date().getTime() + tokenData.expires_in * 1000).toString());
if (tokenData.refresh_token) {
localStorage.setItem('refresh_token', tokenData.refresh_token);
}
await this.fetchUserInfo(tokenData.access_token);
// Redirect to app home page
window.history.replaceState({}, document.title, '/');
} catch (error) {
console.error('OAuth callback handling failed:', error);
this.setState({ isLoading: false, error: error.message });
}
};
initiateOAuthFlow = () => {
// Generate random state to prevent CSRF
const state = Math.random().toString(36).substring(2);
localStorage.setItem('oauth_state', state);
// Build authorization URL
const authUrl = new URL(this.oauthConfig.authorizationEndpoint);
authUrl.searchParams.append('client_id', this.oauthConfig.clientId);
authUrl.searchParams.append('redirect_uri', this.oauthConfig.redirectUri);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', this.oauthConfig.scope);
authUrl.searchParams.append('state', state);
// Redirect to authorization page
window.location.href = authUrl.toString();
};
handleLogout = async () => {
// Clear stored tokens
localStorage.removeItem('access_token');
localStorage.removeItem('token_expiry');
localStorage.removeItem('refresh_token');
// Redirect to SSO logout page
window.location.href = `https://sso.example.com/logout?redirect_uri=${encodeURIComponent(window.location.origin)}`;
};
render() {
const { isAuthenticated, user, isLoading, error } = this.state;
if (isLoading) {
return <div>Loading...</div>;
}
if (error) {
return <div className="error-message">{error}</div>;
}
if (!isAuthenticated) {
return (
<div>
<h2>Please log in to continue</h2>
<button onClick={this.initiateOAuthFlow}>Login with SSO</button>
</div>
);
}
return (
<div>
<header>
<p>Welcome, {user.name}</p>
<button onClick={this.handleLogout}>Logout</button>
</header>
<main>{/* Application content */}</main>
</div>
);
}
}5. Cross‑Domain Solutions
// Utility functions for handling cross‑origin SSO
const SSOUtils = {
// Set CORS options
getCorsOptions() {
return {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
};
},
// Use hidden iframe for cross‑domain communication
setupIframeMessaging() {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://sso.example.com/session-bridge.html';
document.body.appendChild(iframe);
return new Promise((resolve) => {
window.addEventListener('message', function messageHandler(event) {
if (event.origin !== 'https://sso.example.com') return;
if (event.data.type === 'SESSION_INFO') {
window.removeEventListener('message', messageHandler);
resolve(event.data.payload);
}
});
});
},
// JSONP request for cross‑domain GET
fetchWithJsonp(url, callbackParam = 'callback') {
return new Promise((resolve, reject) => {
const callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
const script = document.createElement('script');
window[callbackName] = (data) => {
delete window[callbackName];
document.body.removeChild(script);
resolve(data);
};
script.onerror = () => {
delete window[callbackName];
document.body.removeChild(script);
reject(new Error('JSONP request failed'));
};
const separator = url.indexOf('?') !== -1 ? '&' : '?';
script.src = `${url}${separator}${callbackParam}=${callbackName}`;
document.body.appendChild(script);
});
},
};6. Front‑End SSO Session Checker Component
// Session checking component that can be integrated into any app
class SSOSessionChecker extends React.Component {
constructor(props) {
super(props);
this.state = { isAuthenticated: false, isChecking: true };
this.checkInterval = props.checkInterval || 5 * 60 * 1000; // default 5 minutes
this.intervalId = null;
}
componentDidMount() {
this.checkSession();
this.intervalId = setInterval(this.checkSession, this.checkInterval);
document.addEventListener('visibilitychange', this.handleVisibilityChange);
}
componentWillUnmount() {
if (this.intervalId) clearInterval(this.intervalId);
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
}
handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
this.checkSession();
}
};
checkSession = async () => {
this.setState({ isChecking: true });
try {
const timestamp = new Date().getTime();
const img = new Image();
const sessionCheck = new Promise((resolve, reject) => {
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
setTimeout(() => reject(new Error('Session check timeout')), 5000);
});
img.src = `https://sso.example.com/session-check.png?t=${timestamp}`;
const isAuthenticated = await sessionCheck;
this.setState({ isAuthenticated, isChecking: false });
if (!isAuthenticated && this.props.onSessionExpired) {
this.props.onSessionExpired();
}
} catch (error) {
console.error('Session check failed:', error);
this.setState({ isChecking: false });
}
};
render() {
return this.props.children({
isAuthenticated: this.state.isAuthenticated,
isChecking: this.state.isChecking,
checkSession: this.checkSession,
});
}
}7. Seamless Token Refresh Implementation
// Token auto‑refresh manager
class TokenRefreshManager {
constructor(options) {
this.refreshEndpoint = options.refreshEndpoint || 'https://sso.example.com/oauth/token';
this.clientId = options.clientId;
this.tokenExpiryThreshold = options.tokenExpiryThreshold || 5 * 60 * 1000; // 5 minutes
this.refreshPromise = null;
}
// Initialize refresh timer
setupRefreshTimer() {
const accessToken = localStorage.getItem('access_token');
const tokenExpiry = localStorage.getItem('token_expiry');
const refreshToken = localStorage.getItem('refresh_token');
if (!accessToken || !tokenExpiry || !refreshToken) return;
const expiresAt = parseInt(tokenExpiry);
const now = new Date().getTime();
const timeUntilRefresh = expiresAt - now - this.tokenExpiryThreshold;
if (timeUntilRefresh <= 0) {
this.refreshToken();
} else {
setTimeout(() => this.refreshToken(), timeUntilRefresh);
}
}
// Refresh token
refreshToken() {
if (this.refreshPromise) return this.refreshPromise;
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) return Promise.reject(new Error('No refresh token available'));
this.refreshPromise = fetch(this.refreshEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: this.clientId,
}),
})
.then((response) => {
if (!response.ok) throw new Error('Token refresh failed');
return response.json();
})
.then((data) => {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('token_expiry', (new Date().getTime() + data.expires_in * 1000).toString());
if (data.refresh_token) {
localStorage.setItem('refresh_token', data.refresh_token);
}
const nextRefreshTime = data.expires_in * 1000 - this.tokenExpiryThreshold;
setTimeout(() => this.refreshToken(), nextRefreshTime);
return data;
})
.catch((error) => {
console.error('Token refresh failed:', error);
localStorage.removeItem('access_token');
localStorage.removeItem('token_expiry');
localStorage.removeItem('refresh_token');
window.dispatchEvent(new CustomEvent('auth:required'));
throw error;
})
.finally(() => {
this.refreshPromise = null;
});
return this.refreshPromise;
}
// Get a valid access token
getAccessToken() {
const accessToken = localStorage.getItem('access_token');
const tokenExpiry = localStorage.getItem('token_expiry');
if (!accessToken || !tokenExpiry) {
window.dispatchEvent(new CustomEvent('auth:required'));
return Promise.reject(new Error('Not logged in'));
}
const expiresAt = parseInt(tokenExpiry);
const now = new Date().getTime();
if (expiresAt - now < this.tokenExpiryThreshold) {
return this.refreshToken().then((data) => data.access_token);
}
return Promise.resolve(accessToken);
}
}8. Front‑End API Request Interceptor
// Axios interceptor that automatically adds authentication token
class ApiClient {
constructor() {
this.tokenManager = new TokenRefreshManager({
clientId: 'your-client-id',
refreshEndpoint: 'https://sso.example.com/oauth/token',
});
this.axiosInstance = axios.create({ baseURL: 'https://api.example.com' });
// Request interceptor
this.axiosInstance.interceptors.request.use(async (config) => {
try {
const token = await this.tokenManager.getAccessToken();
config.headers.Authorization = `Bearer ${token}`;
return config;
} catch (error) {
return Promise.reject(error);
}
}, (error) => Promise.reject(error));
// Response interceptor
this.axiosInstance.interceptors.response.use((response) => response, async (error) => {
if (error.response && error.response.status === 401) {
try {
await this.tokenManager.refreshToken();
const token = localStorage.getItem('access_token');
error.config.headers.Authorization = `Bearer ${token}`;
return this.axiosInstance.request(error.config);
} catch (refreshError) {
window.dispatchEvent(new CustomEvent('auth:required'));
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
});
// Initialize token refresh timer
this.tokenManager.setupRefreshTimer();
}
get(url, config) { return this.axiosInstance.get(url, config); }
post(url, data, config) { return this.axiosInstance.post(url, data, config); }
put(url, data, config) { return this.axiosInstance.put(url, data, config); }
delete(url, config) { return this.axiosInstance.delete(url, config); }
}9. Full SSO Process Summary
User accesses application : Front‑end checks local storage for valid authentication data; if missing, redirects to the SSO login page.
SSO login : User enters credentials on the SSO server, which validates them and creates a session, issuing either a cookie or a token.
Redirect back to application : For cookie‑based SSO, the browser carries the session cookie; for token‑based SSO, the token is passed as a URL parameter; for OAuth, an authorization code is exchanged for an access token.
Application validates authentication : Verifies the cookie or token, fetches user information, and establishes an internal session.
Session maintenance : Periodically checks SSO session status, automatically refreshes tokens nearing expiry, and handles session expiration.
Single logout : User clicks logout, the app clears local authentication data, and redirects to the SSO logout endpoint to terminate the SSO session.
10. Security Best Practices
// Secure SSO client implementation
class SecureSSO {
constructor() {
// Use secure storage (e.g., encrypted sessionStorage)
this.storage = new SecureStorage();
// CSRF protection token
this.csrfToken = this.generateRandomToken();
}
generateRandomToken(length = 32) {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('');
}
// Prevent clickjacking
preventClickjacking() {
if (window.self !== window.top) {
document.body.innerHTML = 'For security, this page cannot be displayed in an iframe.';
}
}
// XSS sanitization (requires DOMPurify library)
sanitizeInput(input) {
return DOMPurify.sanitize(input);
}
}
class SecureStorage {
setItem(key, value) {
const encryptedValue = this.encrypt(value);
sessionStorage.setItem(key, encryptedValue);
}
getItem(key) {
const encryptedValue = sessionStorage.getItem(key);
if (!encryptedValue) return null;
return this.decrypt(encryptedValue);
}
removeItem(key) { sessionStorage.removeItem(key); }
// Simple base64 encoding as placeholder (replace with real encryption)
encrypt(value) { return btoa(value); }
decrypt(encrypted) { return atob(encrypted); }
}Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
