Sessions and access

Sessions

Session cookies, reading the user, refresh, local verification and revocation.

Cookies#

After sign-in the SDK sets httpOnly cookies on your app's domain. They are SameSite=Lax, Path=/, and Secure in production. Their name comes from your client ID, so two apps on one domain don't read each other's cookies.

CookieHolds
<clientId>_sessionThe access token: a JWT signed (RS256) with your environment’s key.
<clientId>_session_refreshThe refresh token, used to renew the access token.
<clientId>_session_mfaOnly during an embedded sign-in with MFA: the pending sign-in, for 10 minutes.

By default an access token lasts 60 minutes and a session 7 days; both are set per environment under Authentication → Sessions. Set cookie.name in the config to choose another name.

Reading the session#

CallWhereChecks with the API?Returns
getAuth(config)Next.js serverNo: verifies the signature locallyVerified claims, userId and has(), or null
getUser(config)Next.js serverYes: sees revoked sessionsAuthUser, or null (refreshes first when needed)
req.user (authMiddleware)ExpressYesAuthUser
useSession()BrowserThrough your /api/auth/userThe user for display, loading, has(), refresh()
app/account/page.tsxtype-checked

// app/account/page.tsx — the user record, checked with the API
import { redirect } from 'next/navigation';
import { getUser } from '@3een/auth/nextjs';
import { authConfig } from '@/lib/auth-config';

export default async function AccountPage() {
  // Asks the API, so a revoked session is refused here even while its token
  // would still verify locally. Refreshes an expiring session first.
  const user = await getUser(authConfig);
  if (!user) redirect('/api/auth/signin?redirect_uri=/account');

  return (
    <dl>
      <dt>Email</dt>
      <dd>{user.email}</dd>
      <dt>Verified</dt>
      <dd>{user.emailVerified ? 'Yes' : 'No'}</dd>
    </dl>
  );
}

Refresh#

When the access token is within five minutes of expiring and a refresh cookie exists, the Next.js middleware, getUser and the Express authMiddleware renew it through the API and write the new cookies. If the refresh call fails, for example on a network error, the current token keeps working until it expires. From your own server code, AuthClient.refreshToken(refreshToken) does the same.

Verifying a token yourself#

verifySessionToken(token, { clientId, apiUrl }) checks a token against your environment's published keys at /.well-known/jwks.json?client_id=…. The keys are cached, and fetched again when a new key ID appears (after a rotation). It never throws; it returns one of three results.

lib/verify-bearer.tstype-checked

// Verify a session token yourself, e.g. in a service that receives
// `Authorization: Bearer <token>` from your frontend.
import { verifySessionToken } from '@3een/auth';

export async function userIdFromBearer(authorization: string | null): Promise<string | null> {
  const token = authorization?.startsWith('Bearer ') ? authorization.slice(7) : null;
  if (!token) return null;

  const result = await verifySessionToken(token, {
    clientId: process.env.AUTH_CLIENT_ID!,
    apiUrl: process.env.AUTH_API_URL,
  });

  switch (result.status) {
    case 'valid':
      return result.claims.id ?? null;
    case 'legacy':
      // An old HS256 session: no longer accepted anywhere. Sign in again.
      return null;
    case 'invalid':
      console.warn('Session refused:', result.reason);
      return null;
  }
}
  • A token from another environment, another customer's app or the 3een dashboard is invalid here, even though its signature is genuine: keys are per environment.
  • If the key set can't be fetched, the token is invalid. An outage never becomes a way in.

Revocation#

A session ends when the user signs out, when their password is reset (every session of that user), when an administrator removes it (DELETE /app/users/:id/sessions/:sessionId), or when the account is blocked or deprovisioned. A revoked session can't be refreshed.