Tokens and Sessions
Token lifetime, renewal, revocation delay, and how app sessions differ
There are three separate sessions. They have different lifetimes and controls.
| Session | Where it lives | Lifetime | Who controls it |
|---|---|---|---|
| Shoo session | Session cookie on shoo.dev | Up to 30 days | Shoo and the user |
| App token | Your app, usually localStorage | 1 hour (expires_in: 3600) | Shoo sets exp |
| Your app session | Whatever your backend creates | You decide | You |
On HTTPS the Shoo session cookie is __Host-shoo_session. A local HTTP development server uses shoo_session.
App tokens
The app token is the id_token JWT. It is a bearer token: anyone who has it can use it until exp.
- The default lifetime is one hour. Read
expires_infrom the token response orexpfrom the token. Do not hard-code it. jtiis a unique ID. Shoo does not reject a reused token, sojtiis not replay protection.- Send it to your backend in an
Authorization: Bearerheader over HTTPS. - Do not log it.
Renewal
Shoo has no refresh tokens. To get a new token, the user signs in again.
- When the token expires, the client reports a signed-out or expired state. It does not navigate by itself.
- Call
signIn()on app load or when the user acts, for example from a "Sign in again" button. PassreturnToto come back to the same page. - Sign-in is a full-page redirect to
shoo.dev. If the user still has a Shoo session and your app is still approved, Shoo redirects back without a Google prompt. It is still a page navigation, not a background refresh. - Background token fetches, such as Convex's
fetchAccessToken, returnnullwhen the token is expired. They never start a redirect. - The browser SDK compares
expwith the device clock. A wrong clock changes when the token reads as expired. See known limits.
Revocation
A user can disconnect your app at shoo.dev/me. What happens next depends on how you check tokens.
| Check | After the user disconnects your app |
|---|---|
| Offline JWT verification | Still passes until the token's exp |
Online check (/session/check) | Fails at once with revoked |
The maximum delay for offline checks is the remaining token lifetime, up to one hour.
Browser polling (startSessionMonitor) only clears the token in that browser. It cannot stop someone who copied the token from calling your backend. If an action is sensitive, check online from your backend.
Online checks from your backend
Use the server helper with authorizationCheck: "required":
import { createShooVerifier } from "@shoojs/auth/server";
export const verifyShooTokenOnline = createShooVerifier({
appOrigin: process.env.APP_ORIGIN!,
authorizationCheck: "required"
});It verifies the token offline, then asks Shoo whether the app is still approved. It denies access on revocation, errors, and timeouts.
Run offline verification for ordinary requests. Use the online check only for actions that need it, such as payments, deletes, or permission changes. See server verification.
Online check endpoint
For other languages, call the endpoint directly:
POST https://shoo.dev/session/check
Authorization: Bearer <token>
Origin: https://app.example.comShoo derives the expected audience from Origin. Send your exact app origin.
200 { "status": "active" }: allow.401 { "status": "login_required", "reason": "revoked" | "expired" | "invalid_token" }: deny.- Anything else, a timeout, or no response: deny. A required check that cannot complete must not allow access.
Do not follow redirects with the bearer token.
Your own app session
If your backend exchanges a Shoo token for its own session cookie, that session has its own expiry and revocation rules. Shoo cannot end it.
- A long app session keeps working after the user disconnects your app in Shoo.
- If you want Shoo revocation to apply, keep your session no longer than the token, or call the online check on sensitive actions.
What each control does
| Action | Where | Effect |
|---|---|---|
| App sign-out | Your app (clearIdentity) | Removes the token from this browser. A copied token still works offline until exp. |
| Shoo logout | shoo.dev/me | Ends every Shoo session of this user, in all browsers. Copied Shoo session cookies stop working. Unused authorization codes and sign-ins in progress stop working. Next sign-in needs Google. |
| Disconnect app | shoo.dev/me | Online checks fail. Offline checks pass until exp. Next sign-in asks for approval again. |
| Delete Shoo account | shoo.dev/me | Deletes the account. Keeps a hashed deletion record. Online checks fail. Offline checks pass until exp. Does not delete your app's data. |
Shoo logout does not sign the user out of apps:
- App tokens that Shoo already issued stay valid until
exp, up to one hour. Offline and online checks both still pass, because the app is still approved. - Your own app session is separate. Shoo cannot end it.
To stop one app, the user disconnects it. To end your app's session, sign the user out in your app.
Signing keys
Tokens are signed with ES256. The token header has a kid. Public keys are at https://shoo.dev/.well-known/jwks.json.
Fetch keys from the JWKS URL and cache them, as jose's createRemoteJWKSet does. Do not copy a key into your code. Keys can change.