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:
- Your app redirects to Mushu's authorize endpoint
- Mushu redirects to the identity provider (Apple, Google, or Facebook)
- User authenticates
- Mushu redirects back to your app with JWT tokens in the URL fragment
- 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:
| Storage | Pros | Cons |
|---|---|---|
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:
- Decode the JWT and check the
expclaim before each request - If expiring within 5 minutes, refresh proactively
- If a request returns 401, refresh and retry once
- 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.