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

Server Verification

Verify Shoo tokens on your backend with a fixed issuer and exact audience

The browser is not trusted. A user can put any value in localStorage. Verify the token on your server before you use its claims.

Use the server helper

npm install @shoojs/auth
lib/shoo.ts
import { createShooVerifier } from "@shoojs/auth/server";

const appOrigin = process.env.APP_ORIGIN;
if (!appOrigin) throw new Error("APP_ORIGIN is not set");

// Create once per process so the key cache is shared.
export const verifyShooToken = createShooVerifier({
  appOrigin,
  // Allows http://localhost only outside production.
  allowLocalhost: process.env.NODE_ENV !== "production"
});
const identity = await verifyShooToken(token);
identity.userId; // pairwise_sub, the ID to store
identity.expiresAt; // exp, in seconds
identity.claims; // all verified claims

verifyShooToken throws when the token is invalid, expired, or issued to another app. Return 401 when it throws. Do not send the error text to the client.

Options

OptionDefaultDescription
appOriginRequiredYour exact app origin, from deployment config. Never from request headers.
issuer"https://shoo.dev"Shoo issuer. Change only for a local Shoo server.
allowLocalhostfalseAllows http: on localhost, 127.0.0.1, and [::1]. Keep off in production.
authorizationCheck"offline""required" also asks Shoo if the app is still approved. Denies if Shoo cannot answer.

The helper checks the ES256 signature against Shoo's JWKS, iss, the exact aud, exp, iat, and that sub equals pairwise_sub.

Set the app origin

EnvironmentAPP_ORIGIN
Localhttp://localhost:3000
Productionhttps://app.example.com

Each backend accepts exactly one origin. Do not accept localhost or preview origins in production. See identity and privacy.

Revocation and online checks

Offline verification accepts a token until exp, even after the user disconnects your app. The delay is up to one hour.

For sensitive actions, add a second verifier to lib/shoo.ts that checks online:

lib/shoo.ts
export const verifyShooTokenOnline = createShooVerifier({
  appOrigin,
  allowLocalhost: process.env.NODE_ENV !== "production",
  authorizationCheck: "required"
});

Use it for payments, deletes, and permission changes. It makes one request to Shoo per call and denies access when Shoo is unreachable. See tokens and sessions.

Without the helper

Any JWT library works. With jose:

lib/shoo-jose.ts
import { createRemoteJWKSet, jwtVerify } from "jose";

const SHOO_ISSUER = "https://shoo.dev";
// Create once per process. jose caches keys and refetches on an unknown kid.
const jwks = createRemoteJWKSet(new URL("/.well-known/jwks.json", SHOO_ISSUER));

export async function verifyShooToken(token: string, appOrigin: string) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: SHOO_ISSUER,
    audience: `origin:${new URL(appOrigin).origin}`,
    algorithms: ["ES256"],
    requiredClaims: ["sub", "exp", "iat"]
  });
  if (typeof payload.pairwise_sub !== "string" || payload.pairwise_sub !== payload.sub) {
    throw new Error("Invalid Shoo subject");
  }
  return payload;
}

Pass appOrigin from config. Never fall back to localhost or to the request Host header.

CheckExpected valueWhy
SignatureES256, key from Shoo JWKSThe token came from Shoo and was not changed
isshttps://shoo.devRejects other issuers
audorigin:<your exact origin>Rejects tokens issued to other Shoo apps
expIn the futureRejects expired tokens
subEqual to pairwise_subThe user ID you store

To check online without the helper, call POST /session/check.

Keys and discovery

EndpointURL
JWKShttps://shoo.dev/.well-known/jwks.json
Discoveryhttps://shoo.dev/.well-known/openid-configuration

Keys can change. Always load them from the JWKS URL. Do not copy a key into your code.

Shoo is not a full OpenID Connect provider. Do not configure it as a generic OIDC login provider.

Complete example

The React and Next.js guide shows a protected API route.