Skip to main content
Need auth before you register an app? Start with Quick Ave. Come back here when you need refresh tokens, app branding, configured redirect URIs, or a confidential client.

Start with the embed flow

For most web apps, open Ave from your sign-in button and let the embed choose the best browser surface.
import { startAveAuth } from "@ave-id/embed";

startAveAuth({
  clientId: "YOUR_CLIENT_ID",
  redirectUri: "https://yourapp.com/callback",
  onSuccess: ({ redirectUrl }) => {
    window.location.assign(redirectUrl);
  },
});
startAveAuth() opens the Ave sheet first, escalates to a popup when a top-level browser context is required, and falls back to a redirect when popups are blocked. Omit clientId to use Quick Ave.

Prerequisites

1

Create an app in the developer portal

Go to devs.aveid.net and create an OAuth app. You’ll receive:
  • Client ID — used in all requests; safe to include in browser code
  • Client secret — server-side only; never put this in browser or mobile code
Building a SPA or any app that runs code in the browser? Use PKCE instead of a client secret. PKCE doesn’t require a secret.
2

Register your redirect URI

Add the exact callback URL where Ave should redirect after login (e.g. https://yourapp.com/callback). The URI must match exactly — no wildcards, no trailing-slash differences.For local development, enable development mode on the app to allow localhost, loopback, and Expo Go callback URLs without registering every port. Keep production callbacks registered explicitly.

Login flow

Using @ave-id/sdk? startPkceLogin and finishPkceLogin handle all of these steps automatically — PKCE generation, storage, state verification, code exchange, and token validation. The steps below show what’s happening under the hood.
1

Generate PKCE parameters and redirect the user

Before redirecting, generate PKCE parameters. They prove that the same browser session that started login is the one completing it.
import { generateCodeVerifier, generateCodeChallenge, generateNonce } from "@ave-id/sdk";

const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = crypto.randomUUID(); // CSRF protection
const nonce = generateNonce();     // Replay protection for id_token

// Store these — you need them when the user returns
sessionStorage.setItem("ave_code_verifier", codeVerifier);
sessionStorage.setItem("ave_state", state);
sessionStorage.setItem("ave_nonce", nonce);

const url = new URL("https://aveid.net/signin");
url.searchParams.set("client_id", "YOUR_CLIENT_ID");
url.searchParams.set("redirect_uri", "https://yourapp.com/callback");
url.searchParams.set("scope", "openid profile email");
url.searchParams.set("state", state);
url.searchParams.set("nonce", nonce);
url.searchParams.set("code_challenge", codeChallenge);
url.searchParams.set("code_challenge_method", "S256");

window.location.href = url.toString();
state prevents CSRF attacks — without it, an attacker can trick your app into completing a login they started. nonce prevents replay attacks against the ID token. Both are random strings you generate and store temporarily.
2

Handle the callback

Ave redirects to your redirect_uri with ?code=AUTH_CODE&state=YOUR_STATE. Validate state before doing anything else.
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const returnedState = params.get("state");

const savedState = sessionStorage.getItem("ave_state");
const codeVerifier = sessionStorage.getItem("ave_code_verifier");

if (!returnedState || returnedState !== savedState) {
  throw new Error("State mismatch — possible CSRF attack");
}

// Clear immediately — these are single-use
sessionStorage.removeItem("ave_state");
sessionStorage.removeItem("ave_code_verifier");
The authorization code expires in seconds. Don’t queue the exchange — call the token endpoint immediately after validating state.
3

Exchange the code for tokens

import { exchangeCode } from "@ave-id/sdk";

const tokens = await exchangeCode(
  { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
  { code, codeVerifier }
);
4

Understand the token response

A successful exchange returns:
{
  "access_token": "ave_at_...",
  "access_token_jwt": "eyJ...",
  "id_token": "eyJ...",
  "refresh_token": "rt_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email",
  "user": {
    "id": "identity-uuid",
    "handle": "alice",
    "displayName": "Alice Smith",
    "email": "[email protected]",
    "avatarUrl": "https://..."
  }
}
Ave returns tokens with different purposes:
TokenFormataud claimUse for
access_tokenOpaque stringCalling Ave’s /api/oauth/userinfo
access_token_jwtSigned JWThttps://aveid.netAve APIs that accept JWTs, Connector delegation
id_tokenSigned OIDC JWTYour client IDUser session, Convex auth
refresh_tokenOpaque stringGetting new tokens without re-login
id_token and access_token_jwt are both signed JWTs with different audiences. The id_token audience is your clientId. The access_token_jwt audience is https://aveid.net. This matters for Convex and any library that validates JWT audience.id_token is only returned when you request the openid scope. refresh_token is only returned with offline_access.
5

Create your app session

Use id_token or the user object to create a session in your app:
import { verifyJwt } from "@ave-id/sdk";

const { user, id_token, access_token, refresh_token } = tokens;
const savedNonce = sessionStorage.getItem("ave_nonce");
sessionStorage.removeItem("ave_nonce");

// Option A: use the user object directly (simplest)
// user.id is the identity UUID — use as your stable user key

// Option B: validate id_token for higher assurance
const claims = await verifyJwt(id_token, {
  audience: "YOUR_CLIENT_ID",
  nonce: savedNonce,
});
if (!claims) throw new Error("Invalid id_token");
// claims.sub, claims.email, claims.name, etc.
Store refresh_token securely, such as in an HTTP-only cookie or server-side session, if you requested offline_access. Never put it in localStorage.

Refresh tokens

When the access token expires, exchange the refresh token for a new set without re-authenticating the user.
import { refreshToken } from "@ave-id/sdk";

const newTokens = await refreshToken(
  { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
  { refreshToken: storedRefreshToken }
);
Refresh tokens are rotated on every use — each successful refresh returns a new refresh token and invalidates the previous one. Store the new token immediately after every refresh. Reusing an old refresh token triggers invalid_grant and may revoke related tokens as a security measure.

Error reference

Client ID or client secret doesn’t match any registered app.
The code or refresh token is bad, expired, or was already used. For refresh tokens, this may also mean reuse detection triggered.
A required field is missing. Common cause: forgetting codeVerifier when PKCE is required.
A requested scope isn’t allowed for this app. Check your app’s scope allowlist in the developer portal.
The Connector requestedResource key doesn’t exist or isn’t active.
The Connector grant is missing or was revoked. Re-run the Connector authorization flow.

Next steps

Token details

JWT payload reference for id_token and access_token_jwt, validation steps, and refresh token rotation.

PKCE deep dive

PKCE security model, browser storage guidance, and anti-patterns to avoid.

Convex auth

Exactly how to wire Ave tokens into Convex, including which token to use and why.

Scopes and claims

Complete scope catalog with the exact JWT claims each scope adds.
Last modified on May 23, 2026