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
iss✓Issuer — https://entrybit.net.
sub✓Stable EntryBit user identifier.
aud✓Your client_id.
iat✓Issued-at (Unix seconds).
exp✓Expiry (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://media.entrybit.net/media/avatars/…/full/….webp"
}
FieldPresent when
subAlways (with openid).
email, email_verifiedThe email scope was granted.
name, pictureThe profile scope was granted.

picture, here and in the id_token, is always an absolute https:// URL: the 256px copy of an uploaded photo on media.entrybit.net, or the address of the user’s Google picture. It is omitted when the account has no picture, so treat it as optional even with the profile scope.

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.