Web SPA Integration

This guide covers integrating Mushu authentication into a browser-based single-page application (React, Vue, Next.js, etc.) using OAuth redirect flows.

Overview

Mushu doesn't provide a frontend SDK — you integrate directly with the OAuth endpoints. The flow is:

  1. Your app redirects to Mushu's authorize endpoint
  2. Mushu redirects to the identity provider (Apple, Google, or Facebook)
  3. User authenticates
  4. Mushu redirects back to your app with JWT tokens in the URL fragment
  5. Your JavaScript reads the tokens and stores them

Quick Start

1. Add a Login Button

// Replace with your values
const MUSHU_AUTH_URL = 'https://auth.mushucorp.com';
const APP_ID = 'app_xxxxxxxx';
const CALLBACK_URL = window.location.origin + '/auth/callback';

function loginWithFacebook() {
  const url = MUSHU_AUTH_URL
    + '/auth/facebook/authorize'
    + '?redirect_uri=' + encodeURIComponent(CALLBACK_URL)
    + '&app_id=' + APP_ID;

  window.location.href = url;
}

function loginWithGoogle() {
  const url = MUSHU_AUTH_URL
    + '/auth/google/authorize'
    + '?redirect_uri=' + encodeURIComponent(CALLBACK_URL)
    + '&app_id=' + APP_ID;

  window.location.href = url;
}

2. Handle the Callback

Create a page at /auth/callback that reads tokens from the URL fragment:

// /auth/callback page
function handleAuthCallback() {
  const hash = window.location.hash.substring(1);
  const params = new URLSearchParams(hash);

  const accessToken = params.get('access_token');
  const refreshToken = params.get('refresh_token');

  if (!accessToken) {
    // Check for error
    const error = params.get('error');
    console.error('Auth failed:', error);
    window.location.href = '/login?error=' + (error || 'unknown');
    return;
  }

  // Store tokens
  localStorage.setItem('mushu_access_token', accessToken);
  localStorage.setItem('mushu_refresh_token', refreshToken);

  // Clear the hash (tokens shouldn't stay in the URL)
  window.history.replaceState(null, '', window.location.pathname);

  // Redirect to app
  window.location.href = '/dashboard';
}

3. Make Authenticated Requests

async function fetchWithAuth(url, options = {}) {
  const token = localStorage.getItem('mushu_access_token');

  if (!token) {
    window.location.href = '/login';
    return;
  }

  const response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': 'Bearer ' + token,
    },
  });

  if (response.status === 401) {
    // Token expired — try refreshing
    const refreshed = await refreshTokens();
    if (refreshed) {
      return fetchWithAuth(url, options); // Retry with new token
    }
    // Refresh failed — redirect to login
    window.location.href = '/login';
    return;
  }

  return response;
}

4. Refresh Tokens

async function refreshTokens() {
  const accessToken = localStorage.getItem('mushu_access_token');
  const refreshToken = localStorage.getItem('mushu_refresh_token');

  if (!refreshToken) return false;

  try {
    const response = await fetch('https://auth.mushucorp.com/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        access_token: accessToken,
        refresh_token: refreshToken,
      }),
    });

    if (!response.ok) return false;

    const data = await response.json();
    localStorage.setItem('mushu_access_token', data.tokens.access_token);
    localStorage.setItem('mushu_refresh_token', data.tokens.refresh_token);

    return true;
  } catch {
    return false;
  }
}

React Example

Auth Context

import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const token = localStorage.getItem('mushu_access_token');
    if (token) {
      // Decode JWT to get user info (no network request needed)
      try {
        const payload = JSON.parse(atob(token.split('.')[1]));
        if (payload.exp * 1000 > Date.now()) {
          setUser({ id: payload.sub, type: payload.user_type });
        }
      } catch {
        // Invalid token
      }
    }
    setLoading(false);
  }, []);

  const logout = () => {
    localStorage.removeItem('mushu_access_token');
    localStorage.removeItem('mushu_refresh_token');
    setUser(null);
    window.location.href = '/login';
  };

  return (
    
      {children}
    
  );
}

export const useAuth = () => useContext(AuthContext);

Login Page

const MUSHU_AUTH = 'https://auth.mushucorp.com';
const APP_ID = 'app_xxxxxxxx';
const CALLBACK = window.location.origin + '/auth/callback';

function LoginPage() {
  const { user } = useAuth();

  if (user) {
    return ;
  }

  const facebookUrl = MUSHU_AUTH
    + '/auth/facebook/authorize'
    + '?redirect_uri=' + encodeURIComponent(CALLBACK)
    + '&app_id=' + APP_ID;

  const googleUrl = MUSHU_AUTH
    + '/auth/google/authorize'
    + '?redirect_uri=' + encodeURIComponent(CALLBACK)
    + '&app_id=' + APP_ID;

  return (
    
  );
}

Callback Page

import { useEffect } from 'react';

function AuthCallback() {
  useEffect(() => {
    const hash = window.location.hash.substring(1);
    const params = new URLSearchParams(hash);
    const accessToken = params.get('access_token');
    const refreshToken = params.get('refresh_token');

    if (accessToken) {
      localStorage.setItem('mushu_access_token', accessToken);
      localStorage.setItem('mushu_refresh_token', refreshToken);
      window.history.replaceState(null, '', '/auth/callback');
      window.location.href = '/dashboard';
    } else {
      window.location.href = '/login?error=auth_failed';
    }
  }, []);

  return 
Signing in...
; }

Token Storage

Where to store tokens depends on your security requirements:

StorageProsCons
localStorage Persists across tabs and page reloads Accessible to any JS on the page (XSS risk)
sessionStorage Cleared when tab closes Not shared across tabs
In-memory (JS variable) Most secure (no persistence) Lost on page reload

Security note: Never store tokens in cookies accessible to JavaScript. If your app is vulnerable to XSS, any client-side storage can be compromised. Prioritize preventing XSS (CSP headers, input sanitization) over storage choice.

Token Refresh Strategy

Access tokens expire after 1 hour. Recommended approach:

  1. Decode the JWT and check the exp claim before each request
  2. If expiring within 5 minutes, refresh proactively
  3. If a request returns 401, refresh and retry once
  4. If refresh fails, redirect to login
function isTokenExpiringSoon(token, bufferSeconds = 300) {
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    return payload.exp * 1000 < Date.now() + bufferSeconds * 1000;
  } catch {
    return true;
  }
}

Validating Tokens Server-Side

If your backend needs to verify the user, you have two options:

Option A: Call Mushu (simple)

GET https://auth.mushucorp.com/auth/validate
Authorization: Bearer USER_ACCESS_TOKEN

Option B: Verify JWT locally (faster, no network)

Decode the JWT and verify the signature using Mushu's public key. Check the iss, exp, and app_id claims. This avoids a network round-trip on every request.

Recommendation: For high-traffic apps, verify JWTs locally. For low-traffic apps or prototypes, calling /auth/validate is simpler.

CORS

Mushu auth endpoints accept requests from any origin. The OAuth flow uses full-page redirects (not AJAX), so CORS is not a concern for the sign-in flow. For API calls (validate, refresh), CORS headers are set to allow all origins.

FAQ

Can I use a popup instead of a redirect?

Yes. Open the authorize URL in a popup window. When the callback page loads, use window.opener.postMessage() to send the tokens back to the parent window, then close the popup.

How do I handle multiple sign-in providers?

Each provider uses the same callback URL. The flow is identical — only the authorize endpoint path changes (/auth/facebook/authorize vs /auth/google/authorize). The callback page doesn't need to know which provider was used.

Do I need email/password auth?

Mushu currently supports Apple, Google, and Facebook OAuth only. If your users expect email/password signup, have them sign in with one of the supported providers. Most web apps can use "Continue with Google" or "Continue with Facebook" as the primary sign-in method.

What if the user's token expires while they're using the app?

Use the refresh strategy described above. Wrap your API calls in a function that automatically refreshes on 401 and retries. The user won't notice.