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

Server Verification

Always verify Shoo id_tokens on your server before trusting identity claims

Why verify server-side?

The browser auth flow gives the user a signed id_token, but the browser is an untrusted environment. A user could fabricate or tamper with tokens stored in localStorage.

Never trust unverified client-side claims for authorization decisions.

Always verify the id_token signature, issuer, audience, and expiration on your server before granting access.

Verification with jose

Install the jose library:

bun add jose

Then verify tokens using the JWKS endpoint:

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

const SHOO_BASE_URL = "https://shoo.dev";
const SHOO_ISSUER = "https://shoo.dev";
const jwks = createRemoteJWKSet(
  new URL("/.well-known/jwks.json", SHOO_BASE_URL),
);

export async function verifyShooToken(idToken: string, appOrigin: string) {
  const audience = `origin:${new URL(appOrigin).origin}`;

  const { payload } = await jwtVerify(idToken, jwks, {
    issuer: SHOO_ISSUER,
    audience,
  });

  if (typeof payload.pairwise_sub !== "string") {
    throw new Error("Shoo token missing pairwise_sub");
  }

  return payload;
}

createRemoteJWKSet fetches and caches the public keys from Shoo's JWKS endpoint. Key rotation is handled automatically.

What to verify

Your server must check all of these:

CheckValueWhy
Issuer (iss)https://shoo.devEnsures the token came from Shoo, not a different issuer
Audience (aud)origin:{your_app_origin}Ensures the token was issued for your app, not someone else's
Expiration (exp)Must be in the futurePrevents replay of expired tokens
SignatureValid ES256 against JWKSEnsures the token hasn't been tampered with
Pairwise subMust be a stringThe core identity claim you'll use

The jose library checks issuer, audience, expiration, and signature automatically when you pass the options to jwtVerify. You just need to verify pairwise_sub exists.

Token claims reference

ClaimTypeAlways presentDescription
issstringYesIssuer (https://shoo.dev)
audstringYesAudience (origin:{app_origin})
substringYesSubject (same as pairwise_sub)
pairwise_substringYesDomain-scoped unique user ID
iatnumberYesIssued at (unix timestamp)
expnumberYesExpires at (unix timestamp)
jtistringYesJWT ID for replay protection
emailstringNoOnly with PII consent
email_verifiedbooleanNoOnly with PII consent
namestringNoOnly with PII consent
picturestringNoOnly with PII consent

Full Next.js example

Here's a complete API route that accepts an id_token from the client and verifies it:

pages/api/verify.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { createRemoteJWKSet, jwtVerify } from "jose";

const SHOO_BASE_URL = process.env.SHOO_BASE_URL || "https://shoo.dev";
const SHOO_ISSUER = process.env.SHOO_ISSUER || SHOO_BASE_URL;
const APP_ORIGIN = process.env.APP_ORIGIN || "http://localhost:3000";

const jwks = createRemoteJWKSet(
  new URL("/.well-known/jwks.json", SHOO_BASE_URL),
);

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  const idToken =
    typeof req.body?.idToken === "string" ? req.body.idToken : "";
  if (!idToken) {
    return res.status(400).json({ error: "Missing idToken" });
  }

  try {
    const audience = `origin:${new URL(APP_ORIGIN).origin}`;
    const { payload } = await jwtVerify(idToken, jwks, {
      issuer: SHOO_ISSUER,
      audience,
    });

    if (typeof payload.pairwise_sub !== "string") {
      return res.status(401).json({ error: "Missing pairwise_sub claim" });
    }

    return res.status(200).json({
      userId: payload.pairwise_sub,
      email: payload.email,
      name: payload.name,
    });
  } catch (err) {
    const message =
      err instanceof Error ? err.message : "Verification failed";
    return res.status(401).json({ error: message });
  }
}

On the client, send the token to this endpoint after sign-in:

const identity = auth.identity;
if (identity.token) {
  const res = await fetch("/api/verify", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ idToken: identity.token }),
  });
  const data = await res.json();
  // data.userId is now server-verified
}

JWKS and discovery endpoints

EndpointURL
JWKS (public keys)https://shoo.dev/.well-known/jwks.json
OpenID Configurationhttps://shoo.dev/.well-known/openid-configuration

The JWKS endpoint returns ES256 (P-256) public keys in standard JWK format. Any OIDC-compatible JWT library can use these for verification.