Skip to content
Guide

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
How it works: Single-page appPKCE: no secret in the browser. 1. User → Your app: Clicks Sign in. 2. Your app → Anchr.ing: Redirect + PKCE challenge. 3. User → Anchr.ing: Signs in. 4. Anchr.ing → Your app: Code to callback URL. 5. Your app → Anchr.ing: Code + PKCE verifier. 6. Anchr.ing → Your app: Tokens. 7. Your app: Kept in memory. 8. Your app → Your API: Calls with access token.PKCE: no secret in the browserUserYour appAnchr.ingYour API1Clicks Sign in2Redirect + PKCEchallenge3Signs in4Code to callback URL5Code + PKCE verifier6Tokens7Kept in memory8Calls with access tokenHow it works: Single-page appPKCE: no secret in the browser. 1. User → Your app: Clicks Sign in. 2. Your app → Anchr.ing: Redirect + PKCE challenge. 3. User → Anchr.ing: Signs in. 4. Anchr.ing → Your app: Code to callback URL. 5. Your app → Anchr.ing: Code + PKCE verifier. 6. Anchr.ing → Your app: Tokens. 7. Your app: Kept in memory. 8. Your app → Your API: Calls with access token.PKCE: no secret in the browserUserYour appAnchr.ingYour API1Clicks Sign in2Redirect + PKCE challenge3Signs in4Code to callback URL5Code + PKCE verifier6Tokens7Kept in memory8Calls with access token

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.

web/auth.ts
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 logout

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

web/account.tsx
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 MFA

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

Try it in your own test environment

Every feature works in test on the free plan, so the code above runs against your real sign-in.