Sign in from a single-page app
A browser app signs users in with @anchring/auth/browser: hosted login with passkeys, PKCE, refresh before expiry and a fetch that adds the token.
- You get
- A browser app that signs users in with passkeys and calls your API with a fresh token.
- For
- Frontend developer
- Time
- About 20 min
- Products
- Sign-in
Register a public, PKCE app (kind spa) in the dashboard (Apps) with the redirect http://localhost:5173/auth/callback (your dev server's origin) and the audience of your API, e.g. acme-api. Without an audience, tokens carry the client id as aud and your API answers 401 wrong_audience.
import { createAuth } from '@anchring/auth/browser';
const auth = createAuth({ project: 'acme', env: 'test', clientId: 'test_…' }); // the id the dashboard shows (Apps)
await auth.ready(); // finishes a pending callback / restores the session
auth.user(); // { sub, email, name, claims } or null
auth.user.subscribe((u) => render(u)); // signal / Svelte-store compatible
await auth.signIn(); // redirects to hosted login: passkey, password, email code
await auth.fetch('/api/reports'); // adds Authorization
await auth.token(); // fresh access token if you need it yourself
await auth.signOut(); // revokes the refresh token, RP-initiated logoutThe account button in your framework
One auth for the app, and the user as your framework's own reactive primitive: useAuthUser(auth) from @anchring/auth/react (a hook) or @anchring/auth/vue (a composable), authUser(auth) from @anchring/auth/angular (a signal, unsubscribed with the component). Angular apps that use HttpClient add authInterceptor(auth, { prefix: '/api/' }) so every request to the API carries the token. Each adapter imports nothing but its framework.
import { useAuthUser } from '@anchring/auth/react';
import { auth } from './auth';
export function Account() {
const user = useAuthUser(auth);
if (!user) return <button onClick={() => auth.signIn()}>Sign in</button>;
return <button onClick={() => auth.signOut()}>Sign out {user.email}</button>;
}
// API calls: auth.fetch('/api/reports') adds the token, refreshing it first when it is about to expire.The callback
The SDK handles the callback: when the page loads on the redirect path it exchanges the code (PKCE S256 with state and nonce), verifies the ID token and restores the URL you signed in from. Nothing to write. To route it yourself, turn autoCallback off:
const auth = createAuth({ project: 'acme', env: 'test', clientId: 'test_…', autoCallback: false, refresh: 'cookie' });
// on your /auth/callback route:
location.assign((await auth.handleCallback()) ?? '/');Sign-in options
await auth.signIn({ redirectTo: '/reports' }); // back to /reports after the callback
await auth.signIn({ prompt: 'create' }); // open hosted login on sign-up (self sign-up on)
await auth.signIn({ loginHint: 'ada@acme.example' }); // prefill the email
await auth.signIn({ prompt: 'login', params: { acr_values: 'mfa' } }); // step up to MFAredirectTo must be a same-origin URL or path; anything else throws a TypeError, so it cannot become an open redirect. prompt: 'create' opens hosted login on its sign-up wording when the project has self sign-up turned on (dashboard, Settings).
Sessions and storage
Use refresh: 'cookie': the access token stays in memory and the refresh token goes into an HttpOnly cookie on the project's host, out of reach of page scripts. A reload or a new tab restores the session from that cookie, tabs refresh one at a time, and signOut() revokes it. Without it, tokens are kept in memory only and a reload signs in again (silently while the tenant session is alive). localStorage is deliberately not offered. More in Keeping browser tokens safe.
Email and name in the API
In the browser, auth.user() has the email from the ID token. Access tokens carry no email or name by default; give the app a claims template (Apps → Custom claims template (JSON)) if your API needs them:
{ "email": "{{user.email}}", "email_verified": "{{user.email_verified}}", "name": "{{user.name}}" }Next: protect the API the app calls.
Full example (Single-page app): React · Vue · Angular · Vanilla
Every option, error and token type: the SDK README.