IntegrateSPA & mobile
Next.js integration
Authenticate a Next.js app against Olympus
A Next.js app integrating with Olympus uses the Backend-for-frontend pattern: server-side API routes manage tokens; the React app calls those routes.
Setup
bun add oauth4webapiServer-side auth helper
lib/auth.ts:
import * as oauth from "oauth4webapi";
const ISSUER = new URL(process.env.OLYMPUS_ISSUER!);
const CLIENT_ID = process.env.OLYMPUS_CLIENT_ID!;
const CLIENT_SECRET = process.env.OLYMPUS_CLIENT_SECRET!;
const REDIRECT_URI = process.env.OLYMPUS_REDIRECT_URI!;
let _as: oauth.AuthorizationServer | null = null;
export async function getAuthServer() {
if (_as) return _as;
const res = await oauth.discoveryRequest(ISSUER);
_as = await oauth.processDiscoveryResponse(ISSUER, res);
return _as;
}
export function getClient() {
return { client_id: CLIENT_ID, client_secret: CLIENT_SECRET };
}Login route
app/api/auth/login/route.ts:
import { NextRequest, NextResponse } from "next/server";
import * as oauth from "oauth4webapi";
import { getAuthServer } from "@/lib/auth";
export async function GET(req: NextRequest) {
const as = await getAuthServer();
const codeVerifier = oauth.generateRandomCodeVerifier();
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
const state = oauth.generateRandomState();
const url = new URL(as.authorization_endpoint!);
url.search = new URLSearchParams({
client_id: process.env.OLYMPUS_CLIENT_ID!,
response_type: "code",
scope: "openid profile email offline_access",
redirect_uri: process.env.OLYMPUS_REDIRECT_URI!,
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
}).toString();
const res = NextResponse.redirect(url);
res.cookies.set("pkce_verifier", codeVerifier, { httpOnly: true, secure: true, sameSite: "lax" });
res.cookies.set("pkce_state", state, { httpOnly: true, secure: true, sameSite: "lax" });
return res;
}Callback route
app/api/auth/callback/route.ts:
import { NextRequest, NextResponse } from "next/server";
import * as oauth from "oauth4webapi";
import { getAuthServer, getClient } from "@/lib/auth";
export async function GET(req: NextRequest) {
const as = await getAuthServer();
const code = req.nextUrl.searchParams.get("code");
const state = req.nextUrl.searchParams.get("state");
const expectedState = req.cookies.get("pkce_state")?.value;
const verifier = req.cookies.get("pkce_verifier")?.value;
if (state !== expectedState || !code || !verifier) {
return new NextResponse("state mismatch", { status: 400 });
}
const response = await oauth.authorizationCodeGrantRequest(
as,
getClient(),
new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: process.env.OLYMPUS_REDIRECT_URI!,
code_verifier: verifier,
}),
process.env.OLYMPUS_REDIRECT_URI!,
verifier,
);
const result = await oauth.processAuthorizationCodeOAuth2Response(as, getClient(), response);
// result.access_token, result.id_token, result.refresh_token
const res = NextResponse.redirect("/");
res.cookies.set("session", JSON.stringify({
access_token: result.access_token,
refresh_token: result.refresh_token,
}), { httpOnly: true, secure: true, sameSite: "lax" });
return res;
}Server component example
app/page.tsx:
import { cookies } from "next/headers";
export default async function HomePage() {
const session = cookies().get("session")?.value;
if (!session) {
return <a href="/api/auth/login">Log in</a>;
}
const { access_token } = JSON.parse(session);
// Use access_token to call your APIs
return <div>Logged in</div>;
}Refresh token handling
Wrap your API calls with a refresh check:
async function withFreshToken<T>(fn: (token: string) => Promise<T>): Promise<T> {
const session = await getSession();
if (session.expires_at < Date.now() + 60_000) {
await refreshSession(session.refresh_token);
}
return await fn(session.access_token);
}Update the session cookie after refresh, see Integrate, Refresh tokens.