Olympus Docs
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

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 directly

Pros:

  • 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 memory

Pros:

  • 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.

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 fetch with strict CORS settings.
  • 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.

On this page