> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aveid.net/llms.txt
> Use this file to discover all available pages before exploring further.

# @ave-id/sdk OAuth

> Standard OAuth / PKCE functions for registered Ave apps. Covers PKCE helpers, buildAuthorizeUrl, exchangeCode, refreshToken, fetchUserInfo, and client/server helpers.

These functions are for **registered Ave apps** (apps with a `clientId` from the developer portal). If you haven't registered yet, start with [Quick Ave](/guides/quick-ave) or the [quickstart](/quickstart).

```bash theme={null}
bun add @ave-id/sdk
```

## Import paths

| Import                     | Contents                                                                                                                                                                | Runtime         |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `@ave-id/sdk`              | Core OAuth, PKCE helpers, `verifyJwt`, `fetchJwks`                                                                                                                      | Browser or Node |
| `@ave-id/sdk/client`       | `startPkceLogin`, `finishPkceLogin`, `signIn`, `signInWithFedCm` — browser redirect + FedCM helpers                                                                     | Browser only    |
| `@ave-id/sdk/expo-session` | `configureAveSdkForExpo`, `completeExpoOAuthCallback`, `AveSession`, SecureStore adapter, `wireAveSessionToConvex` — React Native / Expo (needs `expo-crypto` for PKCE) | Expo            |
| `@ave-id/sdk/next`         | `AveSessionProvider`, `useAveSession`, `AveConvexBridge` — Next.js App Router (peer: `react`)                                                                           | Browser         |
| `@ave-id/sdk/server`       | `exchangeCodeServer`, `refreshTokenServer`, `verifyJwt` — uses `clientSecret`                                                                                           | Server only     |

<Warning>
  Never import from `@ave-id/sdk/server` in browser code. Those functions require your `clientSecret`, which must never be exposed to the client.
</Warning>

## Config object

Most functions take a config object as the first argument:

```ts theme={null}
interface AveConfig {
  clientId: string;     // Your app's client ID
  redirectUri: string;  // Registered redirect URI
  issuer?: string;      // Base URL for Ave user-facing pages (default: "https://aveid.net"). Do not set this to the API host "https://api.aveid.net".
}
```

***

## PKCE helpers

### `generateCodeVerifier()`

Generates a cryptographically random PKCE code verifier.

```ts theme={null}
function generateCodeVerifier(): string
```

Returns a 43-character base64url-encoded random string. Store this in `sessionStorage` before redirecting and send it as `codeVerifier` when exchanging the code.

```ts theme={null}
const verifier = generateCodeVerifier();
sessionStorage.setItem("ave_verifier", verifier);
```

***

### `generateCodeChallenge(verifier)`

Generates the PKCE code challenge from a verifier using SHA-256.

```ts theme={null}
function generateCodeChallenge(verifier: string): Promise<string>
```

<ParamField path="verifier" type="string" required>
  The code verifier from `generateCodeVerifier()`.
</ParamField>

Returns `Promise<string>` — the base64url-encoded SHA-256 hash. Pass this as `codeChallenge` to `buildAuthorizeUrl`.

```ts theme={null}
const challenge = await generateCodeChallenge(verifier);
```

***

### `generateNonce()`

Generates a random nonce for ID token replay protection.

```ts theme={null}
function generateNonce(): string
```

Returns a 32-character base64url-encoded random string. Pass it as `nonce` to `buildAuthorizeUrl` and verify it matches the `nonce` claim in the `id_token` after exchange.

***

## Authorization functions

### `buildAuthorizeUrl(config, params)`

Builds the URL to redirect users to Ave for sign-in.

```ts theme={null}
function buildAuthorizeUrl(
  config: AveConfig,
  params?: {
    scope?: Scope[];
    state?: string;
    nonce?: string;
    codeChallenge?: string;
    codeChallengeMethod?: "S256" | "plain";
    organizationId?: string;
    extraParams?: Record<string, string>;
  }
): string
```

<ParamField path="config.clientId" type="string" required>
  Your app's client ID.
</ParamField>

<ParamField path="config.redirectUri" type="string" required>
  The registered redirect URI the user will be sent to after authorization.
</ParamField>

<ParamField path="params.scope" type="Scope[]" default="[&#x22;openid&#x22;, &#x22;profile&#x22;, &#x22;email&#x22;]">
  Scopes to request. Available values: `"openid"`, `"profile"`, `"email"`, `"offline_access"`, `"user_id"`.
</ParamField>

<ParamField path="params.state" type="string">
  CSRF protection token. Generate with `crypto.randomUUID()`. Store and verify on the callback.
</ParamField>

<ParamField path="params.nonce" type="string">
  ID token replay protection. Generate with `generateNonce()`. Verify against the `nonce` claim in `id_token`.
</ParamField>

<ParamField path="params.codeChallenge" type="string">
  PKCE challenge from `generateCodeChallenge()`. Required for public clients.
</ParamField>

<ParamField path="params.organizationId" type="string">
  Optional Ave Business organization ID. When present, Ave verifies that the selected identity belongs to the organization and includes `org_id`, `org_role`, `org_scopes`, `org_signing_authority`, `org_encryption_mode`, `org_key_custody`, and SSO auth metadata in issued tokens. If the organization requires SSO, the current Ave session must come from that organization's active SSO connection.
</ParamField>

<ParamField path="params.prompt" type={`"none" | "login" | "consent" | "select_account" | string`}>
  OAuth-style interaction hint. Use `select_account` when your app offers **Switch account** or **Sign out** so Ave always shows the swipe-to-authorize screen and identity picker instead of silently reusing the last authorization. Combine values with a space (for example `consent select_account`).
</ParamField>

<ParamField path="params.extraParams" type="object">
  Extra query parameters on the Ave sign-in URL. Prefer standard OAuth parameters; encryption is requested via `e2ee:*` scopes — see [App encryption](/sdk/sdk-identity-keys).
</ParamField>

```ts theme={null}
const url = buildAuthorizeUrl(
  { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
  {
    scope: ["openid", "profile", "email", "offline_access"],
    state: crypto.randomUUID(),
    nonce: generateNonce(),
    codeChallenge: challenge,
    codeChallengeMethod: "S256",
  }
);
window.location.href = url;
```

***

### `buildSwitchAccountUrl(config, params?)`

Convenience wrapper around `buildAuthorizeUrl` that sets `prompt=select_account`. Use from your app's **Switch account** or **Use a different profile** action so Ave blocks quick re-authorization and always shows the identity picker plus swipe-to-authorize UI.

```ts theme={null}
function buildSwitchAccountUrl(
  config: AveConfig,
  params?: Parameters<typeof buildAuthorizeUrl>[1]
): string
```

```ts theme={null}
import { buildSwitchAccountUrl, generateCodeChallenge, generateCodeVerifier } from "@ave-id/sdk";

const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);

window.location.href = buildSwitchAccountUrl(
  { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
  {
    scope: ["openid", "profile", "email", "offline_access"],
    state: crypto.randomUUID(),
    nonce: generateNonce(),
    codeChallenge: challenge,
    codeChallengeMethod: "S256",
  }
);
```

For embed flows, pass `extraParams: { prompt: "select_account" }` to `openAveSheet` / `openAvePopup` instead.

***

### `exchangeCode(config, payload)`

Exchanges an authorization code for tokens. Use for PKCE (public client) flows.

```ts theme={null}
function exchangeCode(
  config: AveConfig,
  payload: { code: string; codeVerifier?: string }
): Promise<TokenResponse>
```

<ParamField path="payload.code" type="string" required>
  The authorization code from the callback URL.
</ParamField>

<ParamField path="payload.codeVerifier" type="string">
  The PKCE code verifier from `sessionStorage`. Required if you used PKCE.
</ParamField>

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

***

### `refreshToken(config, payload)`

Exchanges a refresh token for a new set of tokens.

```ts theme={null}
function refreshToken(
  config: AveConfig,
  payload: { refreshToken: string }
): Promise<TokenResponse>
```

<ParamField path="payload.refreshToken" type="string" required>
  The refresh token from a previous token response.
</ParamField>

<Warning>
  Refresh tokens **rotate on every use** — each successful refresh returns a new refresh token and invalidates the old one. Store the new token immediately after every refresh. Sending an old token returns `invalid_grant` and may revoke the entire token family.
</Warning>

***

### `fetchUserInfo(config, accessToken)`

Fetches live identity claims from the userinfo endpoint.

```ts theme={null}
function fetchUserInfo(config: AveConfig, accessToken: string): Promise<UserInfo>
```

<ParamField path="accessToken" type="string" required>
  The opaque `access_token` (not the JWT) from the token response.
</ParamField>

Returns `UserInfo` with `sub`, `name?`, `preferred_username?`, `email?`, `picture?`, and `organization?` when the token was issued for an Ave Business workspace.

***

### `listAveWorkspaceOrganizations(config, accessToken)`

Lists Ave Business organizations that the token identity can use. Use it to build a workspace picker before requesting organization context.

```ts theme={null}
function listAveWorkspaceOrganizations(
  config: { issuer?: string; clientId?: string; fetcher?: typeof fetch },
  accessToken: string
): Promise<AveWorkspaceOrganization[]>
```

```ts theme={null}
import { listAveWorkspaceOrganizations } from "@ave-id/sdk";

const organizations = await listAveWorkspaceOrganizations(
  { clientId: "YOUR_CLIENT_ID" },
  tokens.access_token
);
```

Pass the selected organization's `id` as `organizationId` to `startPkceLogin()` or `buildAuthorizeUrl()`.

***

### `createAveWorkspaceOrganization(config, accessToken, params)`

Creates an Ave Business workspace for the token identity. Call this only after your UI tells the user that it will create an Ave-managed workspace.

```ts theme={null}
function createAveWorkspaceOrganization(
  config: { issuer?: string; clientId?: string; fetcher?: typeof fetch },
  accessToken: string,
  params: {
    name: string;
    clientId?: string;
    userConfirmedAveWorkspaceCreation: true;
  }
): Promise<AveWorkspaceOrganization>
```

```ts theme={null}
import { createAveWorkspaceOrganization } from "@ave-id/sdk";

const organization = await createAveWorkspaceOrganization(
  { clientId: "YOUR_CLIENT_ID" },
  tokens.access_token,
  {
    name: "Example Co",
    userConfirmedAveWorkspaceCreation: true,
  }
);
```

The returned `organization.id` is the `organizationId` for the follow-up workspace sign-in.

***

## Client helpers

Import from `@ave-id/sdk/client`. Browser-only.

### `supportsFedCm()`

Checks whether the current browser environment is eligible to attempt FedCM.

```ts theme={null}
function supportsFedCm(): boolean
```

***

### `signInWithFedCm(params)`

Attempts a browser-native FedCM sign-in and returns tokens immediately without a redirect callback.

```ts theme={null}
async function signInWithFedCm(params: {
  clientId: string;
  redirectUri: string;
  scope?: string;
  issuer?: string;
  state?: string;
  nonce?: string;
  prompt?: "none" | "login" | "consent" | "select_account" | string;
  mediation?: CredentialMediationRequirement;
}): Promise<FedCmTokenResponse>
```

Ave returns the normal token payload. For E2EE apps, Ave continues in a FedCM dialog so the existing Ave authorization UI can unlock the user's master key and prepare the plaintext `app_key` handoff before resuming automatically.

FedCM does not claim Ave Business organization-context support. Use `signIn()` or `startPkceLogin()` with `organizationId` when signing into a business workspace.

***

### `signIn(params)`

Smart sign-in helper for browser apps.

It tries FedCM first when available, and falls back to the standard PKCE redirect flow when it isn't.

```ts theme={null}
async function signIn(params: {
  clientId: string;
  redirectUri: string;
  scope?: string;
  issuer?: string;
  state?: string;
  nonce?: string;
  organizationId?: string;
  mediation?: CredentialMediationRequirement;
  preferFedCm?: boolean;
}): Promise<FedCmTokenResponse | null>
```

Returns:

* `FedCmTokenResponse` when FedCM completes in-page
* `null` when the helper falls back to `startPkceLogin()` and redirects the browser

When `organizationId` is present, `signIn()` uses the PKCE redirect flow so Ave can verify organization membership and SSO policy before issuing tokens.

```ts theme={null}
import { finishPkceLogin, signIn } from "@ave-id/sdk/client";

const result = await signIn({
  clientId: "YOUR_CLIENT_ID",
  redirectUri: "https://yourapp.com/callback",
  scope: "openid profile email offline_access",
});

if (result) {
  // FedCM completed immediately
} else {
  // Browser redirected for PKCE fallback
}

// On your callback page:
const tokens = await finishPkceLogin({
  clientId: "YOUR_CLIENT_ID",
  redirectUri: "https://yourapp.com/callback",
});
```

### `startPkceLogin(params)`

Generates PKCE params, saves them to `sessionStorage`, and redirects the user to Ave. The browser-friendly alternative to calling `generateCodeVerifier` + `buildAuthorizeUrl` + `window.location.href` manually.

```ts theme={null}
async function startPkceLogin(params: {
  clientId: string;
  redirectUri: string;
  scope?: string;
  issuer?: string;
  state?: string;   // optional — auto-generated if omitted
  nonce?: string;   // optional — auto-generated if omitted
  organizationId?: string;
  prompt?: "none" | "login" | "consent" | "select_account" | string;
}): Promise<void>
```

Automatically generates a PKCE verifier/challenge, a `nonce` for ID token replay protection, and a `state` value for CSRF protection — unless you supply your own. All three are stored in `sessionStorage` and retrieved automatically by `finishPkceLogin`.

Set `organizationId` to sign into an Ave Business organization as an app workspace. Ave verifies membership before issuing tokens and includes organization claims in the returned JWTs. See [Business workspaces](/guides/business-workspaces).

***

### `finishPkceLogin(options)`

Handles the OAuth callback: reads stored PKCE state, verifies the returned `state` against the stored value, exchanges the authorization code, verifies the returned tokens, and cleans up `sessionStorage`. Returns `null` when no `code` parameter is present — safe to call on every page load.

```ts theme={null}
async function finishPkceLogin(options: {
  clientId: string;
  redirectUri: string;
  issuer?: string;
  url?: string;        // override the callback URL to parse (default: window.location.href)
  cleanUrl?: boolean;  // remove code/state from the URL after exchange (default: true)
}): Promise<TokenResponse | null>
```

```ts theme={null}
import { startPkceLogin, finishPkceLogin } from "@ave-id/sdk/client";

// On your login page:
await startPkceLogin({
  clientId: "YOUR_CLIENT_ID",
  redirectUri: "https://yourapp.com/callback",
  scope: "openid profile email offline_access",
});

// On your /callback page:
const tokens = await finishPkceLogin({
  clientId: "YOUR_CLIENT_ID",
  redirectUri: "https://yourapp.com/callback",
});

if (tokens) {
  // User is signed in — tokens.id_token, tokens.access_token, etc.
}
```

***

## JWT verification

These functions are available in `@ave-id/sdk`, `@ave-id/sdk/client`, and `@ave-id/sdk/server`.

### `verifyJwt(token, options?)`

Verifies an Ave-issued JWT: parses the token, fetches the OIDC discovery document, validates the RS256 signature against the JWKS, and checks issuer, expiry, audience, and nonce claims. Returns the decoded payload on success or `null` on any validation failure.

```ts theme={null}
async function verifyJwt<T extends JwtPayload = AveJwtClaims>(
  token: string,
  options?: VerifyJwtOptions,
): Promise<T | null>
```

```ts theme={null}
interface VerifyJwtOptions {
  issuer?: string;             // Base issuer URL (default: "https://aveid.net")
  expectedIssuer?: string;     // Exact issuer string to validate iss claim against
  audience?: string | string[]; // Expected aud claim (e.g. your clientId)
  nonce?: string;              // Expected nonce claim (replay protection)
  jwksUrl?: string;            // Override JWKS endpoint URL
  jwks?: JwksResponse;         // Pre-fetched JWKS (skips network call)
  discoveryUrl?: string;       // Override OIDC discovery endpoint
  clockSkewSeconds?: number;   // Clock tolerance for exp/iat checks (default: 30)
  fetcher?: typeof fetch;      // Custom fetch implementation
}
```

<ParamField path="token" type="string" required>
  The raw JWT string to verify.
</ParamField>

<ParamField path="options.audience" type="string | string[]">
  Expected `aud` claim. Pass your `clientId` when verifying `id_token`. Pass `"https://aveid.net"` when verifying `access_token_jwt`.
</ParamField>

<ParamField path="options.nonce" type="string">
  Expected `nonce` claim. Pass the nonce you sent in the authorization request to prevent replay attacks.
</ParamField>

<ParamField path="options.clockSkewSeconds" type="number" default="30">
  Tolerance in seconds applied to `exp` and `nbf` checks to handle clock drift between servers.
</ParamField>

Returns `Promise<T | null>` — the decoded payload typed as `T`, or `null` if any check fails (invalid signature, expired, issuer mismatch, audience mismatch, nonce mismatch, or malformed token).

```ts theme={null}
import { verifyJwt } from "@ave-id/sdk";

// Verify an id_token on your server after code exchange:
const claims = await verifyJwt(tokens.id_token, {
  audience: "YOUR_CLIENT_ID",
  nonce: savedNonce,
});

if (!claims) {
  throw new Error("Invalid id_token");
}

// claims.sub — identity UUID
// claims.email, claims.name, etc.
```

<Note>
  `verifyJwt` fetches the OIDC discovery document and JWKS automatically and caches both for 5 minutes. For server environments with no global `fetch`, pass a `fetcher` option.
</Note>

***

### `fetchJwks(options?)`

Fetches and caches the Ave JWKS (JSON Web Key Set). Used internally by `verifyJwt`. Useful if you need direct access to the signing keys.

```ts theme={null}
async function fetchJwks(options?: {
  issuer?: string;
  jwksUrl?: string;
  fetcher?: typeof fetch;
}): Promise<JwksResponse>
```

```ts theme={null}
import { fetchJwks } from "@ave-id/sdk";

const { keys } = await fetchJwks();
// keys: JwkKey[] — RSA public keys used to verify Ave JWTs
```

***

## Server helpers

Import from `@ave-id/sdk/server`. Server runtime only — requires your `clientSecret`.

### `exchangeCodeServer(config, payload)`

Same as `exchangeCode` but uses `clientSecret` instead of `codeVerifier`. Use this for confidential clients (server-side token exchange).

```ts theme={null}
async function exchangeCodeServer(
  config: { clientId: string; clientSecret: string; redirectUri: string; issuer?: string },
  payload: { code: string }
): Promise<TokenResponse>
```

```ts theme={null}
import { exchangeCodeServer } from "@ave-id/sdk/server";

const tokens = await exchangeCodeServer(
  {
    clientId: process.env.AVE_CLIENT_ID!,
    clientSecret: process.env.AVE_CLIENT_SECRET!,
    redirectUri: "https://yourapp.com/callback",
  },
  { code }
);
```

### `refreshTokenServer(config, payload)`

Refreshes tokens using client secret (confidential client).

```ts theme={null}
async function refreshTokenServer(
  config: { clientId: string; clientSecret: string; redirectUri: string; issuer?: string },
  payload: { refreshToken: string }
): Promise<TokenResponse>
```

***

## TokenResponse

The shape returned by `exchangeCode`, `exchangeCodeServer`, `refreshToken`, and `refreshTokenServer`:

```ts theme={null}
interface TokenResponse {
  access_token: string;      // Opaque bearer token for Ave API endpoints
  access_token_jwt: string;  // Signed Ave API JWT (aud: https://aveid.net)
  id_token?: string;         // OIDC JWT (aud: your clientId) — requires openid scope
  refresh_token?: string;    // Rotation token — requires offline_access scope
  token_type: "Bearer";
  expires_in: number;        // Seconds until access_token expires
  scope: string;             // Space-separated granted scopes
  user_id?: string;          // Present when user_id scope is granted
  user?: {                   // May be absent depending on scopes/config
    id: string;
    handle: string;
    displayName: string;
    email?: string;
    avatarUrl?: string;
  } | null;
}
```

***

## Error handling

All SDK functions throw an `Error` when the server returns a non-2xx response. The error message is the `error` field from the response body (e.g. `"invalid_grant"`, `"invalid_client"`).

```ts theme={null}
try {
  const tokens = await exchangeCode(config, { code, codeVerifier });
} catch (err) {
  if (err.message === "invalid_grant") {
    // Code expired or PKCE verifier mismatch
  }
}
```

***

## See also

* [Identity keys and wrapped payloads](/sdk/sdk-identity-keys) — `getIdentityKey`, envelope decryption, and passing secrets through redirects after OAuth.
