@shoojs/auth
Browser client and server verifier for Shoo
Install
npm install @shoojs/authThe package has two entry points:
| Import | Runs in | Use |
|---|---|---|
@shoojs/auth | Browser | Sign-in, callback, stored identity |
@shoojs/auth/server | Server | Token verification |
For React, use @shoojs/react.
createShooAuth(options)
Creates a browser client. Call it in the browser, not during server rendering.
import { createShooAuth } from "@shoojs/auth";
const auth = createShooAuth({ callbackPath: "/auth/callback" });ShooAuthOptions
| Option | Type | Default | Description |
|---|---|---|---|
shooBaseUrl | string | "https://shoo.dev" | Shoo server URL |
callbackPath | string | "/auth/callback" | Path Shoo redirects to after sign-in |
redirectUri | string | Origin + callbackPath | Full callback URL |
clientId | string | "origin:<redirect origin>" | Derived from redirectUri. Do not change. |
requestPii | boolean | false | Request profile data on every sign-in |
storageKey | string | "shoo_identity" | localStorage key for the identity |
pkceStorageKey | string | "shoo_pkce" | sessionStorage prefix for sign-in state |
returnToStorageKey | string | "shoo_return_to" | Deprecated. Has no effect. |
fallbackPath | string | "/" | Return path when none is stored |
The hosted script uses /shoo/callback by default. The npm package uses /auth/callback.
The return path is saved inside the sign-in transaction, under pkceStorageKey. returnToStorageKey is still accepted so old code compiles. Remove it from your code.
Sign-in
auth.startSignIn(params?)
Saves a sign-in transaction in sessionStorage and navigates to Shoo.
await auth.startSignIn();
await auth.startSignIn({ requestPii: true, returnTo: "/settings" });| Option | Type | Description |
|---|---|---|
requestPii | boolean | Request profile data for this sign-in |
returnTo | string | Same-origin path to return to. Default: current page. |
bundle | PkceBundle | Use your own PKCE bundle |
redirectUri, clientId, shooBaseUrl | string | Per-sign-in overrides. Saved with the transaction. |
It rejects without navigating if storage is not writable.
auth.handleCallback(params?)
Run on the callback page. Exchanges the code, saves the identity, removes callback parameters from the URL, and navigates to the return path.
try {
await auth.handleCallback();
} catch (error) {
// Denied, expired, or mismatched sign-in. Offer a retry.
}- Returns
nullwhen the URL has no callback parameters. - Throws when the user denied access, the state does not match, the transaction expired (10 minutes), or the exchange failed. A denial, expiry, or failed exchange removes the stored transaction. A callback with an unknown state leaves other pending sign-ins alone.
- Concurrent calls in the same window share one exchange.
- A sign-in that used a
redirectUrioverride finishes on that route. A new client finds the route in the stored transaction. - The client must use the same
storageKeyandpkceStorageKeyas the client that started the sign-in. Otherwise it does not find the transaction.
| Option | Type | Description |
|---|---|---|
url | string | URL to read. Default: current URL. |
redirectTo | string | Same-origin path that overrides the stored return path |
returnTo | string | Alias for redirectTo |
fallbackPath | string | Used when no return path is stored |
auth.finishSignIn(params?)
Same exchange as handleCallback, but does not navigate unless redirectAfter: true.
| Option | Default | Description |
|---|---|---|
clearCallbackParams | true | Remove callback parameters from the URL |
redirectAfter | false | Navigate to the return path after the exchange |
consumeReturnTo | Deprecated. Has no effect. |
It also accepts the handleCallback options. The transaction is always removed after the exchange, so consumeReturnTo does nothing.
Identity
auth.getIdentity()
const identity = auth.getIdentity();
// { userId: "ps_…", token: "eyJ…", expiresIn: 3600, receivedAt: 1767225600000 }| Property | Type | Description |
|---|---|---|
userId | string | null | The pairwise_sub, or null if signed out |
token | string | undefined | The id_token |
expiresIn | number | undefined | Token lifetime in seconds |
receivedAt | number | undefined | When the token was stored, in ms |
Expired, corrupt, or unreadable stored identities read as { userId: null }.
auth.subscribe(listener)
Calls listener when the identity changes in this tab, in another tab, or on expiry. Returns an unsubscribe function. It does not call the listener at once; read getIdentity() first.
const unsubscribe = auth.subscribe((identity) => render(identity));auth.clearIdentity()
Removes the stored identity from this browser. This is app sign-out. It does not revoke the token.
auth.decodeIdentityClaims(token?)
Decodes the token payload as UTF-8 without verifying the signature. Use it for display only. Returns null for malformed input.
Session checks
auth.checkSession()
Asks Shoo whether the stored token is still valid and the app is still approved.
const result = await auth.checkSession();{ status: "active" }{ status: "login_required", reason: "revoked" | "expired" | "invalid_token" }{ status: "unsupported" }for an older Shoo server
This only protects the browser UI. Your backend must verify tokens itself. See tokens and sessions.
auth.startSessionMonitor(options?)
Runs checkSession on an interval.
const monitor = auth.startSessionMonitor({
onLoginRequired: () => auth.clearIdentity()
});
monitor.stop();| Option | Default | Description |
|---|---|---|
intervalMs | 60000 | Check interval |
immediate | true | Check once at start |
onResult | Called with every current result | |
onLoginRequired | Called on revoked, expired, or invalid_token | |
onError | Called on network or server errors |
Results that arrive after stop() or after the token changes are ignored.
Lower-level functions
These are exported at the top level and on the client, except deriveClientIdFromRedirectUri. It is a top-level export only.
| Function | Description |
|---|---|
createPkceBundle() | Random state, verifier, and S256 challenge |
createSignInUrl(params) | Build an /authorize URL without navigating |
parseCallback(url?) | Returns { code, state }, { error, state }, or null. Throws on malformed callbacks. |
clearCallbackParams() | Remove callback parameters from the URL bar |
exchangeCode(params) | POST /token with a code and verifier |
persistIdentity(userId, token) | Write an identity to storage. Throws if the token is malformed, expired, or for another user. |
deriveClientIdFromRedirectUri(uri) | Top level only. "origin:" + new URL(uri).origin |
The package also exports readCallbackStorageOptions. The hosted script uses it. It is internal, not part of the public API, and can change in any release. Do not use it.
parseCallback returns a union. Check which field is set before you read it:
import { parseCallback } from "@shoojs/auth";
const callback = parseCallback();
if (callback?.error) showError(callback.error);
else if (callback) console.log(callback.code);Known limits
- Device clock. The browser SDK compares the token
expwith the device clock. If the clock is fast by more than the token lifetime, sign-in fails with "Invalid or expired identity token". If the clock is wrong by less, the token reads as expired too early or too late. Your backend uses its own clock. - One SDK copy per page. Load either
shoo.jsor the npm package on a page, not both, and bundle one version of@shoojs/auth. Copies do not share callback handling. Two copies on one callback page give one success and one failed exchange. - Sign-in state stays in the tab. A started sign-in is saved in that tab's
sessionStorageuntil the tab closes. After 10 minutes it expires and cannot be used. - Upgrades. A sign-in that started with an older SDK version cannot finish with this one. The user starts sign-in again.
Server verifier
import { createShooVerifier } from "@shoojs/auth/server";See server verification.
Types
import type {
CheckSessionOptions,
ExchangeCodeParams,
FinishSignInOptions,
HandleCallbackOptions,
PkceBundle,
SessionCheckResult,
SessionMonitorHandle,
SessionMonitorOptions,
ShooAuthClient,
ShooAuthOptions,
ShooCallback,
ShooIdentity,
StartSignInOptions
} from "@shoojs/auth";IdentityClaims and TokenResponse come from @shoojs/types.