Token Verification & Refresh

Verify a user's identity from your backend using an API key. Refresh expired access tokens transparently — the user never sees a session interruption.

Token types

TokenLifetimeStored asPurpose
access_token 15 minutes httpOnly cookie Sent with every authenticated request. Short-lived to limit exposure.
refresh_token 7 days httpOnly cookie Used only to get a new access token when the old one expires.
partial_token 5 minutes httpOnly cookie Intermediate state during 2FA — exchanged for a full session.
ℹ️
All cookies are set with httpOnly and Secure flags. The browser JavaScript on your client app never has access to the raw token values.

Verify a token

Call this from your backend to confirm a user's identity and check their membership status. Requires an API key — this endpoint is designed for server-to-server use, never called from the browser.

POST /public/verify

Headers

HeaderDescription
X-Org-SlugrequiredYour organization's slug.
X-API-KeyrequiredYour org API key (pk_live_…).

Request body

FieldTypeDescription
token string required The user's access_token JWT string.
Request
curl -X POST https://authworx.uthings.io/api/v1/public/verify \
  -H "X-Org-Slug: acme-corp" \
  -H "X-API-Key: pk_live_abc123" \
  -H "Content-Type: application/json" \
  -d '{ "token": "eyJhbGciOiJIUzI1NiIs..." }'
Response — valid token (200)
{
  "status": "success",
  "data": {
    "valid": true,
    "user": {
      "id": "usr_01HXYZ",
      "name": "Alice Chen",
      "email": "alice@acme.com",
      "role": "user",
      "isEmailVerified": true
    },
    "membership": {
      "role": "member",
      "status": "active",
      "joinedAt": "2025-01-15T10:00:00.000Z"
    },
    "org": {
      "id": "org_01ABCD",
      "name": "Acme Corp",
      "slug": "acme-corp",
      "plan": "pro",
      "status": "active"
    }
  }
}
Response — invalid or expired token (200)
{
  "status": "success",
  "data": { "valid": false }
}
⚠️
Always check data.valid === true and data.membership.status === "active" before granting access. A user may be valid but suspended from the org.

Refresh an access token

Exchange a valid refresh token for a new access token. The old access token is invalidated. Call this when a protected route returns 401 — or proactively before a long operation.

POST /auth/refresh

Cookies required

CookieDescription
refresh_tokenSet during login. Valid for 7 days.
Response — success (200)
{
  "status": "success",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
  }
}
StatusCause
401Refresh token is missing, expired (7 days), or revoked. User must log in again.

Silent refresh pattern

The recommended pattern is transparent refresh inside your protected API route. The user never sees a 401 — if the access token is expired, the route silently refreshes it and retries the verify, all in a single request.

app/api/me/route.ts — transparent refresh
import { cookies } from 'next/headers';
import { authworxVerifyToken, authworxRefreshToken } from '@/lib/authworx';
import { setAuthCookies, clearAuthCookies } from '@/lib/cookies';

async function attemptRefresh(jar: Awaited<ReturnType<typeof cookies>>) {
  const rt = jar.get('refresh_token')?.value;
  if (!rt) return null;

  const { data, error } = await authworxRefreshToken(rt);
  if (error || !data?.accessToken) return null;

  setAuthCookies(jar, data.accessToken, data.refreshToken);
  return data.accessToken;
}

export async function GET() {
  const jar = await cookies();
  let token = jar.get('access_token')?.value;

  // No access token — try refresh before giving up
  if (!token) {
    token = (await attemptRefresh(jar)) ?? undefined;
    if (!token) {
      clearAuthCookies(jar);
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }
  }

  let result = await authworxVerifyToken(token);

  // Token present but invalid (expired mid-flight) — refresh once and retry
  if (!result.data?.valid) {
    const fresh = await attemptRefresh(jar);
    if (!fresh) {
      clearAuthCookies(jar);
      return Response.json({ error: 'Unauthorized' }, { status: 401 });
    }
    result = await authworxVerifyToken(fresh);
  }

  if (!result.data?.valid || result.data.membership?.status !== 'active') {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }

  return Response.json({ status: 'success', data: result.data });
}
💡
Copy the attemptRefresh helper into any protected route to get transparent refresh for free. You only need one instance per route handler.