IntegrateSPA & mobile
SPA integration
Single-page apps end-to-end with Olympus
A single-page app (SPA), React, Vue, Svelte, can't keep secrets. Use the Authorization Code + PKCE flow.
Architecture options
A: Backend-for-frontend (BFF), recommended
Your SPA talks to your backend; your backend talks to Olympus.
Browser (SPA) → Your backend → Olympus Hydra
Your backend manages session/refresh tokens
SPA never sees an OAuth2 token directlyPros:
- Refresh tokens stay server-side (HttpOnly cookie or secure store).
- No tokens in JavaScript = XSS can't steal them.
- Standard pattern; well-supported.
Cons:
- Requires running a backend.
B: SPA-only, works but exposed
Your SPA does the full OAuth2 flow client-side, stores tokens in memory or sessionStorage.
Browser (SPA) → Olympus Hydra (no backend)
Tokens live in browser memoryPros:
- No backend.
- Simpler infrastructure.
Cons:
- Tokens accessible to any JS that runs. XSS = full account takeover.
- Refresh tokens hard to keep safe.
- Generally discouraged for production.
Recommend A unless you have specific reasons for B.
BFF pattern (recommended)
Backend endpoints
Your backend exposes:
GET /api/auth/login, generates PKCE pair, stores verifier in HttpOnly cookie, redirects user to Olympus.GET /api/auth/callback, receives code, exchanges for tokens via/oauth2/token, sets HttpOnly session cookie, redirects to app.POST /api/auth/logout, invalidates session, optionally calls Olympus RP-initiated logout.GET /api/me, returns userinfo for the current session.
SPA code
// Login: redirect to backend
function login() {
window.location.href = '/api/auth/login?return_to=' + encodeURIComponent(window.location.pathname);
}
// Logout
async function logout() {
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/';
}
// Check current user
async function getMe() {
const response = await fetch('/api/me');
if (!response.ok) return null;
return await response.json();
}Calling Olympus APIs
The SPA calls your backend; the backend forwards with the OAuth2 access token attached:
// Your backend has the token
async function proxyToOlympus(path: string, session) {
const token = await session.getAccessToken();
return await fetch(`https://olympus-api/${path}`, {
headers: { Authorization: `Bearer ${token}` }
});
}SPA-only pattern (if you must)
import { generators } from 'openid-client';
async function login() {
const codeVerifier = generators.codeVerifier();
const codeChallenge = generators.codeChallenge(codeVerifier);
sessionStorage.setItem('pkce', codeVerifier);
const params = new URLSearchParams({
client_id: 'spa-client',
response_type: 'code',
redirect_uri: window.location.origin + '/callback',
scope: 'openid profile email offline_access',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: generators.state(),
});
window.location.href = `https://ciam.your-domain/oauth2/auth?${params}`;
}
async function handleCallback() {
const code = new URL(window.location.href).searchParams.get('code');
const verifier = sessionStorage.getItem('pkce');
const response = await fetch('https://ciam.your-domain/oauth2/token', {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'spa-client',
code,
redirect_uri: window.location.origin + '/callback',
code_verifier: verifier,
})
});
const tokens = await response.json();
// tokens.access_token, keep in memory
// tokens.refresh_token, DANGEROUS to keep in JS-accessible storage
}Reduce attack surface by:
- Never storing refresh tokens in localStorage / sessionStorage.
- Keeping access tokens in memory only, accepting that page reloads re-auth.
- Using
fetchwith strict CORS settings.
Recommended libraries
oauth4webapi, modern, framework-agnostic OAuth2 client.oidc-client-ts, full OIDC client with session management.@axa-fr/react-oidc, React-specific, includes a context provider.
Mobile considerations
Mobile apps fall into a similar category as SPAs, public clients, can't keep secrets, must use PKCE. Use the platform's preferred OAuth2 library (AppAuth-iOS, AppAuth-Android).
See Integrate, Mobile integration.