Web SPA
Three solid options depending on how much control you want.
Option A — React widget (fastest)
If React is your stack and a popup login is acceptable, the React SDK is ~10 lines:
<CamelAuthProvider config={{ issuer: "https://accounts.camelcreatives.com", clientId: "your-app-web" }}>
<CamelLoginButton onClick={login} logo="/logo.jpeg" />
</CamelAuthProvider>Pass logo to show your Camel Accounts mark (or your own) on the left of the button; omit it to use the built-in camel mark.
Option B — oidc-client-ts (redirect flow)
Full redirect-based OIDC integration with automatic discovery:
npm install oidc-client-ts// auth.ts
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
export const userManager = new UserManager({
authority: "https://accounts.camelcreatives.com",
client_id: "your-app-web",
redirect_uri: "https://yourapp.com/callback",
scope: "openid profile email",
response_type: "code",
userStore: new WebStorageStateStore({ store: sessionStorage }),
automaticSilentRenew: false, // see note below — refresh via refresh_token instead
loadUserInfo: true, // fetches /oauth/userinfo after sign-in
});
export async function signIn() {
await userManager.signinRedirect(); // builds PKCE + state for you
}
export async function finishSignIn() {
const user = await userManager.signinRedirectCallback();
return user; // .profile contains sub/name/email…, .access_token ready to use
}Router wiring (any framework):
// /callback route
const user = await finishSignIn();
saveRefreshToken(user.refresh_token); // you keep this; userManager keeps its own copy too
router.push("/");Calling an API with proactive refresh:
let cached: { access: string; expiresAt: number } | null = null;
export async function getAccessToken(): Promise<string> {
if (cached && Date.now() < cached.expiresAt - 30_000) return cached.access;
const user = await userManager.getUser();
const res = await fetch("https://accounts.camelcreatives.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: user!.refresh_token!,
client_id: "your-app-web",
}),
});
if (!res.ok) { await userManager.removeUser(); throw new Error("re-login required"); }
const t = await res.json();
cached = { access: t.access_token, expiresAt: Date.now() + t.expires_in * 1000 };
return t.access_token;
}Why not
automaticSilentRenew? Silent renew drives/authorizeinside a hidden iframe (prompt=none). Camel Accounts' authorize endpoint doesn't implementprompt=none, so iframe renewals can fail or pop real UI. The refresh-token grant is the reliable renewal path here.
Option C — Vanilla JS
No library at all: follow Quick Start, which includes a complete single-file HTML demo (~80 lines).
Framework notes
- Next.js: run Options A–C in client components ("use client") — the flow is browser-side by design. Your Route Handlers / Server Actions verify incoming bearer tokens server-side (Backend verification).
- Vue/Svelte/Angular: use Option B's pattern with the framework's router callback page;
oidc-client-tsis framework-agnostic. - Dev mode: register
http://localhost:3000/callbackas an extra redirect URI on your client — multiple URIs per client are fine, and exact matching applies to whichever one you send.
CORS
Browser calls to $ISSUER/oauth/token and $ISSUER/oauth/userinfo from another origin need your origin in the server's CORS_ALLOWED_ORIGINS. Preflights (OPTIONS) are answered with credentials allowed and a 24-hour preflight cache. If you get opaque network errors in dev, ask the operator to add http://localhost:3000 to that list.