> ## 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.

# OAuth authorization code flow

> End-to-end OAuth 2.0 + OIDC flow with full token payload reference, validation steps, and refresh token rotation.

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.

<Tabs>
  <Tab title="id_token">
    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**.

    ```json theme={null}
    {
      "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": "alice@example.com",
      "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
  </Tab>

  <Tab title="access_token_jwt">
    The `access_token_jwt` is a **resource access JWT**. It authorizes calls to Ave's API. Its audience is Ave's own resource audience, not your client ID.

    ```json theme={null}
    {
      "iss": "https://aveid.net",
      "sub": "identity-uuid",
      "aud": "https://aveid.net",
      "exp": 1712345678,
      "iat": 1712342078,
      "scope": "openid profile email",
      "cid": "your_client_id",
      "sid": "user-uuid",
      "uid": "user-uuid"
    }
    ```

    `cid` is your client ID. `uid` is the user UUID, only present when the `user_id` scope was granted.

    **Use `access_token_jwt` for:**

    * Connector token-exchange grant (`subjectToken` field)
    * Passing to Ave API endpoints that accept JWT auth
  </Tab>

  <Tab title="access_token (opaque)">
    The plain `access_token` is an **opaque string**, not a JWT. You cannot decode it.

    **Use the opaque `access_token` for:**

    * Calling `GET /api/oauth/userinfo` with `Authorization: Bearer ACCESS_TOKEN`
    * Any Ave API endpoint that documents accepting a bearer token

    You can use either the opaque token or the JWT for Ave API endpoints. The opaque token is simpler when you do not need to inspect claims client-side.
  </Tab>
</Tabs>

<Note>
  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.
</Note>

## Claim reference

<AccordionGroup>
  <Accordion title="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 was granted)                     |
  </Accordion>
</AccordionGroup>

## Full flow

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

<Tabs>
  <Tab title="SDK (recommended)">
    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.

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

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

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

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

    if (tokens) {
      // tokens.id_token, tokens.access_token, tokens.refresh_token, etc.
      // Token signatures and claims are already verified — create your session.
    }
    ```

    <Note>
      `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.
    </Note>
  </Tab>

  <Tab title="Embed">
    Use `@ave-id/embed` to show Ave login in a sheet or popup without leaving the page. Pass `onTokens` and PKCE, state, and code exchange are all handled automatically.

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

    // Show as a bottom sheet — openAvePopup works the same way
    const { close } = await openAveSheet({
      clientId: "YOUR_CLIENT_ID",
      redirectUri: "https://yourapp.com/callback",
      scope: "openid profile email offline_access",
      onTokens: (tokens) => {
        // tokens.id_token, tokens.access_token, tokens.refresh_token, etc.
        // Token signatures and claims are already verified — create your session.
      },
      onError: ({ error, message }) => {
        console.error(error, message);
      },
    });
    ```

    For an inline embed on a dedicated auth page:

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

    const { destroy } = await mountAveEmbed({
      container: document.getElementById("auth-container"),
      clientId: "YOUR_CLIENT_ID",
      redirectUri: "https://yourapp.com/callback",
      onTokens: (tokens) => {
        destroy();
        // Create your session
      },
      onError: ({ error }) => console.error(error),
    });
    ```

    <Note>
      `onTokens` requires no PKCE setup — the embed handles it. For full cryptographic validation of the returned `id_token`, pass it to `verifyJwt` from `@ave-id/sdk` after receiving tokens.
    </Note>
  </Tab>

  <Tab title="Manual">
    Use the individual helpers when you need full control — server-side token exchange, custom storage, or non-browser environments.

    <Steps>
      <Step title="Create request context">
        Generate `state`, `nonce`, and PKCE parameters before redirecting. Store them in `sessionStorage` (browser) or your server session.

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

        const codeVerifier = generateCodeVerifier();
        const codeChallenge = await generateCodeChallenge(codeVerifier);
        const state = generateNonce();
        const nonce = generateNonce();

        sessionStorage.setItem("ave_verifier", codeVerifier);
        sessionStorage.setItem("ave_state", state);
        sessionStorage.setItem("ave_nonce", nonce);
        ```
      </Step>

      <Step title="Redirect to Ave">
        Build the authorization URL and redirect the user.

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

        const url = buildAuthorizeUrl(
          { clientId: "YOUR_CLIENT_ID", redirectUri: "https://yourapp.com/callback" },
          {
            scope: ["openid", "profile", "email", "offline_access"],
            codeChallenge,
            codeChallengeMethod: "S256",
            state,
            nonce,
          }
        );

        window.location.href = url;
        ```
      </Step>

      <Step title="Handle callback securely">
        Ave redirects to your `redirect_uri` with `?code=...&state=...`. Verify `state` before touching anything else.

        ```ts theme={null}
        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_verifier");
        const savedNonce = sessionStorage.getItem("ave_nonce");

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

        // Clear immediately — single use
        sessionStorage.removeItem("ave_state");
        sessionStorage.removeItem("ave_verifier");
        sessionStorage.removeItem("ave_nonce");
        ```
      </Step>

      <Step title="Exchange authorization code">
        ```ts theme={null}
        import { exchangeCode } from "@ave-id/sdk";

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

        The response includes `access_token`, `access_token_jwt`, `id_token`, `refresh_token`, `expires_in`, `scope`, and `user`.
      </Step>

      <Step title="Validate id_token and create session">
        Validate the `id_token` before trusting it. `verifyJwt` handles JWKS fetching, RS256 signature verification, and all required claim checks.

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

        const claims = await verifyJwt(tokens.id_token, {
          audience: "YOUR_CLIENT_ID",
          nonce: savedNonce,
        });

        if (!claims) {
          throw new Error("Invalid id_token — signature or claims validation failed.");
        }

        // claims.sub    — identity UUID (use as your primary key)
        // claims.email  — verified email (requires email scope)
        // claims.name   — display name (requires profile scope)
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

## 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.

```ts theme={null}
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);
```

For public PKCE clients, the rotating refresh token is the credential for this grant and remains bound to the registered `clientId`. This works for browser, desktop custom-protocol, and native application origins without a client secret.

<Warning>
  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.
</Warning>

## 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`.

```bash theme={null}
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

<AccordionGroup>
  <Accordion title="OIDC discovery document">
    Fetch the discovery document to programmatically get endpoint URLs and signing key locations:

    ```bash theme={null}
    GET https://aveid.net/.well-known/openid-configuration
    ```

    ```json theme={null}
    {
      "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"]
    }
    ```
  </Accordion>

  <Accordion title="Manual ID token validation sequence">
    For non-JS environments or any library that validates tokens without the SDK, follow this order:

    <Steps>
      <Step title="Fetch JWKS">
        `GET https://aveid.net/.well-known/jwks.json`. Cache the response and re-fetch only when you encounter an unknown `kid`.
      </Step>

      <Step title="Verify signature">
        Use the key matching the `kid` in the token header. Ave signs with RS256.
      </Step>

      <Step title="Check timestamps">
        Verify `exp > now` and `iat <= now`. Use a small clock tolerance (60 seconds) to handle drift.
      </Step>

      <Step title="Verify issuer and audience">
        `iss` must equal `https://aveid.net`. `aud` must equal your client ID. Reject anything else.
      </Step>

      <Step title="Verify nonce">
        If you sent a `nonce` in the authorization request, the `id_token` must contain the same value. This prevents replay attacks.
      </Step>
    </Steps>
  </Accordion>
</AccordionGroup>

## Edge cases

<AccordionGroup>
  <Accordion title="Authorization code expires in seconds">
    Don't store the code and exchange later. Exchange immediately after validating `state` on the callback page.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="Missing id_token">
    The `openid` scope was not requested or was not granted. Add `openid` to your scope list.
  </Accordion>

  <Accordion title="Missing refresh_token">
    The `offline_access` scope was not requested or was not granted. Add `offline_access` to your scope list.
  </Accordion>

  <Accordion title="uid claim missing from access_token_jwt">
    The `user_id` scope was not included in the granted scopes. The `uid` claim and `user_id` token response field only appear when `user_id` is in the authorized scope list.
  </Accordion>
</AccordionGroup>
