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
examples/browser/src/main.tsconst 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}`,
});issuermust be the canonical issuer, including/t/{projectId}. The SDK checks that discovery reports the same issuer.redirectUrimust 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.1and[::1].
3. Sign in, handle the callback, sign out
The complete example page wires the whole flow together:
examples/browser/src/main.tsimport { 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.");
}| Method | What 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:
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_accessand never receives refresh tokens. Passingoffline_accessas a scope throwsconfiguration_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.
| Code | Usual cause and fix |
|---|---|
configuration_error | Issuer, client ID, redirect URI or scope is malformed. Fix the configuration. |
state_mismatch | The callback was reused, is more than ten minutes old, or came from another tab or client. Start sign-in again. |
authorization_failed | Hosted login returned an OAuth error, most often access_denied because the player cancelled or declined consent. |
invalid_callback | The callback URL doesn't match redirectUri, or has extra or duplicate parameters. |
issuer_mismatch, discovery_failed | The issuer is wrong or unreachable. Copy it again from the console. |
token_exchange_failed, invalid_id_token | The code expired (codes last 60 seconds), the redirect URI changed, or the token failed validation. Start sign-in again. |
token_expired | The five-minute access token expired. Call login(). |
storage_unavailable | sessionStorage is blocked, for example in some private-browsing modes or sandboxed frames. |
userinfo_failed, logout_failed | A network or server error. Logout has already cleared local state, so the player is signed out on this page. |
cancelled | Your AbortSignal fired. |
Run the example
- Edit the issuer and client ID at the top of
examples/browser/src/main.ts. - Register
http://localhost:4173/as a redirect URI andhttp://localhost:4173as an allowed origin. - 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 - 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.