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

# Server-side confidential clients

> Implement authorization code exchange and token refresh on a trusted server using a client secret.

A **confidential client** is an app whose server can store a `clientSecret` securely — a secret never exposed to browsers, mobile apps, or end users. Server-rendered apps, API backends, and background services are typical confidential clients.

<Note>
  If any part of your app runs in a browser or on a user's device, use [PKCE](/guides/pkce-for-public-clients) instead. Client secrets in browser code are not secret.
</Note>

## Flow overview

<Steps>
  <Step title="Redirect the browser to Ave">
    From your server, construct the authorization URL and redirect the user's browser to `https://aveid.net/signin`. You can still include PKCE parameters — this is actually recommended even for confidential clients, as defense-in-depth.
  </Step>

  <Step title="Receive the callback on your server">
    Your server endpoint at the registered `redirect_uri` receives `?code=...&state=...`. Validate `state` against the value stored in the user's server session before proceeding.
  </Step>

  <Step title="Exchange the code with your client secret">
    ```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 }
    );
    ```

    Or with raw HTTP:

    ```bash theme={null}
    POST https://api.aveid.net/api/oauth/token
    Content-Type: application/json

    {
      "grantType": "authorization_code",
      "code": "AUTH_CODE",
      "redirectUri": "https://yourapp.com/callback",
      "clientId": "YOUR_CLIENT_ID",
      "clientSecret": "YOUR_CLIENT_SECRET"
    }
    ```
  </Step>

  <Step title="Store tokens server-side and issue a session">
    Store `access_token`, `access_token_jwt`, `id_token`, and `refresh_token` in your encrypted server-side session store. Issue your own session cookie to the browser.

    Never send raw Ave tokens to the browser — issue your own short-lived session identifier instead.
  </Step>

  <Step title="Refresh silently when tokens expire">
    ```ts theme={null}
    import { refreshTokenServer } from "@ave-id/sdk/server";

    const newTokens = await refreshTokenServer(
      {
        clientId: process.env.AVE_CLIENT_ID!,
        clientSecret: process.env.AVE_CLIENT_SECRET!,
        redirectUri: "https://yourapp.com/callback",
      },
      { refreshToken: storedRefreshToken }
    );

    // Replace stored refresh token immediately
    await storeRefreshToken(newTokens.refresh_token);
    ```
  </Step>
</Steps>

## Client secret rules

<Check>Store the secret in encrypted runtime config (environment variables, secrets manager)</Check>
<Check>Never ship `clientSecret` to browsers, mobile apps, or frontend code</Check>
<Check>Rotate secrets in the developer portal and your deployment environment at the same time</Check>
<Check>Log token endpoint failures without logging raw token values or secrets</Check>

## Handling refresh token rotation

Ave rotates refresh tokens on every use — the old token is immediately invalidated after a successful refresh. If you store the old token and try to reuse it, you'll get `invalid_grant`.

**Best practice:**

1. Store the new `refresh_token` from every refresh response before discarding the old one
2. Use a database row lock or atomic update to prevent concurrent refreshes from the same token
3. If refresh fails with `invalid_grant`: stop retry loops, clear the session, and force re-authentication

## Recommended session architecture

| Layer                                | What it holds                                                                                |
| ------------------------------------ | -------------------------------------------------------------------------------------------- |
| **Server session store** (encrypted) | `access_token`, `refresh_token`, `id_token`, token expiry                                    |
| **Browser**                          | Short-lived session cookie pointing to server session (HTTP-only, Secure, SameSite=Lax)      |
| **Your API**                         | Issues responses based on server-side session lookups — Ave tokens never cross to the client |

<Tip>
  You can also mint your own short-lived app JWT from the validated Ave `id_token` and send that to the browser. This decouples your session format from Ave's token format and gives you full control over expiry and claims.
</Tip>
