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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Master Front‑End Single Sign‑On (SSO) with React: Complete Guide & Code Samples

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); }
}
Image
Image
ReActAuthenticationTokenSSOOAuth
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.