Skip to content

ID token & UserInfo

The id_token claims — including amr (how the user authenticated) and auth_time — and the UserInfo endpoint (GET|POST /api/oauth/userinfo) that returns scope-gated identity claims.

Updated

OpenID Connect gives you two sources of identity: the id_token returned by the token endpoint (a signed statement about who signed in and how), and the UserInfo endpoint (a live, server-verified lookup of the user’s claims). Both are gated by the scopes the user granted.

The id_token

Issued when the openid scope is granted. It is an RS256 JWT (typ: JWT) that verifies offline against the JWKS.

Claims

ClaimAlwaysMeaning
issIssuer — https://entrybit.net.
subStable EntryBit user identifier.
audYour client_id.
iatIssued-at (Unix seconds).
expExpiry (Unix seconds).
nonceEchoed from your authorize request — verify it matches.
auth_timeWhen the user actually authenticated (Unix seconds).
amrHow the user authenticated (see below).
at_hashBinds the id_token to the access token.
email, email_verifiedWith the email scope.
name, pictureWith the profile scope.

amr — how the user signed in

The amr (Authentication Methods References, RFC 8176) array records the factors used at login. EntryBit stamps these values:

ValueMeaning
pwdPassword.
otpOne-time code (authenticator / email).
smsSMS one-time code.
swkPasskey (software-backed key).
userPasskey with user verification.
fedFederated login (Google).
mfaTwo or more factors were used.

For example, ["swk","user"] is a verified passkey, ["pwd","otp","mfa"] is password plus a code, and ["fed"] is Google. Combined with auth_time, this lets a sensitive screen require a recent or stronger login — request one with prompt=login or max_age on the authorize request.

Validating it yourself

If you decode the id_token on the client, fully validate the JWT — do not trust decoded claims blindly:

  1. Verify the signature against /.well-known/jwks.json.
  2. Check iss == https://entrybit.net.
  3. Check aud == your client_id.
  4. Check exp is in the future.
  5. Check nonce equals the value you sent (so make nonce required if you consume the id_token). Many libraries — including expo-auth-session — do not validate id_tokens for you.

With jose in Node.js, steps 1–4 are options and step 5 is yours:

// npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://entrybit.net/.well-known/jwks.json'));

export async function verifyIdToken(idToken: string, expectedNonce: string) {
  // Steps 1–4 — signature, iss, aud, exp — enforced by jose.
  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer: 'https://entrybit.net',
    audience: CLIENT_ID,
    algorithms: ['RS256'],
  });

  // Step 5 — the nonce binds this id_token to YOUR authorize request.
  if (payload.nonce !== expectedNonce) {
    throw new Error('id_token nonce mismatch — possible replay');
  }

  return payload; // { sub, amr?, auth_time?, email?, name?, … }
}

If you only need the user’s profile, prefer UserInfo below — it is already server-verified.

UserInfo endpoint

Returns the signed-in user’s claims, gated by the granted scopes. Available over GET and POST (OIDC Core §5.3.1 — the two are identical).

GET  /api/oauth/userinfo
POST /api/oauth/userinfo
Authorization: Bearer <access_token>

Requires the openid scope. The email and profile scopes gate their respective claims.

curl https://entrybit.net/api/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Response

{
  "sub": "usr_9f1c2ab34cd56ef7",
  "email": "dana@example.com",
  "email_verified": true,
  "name": "Dana Cohen",
  "picture": "https://entrybit.net/…"
}
FieldPresent when
subAlways (with openid).
email, email_verifiedThe email scope was granted.
name, pictureThe profile scope was granted.

Errors

401/403 responses carry a WWW-Authenticate: Bearer challenge:

StatuserrorMeaning
401invalid_tokenMissing, expired, or invalid access token — refresh or re-login.
403insufficient_scopeThe token lacks openid.

See Errors for the full Bearer-challenge contract.