SUPER EARLY WIP — USE AT YOUR OWN RISK■
shoo
GitHubCOMING SOON
API Reference

@shoojs/auth

Browser client and server verifier for Shoo

Install

npm install @shoojs/auth

The package has two entry points:

ImportRuns inUse
@shoojs/authBrowserSign-in, callback, stored identity
@shoojs/auth/serverServerToken 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

OptionTypeDefaultDescription
shooBaseUrlstring"https://shoo.dev"Shoo server URL
callbackPathstring"/auth/callback"Path Shoo redirects to after sign-in
redirectUristringOrigin + callbackPathFull callback URL
clientIdstring"origin:<redirect origin>"Derived from redirectUri. Do not change.
requestPiibooleanfalseRequest profile data on every sign-in
storageKeystring"shoo_identity"localStorage key for the identity
pkceStorageKeystring"shoo_pkce"sessionStorage prefix for sign-in state
returnToStorageKeystring"shoo_return_to"Deprecated. Has no effect.
fallbackPathstring"/"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" });
OptionTypeDescription
requestPiibooleanRequest profile data for this sign-in
returnTostringSame-origin path to return to. Default: current page.
bundlePkceBundleUse your own PKCE bundle
redirectUri, clientId, shooBaseUrlstringPer-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 null when 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 redirectUri override finishes on that route. A new client finds the route in the stored transaction.
  • The client must use the same storageKey and pkceStorageKey as the client that started the sign-in. Otherwise it does not find the transaction.
OptionTypeDescription
urlstringURL to read. Default: current URL.
redirectTostringSame-origin path that overrides the stored return path
returnTostringAlias for redirectTo
fallbackPathstringUsed when no return path is stored

auth.finishSignIn(params?)

Same exchange as handleCallback, but does not navigate unless redirectAfter: true.

OptionDefaultDescription
clearCallbackParamstrueRemove callback parameters from the URL
redirectAfterfalseNavigate to the return path after the exchange
consumeReturnToDeprecated. 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 }
PropertyTypeDescription
userIdstring | nullThe pairwise_sub, or null if signed out
tokenstring | undefinedThe id_token
expiresInnumber | undefinedToken lifetime in seconds
receivedAtnumber | undefinedWhen 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();
OptionDefaultDescription
intervalMs60000Check interval
immediatetrueCheck once at start
onResultCalled with every current result
onLoginRequiredCalled on revoked, expired, or invalid_token
onErrorCalled 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.

FunctionDescription
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 exp with 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.js or 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 sessionStorage until 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.