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

# Scopes and claims

> Every available scope, what JWT claims it adds to id_token and access_token_jwt, and how to design your permission model.

Scopes are space-separated strings you pass in the `scope` parameter of your authorization request. They control what information Ave includes in the tokens and what actions your app can perform.

```text theme={null}
scope=openid profile email offline_access
```

<Note>
  Ave rejects unsupported scopes and standard scopes outside your app’s configured allowlist with `invalid_scope`. Dynamic scopes such as `user_id` and encryption scopes still require user consent. Check the returned `scope` string to see what was granted.
</Note>

## Standard scopes

These are available to all OAuth apps without any special configuration.

<ParamField path="openid" type="scope">
  Required for OIDC. Without this scope, no `id_token` is returned — just an access token.

  **Adds to `id_token`:**

  ```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"
  }
  ```

  The `sub` is the Ave **identity UUID**. The permanent user UUID is shared only when the separate `user_id` scope is granted.

  **You almost always want this.** Without `openid`, you cannot use Convex auth, validate identity server-side, or get an `id_token`.
</ParamField>

<ParamField path="profile" type="scope">
  Adds display name, username handle, and avatar URL.

  **Adds to `id_token`:**

  ```json theme={null}
  {
    "name": "Alice Smith",
    "preferred_username": "alice",
    "picture": "https://avatars.aveid.net/..."
  }
  ```

  **Adds to `/userinfo` response:**

  ```json theme={null}
  {
    "sub": "identity-uuid",
    "name": "Alice Smith",
    "preferred_username": "alice",
    "picture": "https://avatars.aveid.net/..."
  }
  ```
</ParamField>

<ParamField path="email" type="scope">
  Adds the user's email address.

  **Adds to `id_token`:**

  ```json theme={null}
  {
    "email": "alice@example.com"
  }
  ```

  **Adds to `/userinfo` response:**

  ```json theme={null}
  {
    "sub": "identity-uuid",
    "email": "alice@example.com"
  }
  ```

  <Note>
    Not all Ave identities have a verified email address. If the user has no email on their identity, this claim is absent even if the scope was granted.
  </Note>
</ParamField>

<ParamField path="offline_access" type="scope">
  Issues a refresh token alongside the access token.

  **Adds to token response:**

  ```json theme={null}
  {
    "refresh_token": "rt_a1b2c3d4e5f6..."
  }
  ```

  Without this scope, you get no `refresh_token`. When the `access_token` expires, the user must log in again.

  Use this for long-lived sessions or background sync scenarios. Store the refresh token in an HTTP-only cookie or your server-side session storage — never in `localStorage`.
</ParamField>

## Extended scopes

These are requested dynamically at sign-in, not configured in the developer portal.

<ParamField path="user_id" type="scope">
  **Discouraged for most apps.** Exposes the permanent user UUID — stable across all identities for the same Ave account. Users see a warning on the consent screen.

  Prefer `sub` (identity ID) unless you need to link multiple identities to one app account.

  **Adds to `access_token_jwt`:**

  ```json theme={null}
  {
    "uid": "user-uuid"
  }
  ```

  **Adds to token response:**

  ```json theme={null}
  {
    "user_id": "user-uuid"
  }
  ```

  <Note>
    `user_id` is **not** in the `id_token`. It is in `access_token_jwt` as `uid` and in the top-level token response as `user_id`.
  </Note>
</ParamField>

## Encryption scopes

Requested dynamically at sign-in — not configured in the developer portal. Request exactly one per authorization.

<ParamField path="e2ee:symmetric" type="scope">
  Provisions a per-user AES-256 app key. Delivered after consent in the redirect fragment as `#app_key=`. Legacy apps with `supportsE2ee` behave like this scope when no `e2ee:*` scope is listed.
</ParamField>

<ParamField path="e2ee:asymmetric" type="scope">
  Provisions a stable ECDH P-256 keypair per user. Public and private keys are returned in the fragment (`#app_public_key=`, `#app_private_key=`). Store the private key in your app; persist the public key in your database. Other users' public keys are available via `/api/encryption/app-lookup`.
</ParamField>

<ParamField path="e2ee:reset" type="scope">
  **Dangerous — key rotation.** Combine with `e2ee:symmetric` or `e2ee:asymmetric` (or rely on the stored mode on re-sign-in). Regenerates per-app encryption keys instead of reusing existing ones. The redirect fragment includes new keys, previous keys as `#app_key_old` / `#app_public_key_old` / `#app_private_key_old`, and `#app_key_reset=true`. Re-sign-in **without** this scope never rotates keys.
</ParamField>

<ParamField path="e2ee:pqc:kyber" type="scope">
  Reserved for post-quantum key exchange. Not yet available.
</ParamField>

<ParamField path="e2ee:pqc:dilithium" type="scope">
  Reserved for post-quantum signatures. Not yet available.
</ParamField>

## Scope-to-token mapping

| Scope            | id\_token claims added                                | access\_token\_jwt claims added | token response fields                               |
| ---------------- | ----------------------------------------------------- | ------------------------------- | --------------------------------------------------- |
| `openid`         | `iss`, `sub`, `aud`, `exp`, `iat`, `auth_time`, `azp` | —                               | enables `id_token`                                  |
| `profile`        | `name`, `preferred_username`, `picture`               | —                               | `user.displayName`, `user.handle`, `user.avatarUrl` |
| `email`          | `email`                                               | —                               | `user.email`                                        |
| `offline_access` | —                                                     | —                               | `refresh_token`                                     |
| `user_id`        | —                                                     | `uid`                           | `user_id`                                           |

The access JWT always carries `iss`, `sub`, `aud`, `exp`, `iat`, `jti`, `scope`, and `cid`. Ave checks its stored authorization when accepting it at Ave APIs.

Authorization-code and FedCM responses include `user.id` for the selected identity. Other `user` fields follow the scopes above; account-owner fields such as `isPrimary` are never shared with apps. Refresh responses do not include a `user` object.

## Organization context claims

Organization claims are not enabled by a separate scope. They appear when the authorization request includes a valid `organization_id` and the selected identity is an active member of that Ave Business organization.

```json theme={null}
{
  "auth_context": "organization",
  "org_id": "org_...",
  "org_name": "Example Co",
  "org_member_id": "orgmem_...",
  "org_role": "admin",
  "org_scopes": ["read", "sign", "approve"],
  "org_signing_authority": true,
  "org_encryption_mode": "standard",
  "org_key_custody": "ave_standard",
  "auth_method": "enterprise_sso",
  "sso_connection_id": "sso_..."
}
```

Apps that use Ave organizations as workspaces should require `auth_context: "organization"` and match `org_id` against the workspace being accessed. See [Business workspaces](/guides/business-workspaces).

## Connector scopes

Connector flows use resource-defined scopes, not OIDC scopes. The target resource defines what scopes it exposes (for example `resource.read`, `resource.write`). When a user approves the connector, they grant specific scopes from the resource's available list.

When you perform the token-exchange grant, `requestedScope` must be a subset of what was granted. Requesting more than granted returns `invalid_scope`.

See [Connector app-to-app](/guides/connector-app-to-app) for details.

## Requesting scopes safely

<Steps>
  <Step title="Start with the minimum">
    Begin with `openid profile` (or just `openid` if you do not need display names). Add scopes when a specific feature requires them.
  </Step>

  <Step title="Check what was actually granted">
    Read the `scope` field in the token response. It contains the actual granted scopes. Requests containing disallowed scopes fail with `invalid_scope`.

    ```ts theme={null}
    const grantedScopes = tokens.scope.split(" ");
    const hasEmail = grantedScopes.includes("email");
    ```
  </Step>

  <Step title="Handle missing claims gracefully">
    Treat any optional claim (`email`, `name`, etc.) as potentially absent. `email` requires both the `email` scope and a verified email on the selected identity. Do not throw or break when a claim is missing.
  </Step>

  <Step title="Review scope changes">
    Adding a new scope changes what users see on the consent screen. Have product and legal review any scope additions.
  </Step>
</Steps>

## Common mistakes

<AccordionGroup>
  <Accordion title="Requesting user_id without user awareness">
    Users see a warning on the Ave consent screen when an app requests `user_id`. Only request it when you genuinely need cross-identity linking.
  </Accordion>

  <Accordion title="Assuming email is always present">
    Email is present only if you requested `email` scope and the identity has a verified email address. If the user has not linked and verified an email yet, Ave blocks the authorization flow until they do. Always treat the claim as optional in your app anyway.
  </Accordion>

  <Accordion title="Re-requesting scopes on refresh">
    Refresh token grants cannot add new scopes. The new tokens will have the same scopes as the original grant. To get additional scopes, run a new authorization code flow with the new scope.
  </Accordion>

  <Accordion title="Confusing sub with user_id">
    `sub` (identity UUID) is not the same as `user_id` (user UUID). A user can have multiple identities, each with a different `sub`. If you store `sub` as a user identifier and a user creates a second identity, they appear as a different user. Decide upfront whether you identify by identity or by user.
  </Accordion>
</AccordionGroup>
