Skip to main content
The authorization code flow is the standard way to authenticate users with Ave. The user is redirected to Ave, authenticates, and Ave redirects back with a short-lived code. You exchange the code for tokens server-side (or via PKCE from the browser).

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.
The 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.
{
  "iss": "https://aveid.net",
  "sub": "identity-uuid",
  "aud": "your_client_id",
  "exp": 1712345678,
  "iat": 1712342078,
  "auth_time": 1712342078,
  "azp": "your_client_id",
  "sid": "user-uuid",
  "nonce": "random-nonce-from-request",
  "name": "Alice Smith",
  "preferred_username": "alice",
  "email": "[email protected]",
  "picture": "https://avatars.aveid.net/..."
}
Profile claims (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

ClaimPresent inDescription
issBoth JWTsAlways https://aveid.net
subBoth JWTsIdentity UUID (changes per Ave identity)
audBoth JWTsclientId in id_token; https://aveid.net in access_token_jwt
expBoth JWTsExpiry timestamp (Unix seconds)
iatBoth JWTsIssued-at timestamp
sidBoth JWTsPermanent user UUID (stable across all identities)
azpid_token onlyYour client ID (authorized party)
auth_timeid_token onlyWhen the user last authenticated
nonceid_token onlyEcho of the nonce from the authorization request
nameid_token onlyDisplay name (requires profile scope)
preferred_usernameid_token onlyIdentity handle (requires profile scope)
emailid_token onlyVerified email address (requires email scope)
pictureid_token onlyAvatar URL (requires profile scope)
cidaccess_token_jwt onlyYour client ID
scopeaccess_token_jwt onlySpace-separated granted scopes
uidaccess_token_jwt onlyUser UUID (only if user_id scope + app config)

Full flow

There are three ways to implement this — pick what fits your setup.

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 request offline_access scope.
import { refreshToken } from "@ave-id/sdk";

const newTokens = await refreshToken(
  { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
  { refreshToken: storedRefreshToken }
);

// Store the new refresh token immediately
storeRefreshToken(newTokens.refresh_token);
Refresh tokens rotate on every use. Each successful refresh invalidates the old token and issues a new one. If you try to reuse an old refresh token, you will get invalid_grant and the server may revoke the entire token family as a security measure. Always persist the new refresh_token from the response before discarding the old one.

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.
GET https://api.aveid.net/api/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN
The returned claims depend on the scopes your access token has. The response always includes sub (identity UUID).

OIDC discovery & manual token validation

Fetch the discovery document to programmatically get endpoint URLs and signing key locations:
GET https://aveid.net/.well-known/openid-configuration
{
  "issuer": "https://aveid.net",
  "authorization_endpoint": "https://aveid.net/signin",
  "token_endpoint": "https://api.aveid.net/api/oauth/token",
  "userinfo_endpoint": "https://api.aveid.net/api/oauth/userinfo",
  "jwks_uri": "https://aveid.net/.well-known/jwks.json",
  "scopes_supported": ["openid", "profile", "email", "offline_access", "user_id"],
  "response_types_supported": ["code"],
  "id_token_signing_alg_values_supported": ["RS256"]
}
For non-JS environments or any library that validates tokens without the SDK, follow this order:
1

Fetch JWKS

GET https://aveid.net/.well-known/jwks.json. Cache the response and re-fetch only when you encounter an unknown kid.
2

Verify signature

Use the key matching the kid in the token header. Ave signs with RS256.
3

Check timestamps

Verify exp > now and iat <= now. Use a small clock tolerance (60 seconds) to handle drift.
4

Verify issuer and audience

iss must equal https://aveid.net. aud must equal your client ID. Reject anything else.
5

Verify nonce

If you sent a nonce in the authorization request, the id_token must contain the same value. This prevents replay attacks.

Edge cases

Don’t store the code and exchange later. Exchange immediately after validating state on the callback page.
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.
The openid scope was not requested or was not granted. Add openid to your scope list.
The offline_access scope was not requested or was not granted. Add offline_access to your scope list.
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.
Last modified on April 28, 2026