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/authimport { 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 claimsverifyShooToken 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
| Option | Default | Description |
|---|---|---|
appOrigin | Required | Your exact app origin, from deployment config. Never from request headers. |
issuer | "https://shoo.dev" | Shoo issuer. Change only for a local Shoo server. |
allowLocalhost | false | Allows 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
| Environment | APP_ORIGIN |
|---|---|
| Local | http://localhost:3000 |
| Production | https://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:
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:
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.
| Check | Expected value | Why |
|---|---|---|
| Signature | ES256, key from Shoo JWKS | The token came from Shoo and was not changed |
iss | https://shoo.dev | Rejects other issuers |
aud | origin:<your exact origin> | Rejects tokens issued to other Shoo apps |
exp | In the future | Rejects expired tokens |
sub | Equal to pairwise_sub | The user ID you store |
To check online without the helper, call POST /session/check.
Keys and discovery
| Endpoint | URL |
|---|---|
| JWKS | https://shoo.dev/.well-known/jwks.json |
| Discovery | https://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.