Browser quickstart

Add sign-in to a single-page app or web game with the browser SDK. It sends players to the hosted login page, completes the authorization-code flow with PKCE, and keeps tokens in memory.

Before you start

  • A project with a Browser application. See Console onboarding.
  • Its Issuer and Client ID from Integration settings.
  • The exact URL of the page that handles the callback, registered under Redirect URIs, and that page's origin under Allowed origins.

1. Add the SDK

The SDK is the @auth4/sdk-browser workspace package in packages/sdk-browser. It isn't published to a package registry yet, so apps inside this repository add it as a workspace dependency ("@auth4/sdk-browser": "workspace:*"), and the example imports it from source. The package is TypeScript source, so you need a bundler such as Vite.

2. Create the client

Excerpt from examples/browser/src/main.ts
const auth4 = new Auth4BrowserClient({
  issuer: "https://auth.auth4.dev/t/ten_replace_with_your_tenant",
  clientId: "cli_replace_with_your_browser_client",
  redirectUri: `${window.location.origin}${window.location.pathname}`,
});
  • issuer must be the canonical issuer, including /t/{projectId}. The SDK checks that discovery reports the same issuer.
  • redirectUri must match a registered redirect URI character for character. It can't contain a fragment or OAuth callback parameters.
  • Use HTTPS. Plain HTTP is accepted only for localhost, 127.0.0.1 and [::1].

3. Sign in, handle the callback, sign out

The complete example page wires the whole flow together:

examples/browser/src/main.ts
import { Auth4BrowserClient, Auth4BrowserError } from "../../../packages/sdk-browser/src/index.ts";

// Replace these public client values with the tenant and browser client from the Auth4 console.
const auth4 = new Auth4BrowserClient({
  issuer: "https://auth.auth4.dev/t/ten_replace_with_your_tenant",
  clientId: "cli_replace_with_your_browser_client",
  redirectUri: `${window.location.origin}${window.location.pathname}`,
});

const status = document.querySelector<HTMLElement>("#status")!;
const profile = document.querySelector<HTMLElement>("#profile")!;
const signIn = document.querySelector<HTMLButtonElement>("#sign-in")!;
const signOut = document.querySelector<HTMLButtonElement>("#sign-out")!;

function showSignedOut(message = "You are signed out."): void {
  status.textContent = message;
  profile.textContent = "";
  signIn.hidden = false;
  signOut.hidden = true;
}

async function showSignedIn(): Promise<void> {
  const user = await auth4.getUser();
  status.textContent = "You are signed in.";
  profile.textContent = user.email ?? user.sub;
  signIn.hidden = true;
  signOut.hidden = false;
}

signIn.addEventListener("click", () => {
  void auth4.login().catch((error: unknown) => {
    status.textContent =
      error instanceof Auth4BrowserError ? error.message : "Unable to start sign-in.";
  });
});

signOut.addEventListener("click", () => {
  void auth4
    .logout()
    .then(() => showSignedOut())
    .catch((error: unknown) => {
      showSignedOut(
        error instanceof Auth4BrowserError
          ? error.message
          : "Signed out locally; token revocation failed.",
      );
    });
});

const callbackUrl = new URL(window.location.href);
if (callbackUrl.searchParams.has("code") || callbackUrl.searchParams.has("error")) {
  void auth4
    .handleCallback(callbackUrl)
    .then(showSignedIn)
    .catch((error: unknown) => {
      showSignedOut(
        error instanceof Auth4BrowserError ? error.message : "Sign-in could not be completed.",
      );
    });
} else if (auth4.getSession()) {
  void showSignedIn().catch(() => showSignedOut());
} else {
  showSignedOut("Use the button to continue to hosted login.");
}
MethodWhat it does
login()Creates state, a nonce and a PKCE verifier, saves them insessionStorage for up to ten minutes, and navigates to hosted login. The default scope is openid profile email.
handleCallback()Checks the callback's state and issuer, exchanges the code, validates the ID token signature and claims, and removes the callback parameters from the address bar. Each callback works once.
getSession()Returns the in-memory session, or null when signed out or expired.
getUser()Calls UserInfo with the access token and checks that the subject matches the ID token. Returns sub, and email when the email scope was granted.
getAccessToken()Returns the live access token, or null when signed out. Throwstoken_expired once it has expired.
logout()Clears the session from memory, then revokes the access token. Pass returnTo to navigate afterwards; it must use the redirect URI's origin.

4. Call your API

Send the access token as a bearer token and verify it on your server with the server SDK:

Calling a protected API
import { Auth4BrowserError } from "@auth4/sdk-browser";

// Returns null when there is no live token; the caller decides when to send the player to login().
function currentAccessToken(): string | null {
  try {
    return auth4.getAccessToken();
  } catch (error) {
    if (error instanceof Auth4BrowserError && error.code === "token_expired") return null;
    throw error;
  }
}

async function loadProfile(): Promise<unknown> {
  const token = currentAccessToken();
  if (!token) {
    await auth4.login(); // navigates away to hosted login
    return null;
  }
  const response = await fetch("https://api.your-game.example/profile", {
    headers: { Authorization: `Bearer ${token}` },
  });
  return response.json();
}

Token lifetime in the browser

  • Access tokens expire after at most five minutes and exist only in memory.
  • Reloading the page or opening a new tab starts signed out. Call login() again when you need a token.
  • The browser SDK never requests offline_access and never receives refresh tokens. Passingoffline_access as a scope throws configuration_error.

See Tokens and revocation for the reasoning.

Errors

Every failure is an Auth4BrowserError with a stable code. Show a short message and offer to sign in again. Don't retry callbacks automatically, because authorization codes and state are single-use.

CodeUsual cause and fix
configuration_errorIssuer, client ID, redirect URI or scope is malformed. Fix the configuration.
state_mismatchThe callback was reused, is more than ten minutes old, or came from another tab or client. Start sign-in again.
authorization_failedHosted login returned an OAuth error, most often access_denied because the player cancelled or declined consent.
invalid_callbackThe callback URL doesn't match redirectUri, or has extra or duplicate parameters.
issuer_mismatch, discovery_failedThe issuer is wrong or unreachable. Copy it again from the console.
token_exchange_failed, invalid_id_tokenThe code expired (codes last 60 seconds), the redirect URI changed, or the token failed validation. Start sign-in again.
token_expiredThe five-minute access token expired. Call login().
storage_unavailablesessionStorage is blocked, for example in some private-browsing modes or sandboxed frames.
userinfo_failed, logout_failedA network or server error. Logout has already cleared local state, so the player is signed out on this page.
cancelledYour AbortSignal fired.

Run the example

  1. Edit the issuer and client ID at the top of examples/browser/src/main.ts.
  2. Register http://localhost:4173/ as a redirect URI andhttp://localhost:4173 as an allowed origin.
  3. From the repository root, build and serve it:
    pnpm exec vite build examples/browser --emptyOutDir
    pnpm exec vite preview examples/browser --host localhost --port 4173 --strictPort
  4. Open http://localhost:4173/ and select Sign in.

The example's own notes are in examples/browser/README.md. The SDK's automated tests are in packages/sdk-browser/src/index.test.ts.