The two JWT tokens
Ave returns two different JWTs on a successful token exchange. Understanding the difference is critical before writing any validation or auth integration.- id_token
- access_token_jwt
- access_token (opaque)
The Profile claims (
id_token is an OIDC identity token. It proves who the user is. It is signed by Ave and its audience is your client ID.name, preferred_username, picture) are only present if you requested the profile scope. email is only present if you requested email and the selected identity has a verified email. nonce is only present if you included one in the authorization request.Use id_token for:- Establishing your app session (validating who logged in)
- Convex custom auth integration
- Any system that needs to verify Ave-issued identity
If you are building Convex auth, use
id_token. Its aud is your clientId, which is what Convex validates against. The access_token_jwt has aud: "https://aveid.net" — Convex will reject it because the audience does not match your app.Claim reference
Full claim reference
Full claim reference
| Claim | Present in | Description |
|---|---|---|
iss | Both JWTs | Always https://aveid.net |
sub | Both JWTs | Identity UUID (changes per Ave identity) |
aud | Both JWTs | clientId in id_token; https://aveid.net in access_token_jwt |
exp | Both JWTs | Expiry timestamp (Unix seconds) |
iat | Both JWTs | Issued-at timestamp |
sid | Both JWTs | Permanent user UUID (stable across all identities) |
azp | id_token only | Your client ID (authorized party) |
auth_time | id_token only | When the user last authenticated |
nonce | id_token only | Echo of the nonce from the authorization request |
name | id_token only | Display name (requires profile scope) |
preferred_username | id_token only | Identity handle (requires profile scope) |
email | id_token only | Verified email address (requires email scope) |
picture | id_token only | Avatar URL (requires profile scope) |
cid | access_token_jwt only | Your client ID |
scope | access_token_jwt only | Space-separated granted scopes |
uid | access_token_jwt only | User UUID (only if user_id scope + app config) |
Full flow
There are three ways to implement this — pick what fits your setup.- SDK (recommended)
- Embed
- Manual
Two function calls cover the entire flow.
startPkceLogin generates PKCE, nonce, and state; stores them in sessionStorage; and redirects. finishPkceLogin reads them back, verifies the returned state, exchanges the code, and validates the returned tokens.finishPkceLogin verifies token signatures internally — no separate validation step needed. It returns null when there’s no code in the URL, so it’s safe to mount on your callback route without guarding against non-callback page loads.Refresh token flow
Refresh tokens allow you to get new access tokens without requiring the user to log in again. They are only issued when you requestoffline_access scope.
Userinfo endpoint
GET /api/oauth/userinfo returns live identity claims. Use this when you need fresh data that may not be in the cached id_token.
sub (identity UUID).
OIDC discovery & manual token validation
OIDC discovery document
OIDC discovery document
Fetch the discovery document to programmatically get endpoint URLs and signing key locations:
Manual ID token validation sequence
Manual ID token validation sequence
For non-JS environments or any library that validates tokens without the SDK, follow this order:
Fetch JWKS
GET https://aveid.net/.well-known/jwks.json. Cache the response and re-fetch only when you encounter an unknown kid.Check timestamps
Verify
exp > now and iat <= now. Use a small clock tolerance (60 seconds) to handle drift.Verify issuer and audience
iss must equal https://aveid.net. aud must equal your client ID. Reject anything else.Edge cases
Authorization code expires in seconds
Authorization code expires in seconds
Redirect URI must match exactly
Redirect URI must match exactly
Protocol, hostname, path, and query string must all match exactly. A trailing slash difference will cause
invalid_grant. Register the exact URI you use.Development mode relaxes this only for localhost, loopback, and Expo Go redirect URLs so local ports can change without adding every callback. Production callback URLs still need explicit registration.Missing id_token
Missing id_token
The
openid scope was not requested or was not granted. Add openid to your scope list.Missing refresh_token
Missing refresh_token
The
offline_access scope was not requested or was not granted. Add offline_access to your scope list.uid claim missing from access_token_jwt
uid claim missing from access_token_jwt
The
user_id scope was requested, but your app doesn’t have allowUserIdScope configured in the developer portal, or the scope wasn’t granted. The uid claim only appears when both conditions are met.