Verify on your server

Check who signed in on your own backend with verifyAccessToken (website apps) or verifyIdToken (apps without a secret), and why asking UNET who a token belongs to is not enough.

If your backend gives a person something because they signed in with UNET, it must check, itself, who the person is. Never trust an id, a name, or a "signed in" flag that the browser sends. This page shows the two checks the SDK provides and when to use each.

Why not just ask UNET whose token it is

It is tempting to take the access token the browser sends and ask UNET's profile endpoint who it belongs to. That works for any valid token, including a token UNET issued to a different app that the same person signed in to. A hostile app could pass such a token to your server and be let in as that person.

The check has to tie the token to your app. The SDK offers two ways, depending on what your app is:

Your appUseWhat it proves
A website with a server (it has an App ID and an App Secret)verifyAccessTokenThe token is live and was issued to your app. Notices when the person removes your app.
A single-page site or app (no secret)verifyIdTokenA signed statement by UNET that this person signed in to your App ID. Does not know about later removal.

Website apps: verifyAccessToken

Keep the App Secret on the server. The browser sends the access token (await unet.getAccessToken()), your server asks UNET whether it is live and whose it is.

import express from "express";
import { verifyAccessToken } from "@unet/id-web";

const app = express();

app.use(async (req, res, next) => {
  const token = req.headers.authorization?.replace(/^Bearer /, "");
  if (!token) return res.status(401).end();

  const user = await verifyAccessToken(token, {
    clientId: process.env.UNET_CLIENT_ID!,
    clientSecret: process.env.UNET_CLIENT_SECRET!,
  });
  if (!user) return res.status(401).end(); // expired, removed by the person, or issued to another app

  res.locals.user = user; // user.sub is the person's id for your app
  next();
});

The result has sub, the profile fields (username, displayName, avatarUrl), scopes and expiresAt (epoch milliseconds). Pass profile: false to skip the second request that reads the profile; you then get sub, scopes and expiresAt.

Apps without a secret: verifyIdToken

A public app cannot ask UNET as an app, so the front end hands over the ID token instead, which UNET signed for your App ID. Your server checks the signature locally against UNET's published keys, so there is no request to UNET for most checks.

In the browser:

const idToken = await unet.getIdToken(); // null when signed out, or the server issued none
await fetch("/api/me", { headers: { Authorization: `Bearer ${idToken}` } });

On the server:

import { verifyIdToken } from "@unet/id-web";

export async function whoIs(idToken: string) {
  const claims = await verifyIdToken(idToken, { clientId: "unet_xxx" });
  if (!claims) return null; // forged, for another app, or expired
  return claims.sub; // the person's id for your app
}

verifyIdToken checks the RS256 signature, the issuer, that the audience is your App ID, and the expiry (60 seconds of clock difference are tolerated; change it with clockToleranceSeconds). The keys are cached for ten minutes, and a key rotation is picked up. It needs Web Crypto: Node 19 or newer, Deno, Bun or Cloudflare Workers.

An ID token lives about an hour like the access token. getIdToken() renews it with the session, so ask it each time you call your server.

Store sub, not the name

Use sub as the key of your user table. It is stable for the person on your app and different on every other app by default, so it cannot be used to recognise the person elsewhere. username and displayName can change, so show them but do not key on them. See User ids.

Handling errors

Both functions answer null when the token is not valid, and throw when they cannot be sure. Never treat a thrown error as "valid", and do not treat it as "forged" either: it may be an outage.

ThrownMeaningWhat to do
UnetIdError with code: "config"A required option is missing (clientId, clientSecret).Fix your configuration.
UnetIdError with code: "invalid_client"UNET rejected the App ID or Secret, or the app is not a website app.Check the secret; single-page apps use verifyIdToken.
NetworkErrorUNET or its keys could not be reached.Answer 503 and let the client retry.
UnetIdError with code: "introspection_error", "userinfo_error" or "jwks_error"UNET answered with an error.Answer 503; log it.

Caching

verifyAccessToken asks UNET each time. The token endpoints share a per-address rate limit, so a busy server should not verify on every request. Keep a successful result for a short time, for example a minute, keyed by the token, and drop it when the token expires (expiresAt). A longer cache delays noticing that a person removed your app. For a longer-lived session of your own, verify once, then issue your own session cookie, and re-verify when it matters.

Common mistakes

  • Checking with the profile endpoint only. It does not tell whether the token was issued to your app.
  • Putting the App Secret in front-end code. A website app's secret is for servers only. Single-page apps use verifyIdToken.
  • Using the username as the key. Use sub.
  • Treating a thrown error as a pass or a fail. It means "unknown".

What next