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

# JWT Scopes & Access Lists

> Read access list / gate scopes from the user's JWT to gate parts of your app.

When you configure [Access Lists](https://docs.dynamic.xyz/docs/auth/access-control/access-lists.md) or [Gates](https://docs.dynamic.xyz/docs/auth/access-control/gates.md) (NFT/token gating) in the Dynamic Console, Dynamic encodes the user's permissions as **scopes** on the session JWT. Reading those scopes lets you conditionally render or block parts of your app.

> [!NOTE]
>
> These session scopes are different from the `TokenScope` enum used for **elevated access tokens** (e.g. `wallet:export`). Elevated scopes are short-lived and granted via step-up auth — see [Step-Up Authentication](https://docs.dynamic.xyz/docs/javascript/authentication-methods/step-up-auth/overview.md).

## How it works

Dynamic emits scopes as a standard JWT `scope` claim — a single whitespace-separated string per [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693#section-4.2). To check them client-side:

1. Read the JWT from `client.token`.
2. Decode the payload (a Base64URL JSON blob).
3. Split the `scope` claim into a string array.

The SDK doesn't export a hook for this in vanilla JavaScript — it's a small utility you can drop in:

```typescript theme={"system"}
import { getDefaultClient } from '@dynamic-labs-sdk/client';

type JwtPayload = {
  exp: number;
  iat: number;
  scope?: string;
  sub: string;
  [key: string]: unknown;
};

const decodeJwt = (jwt: string): JwtPayload => {
  const [, payload] = jwt.split('.');
  const base64 = payload.replaceAll('-', '+').replaceAll('_', '/');
  const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
  return JSON.parse(atob(padded)) as JwtPayload;
};

export const getUserScopes = (client = getDefaultClient()): string[] => {
  if (!client.token) return [];
  return decodeJwt(client.token).scope?.split(/\s+/).filter(Boolean) ?? [];
};
```

## Checking scopes

Wrap the helper above in a `userHasScopes` function with `AND` / `OR` semantics matching the React hook:

```typescript theme={"system"}
type LogicOperator = 'AND' | 'OR';

export const userHasScopes = (
  required: string | string[],
  logicOperator: LogicOperator = 'OR'
): boolean => {
  const userScopes = getUserScopes();
  const requiredList = Array.isArray(required) ? required : [required];

  return logicOperator === 'AND'
    ? requiredList.every((scope) => userScopes.includes(scope))
    : requiredList.some((scope) => userScopes.includes(scope));
};
```

## Usage

> [tab] JavaScript
>
> ```javascript theme={"system"}
> import { userHasScopes } from './scopes';
>
> if (userHasScopes('signing')) {
>   // user can sign
> }
>
> if (userHasScopes(['signing', 'creating'], 'AND')) {
>   // user can sign AND create
> }
>
> if (userHasScopes(['signing', 'creating'], 'OR')) {
>   // user can sign OR create
> }
> ```

> [tab] React
>
> Re-evaluate when the JWT changes by depending on the token from `useUser` (or wrap in `useOnEvent` for `tokenChanged`).
>
> ```tsx theme={"system"}
> import { useUser } from '@dynamic-labs-sdk/react-hooks';
> import { userHasScopes, getUserScopes } from './scopes';
>
> function GatedFeature() {
>   const { data: user } = useUser();
>   const canSign = userHasScopes('signing');
>
>   if (!user) return <p>Please sign in</p>;
>   return canSign ? <SigningWidget /> : <p>You don't have signing access</p>;
> }
> ```
>
> Want a custom hook? Subscribe to `tokenChanged` once and memoize:
>
> ```tsx theme={"system"}
> import { useOnEvent } from '@dynamic-labs-sdk/react-hooks';
> import { useState } from 'react';
> import { getUserScopes } from './scopes';
>
> export function useUserScopes() {
>   const [scopes, setScopes] = useState<string[]>(() => getUserScopes());
>
>   useOnEvent({
>     event: 'tokenChanged',
>     listener: () => setScopes(getUserScopes()),
>   });
>
>   return scopes;
> }
> ```

## Server-side validation

**Always validate scopes on the server when gating sensitive resources.** A determined client can hide UI but cannot fake a JWT signature — verify the JWT against your project's JWKS in your backend, then check the `scope` claim there too. See [Cookie Authentication](https://docs.dynamic.xyz/docs/javascript/authentication-methods/cookie-authentication.md) for a server-aware setup.

## Limitations

* **No mixing of `AND` / `OR` in one call.** The helper above mirrors the React hook — for nested predicates, compose two calls.
* **Cookie-based sessions hide the token.** In cookie-auth mode, `client.token` is `null` because the token lives in an HTTP-only cookie. Read scopes server-side and pass them back to the client through your own API.
* Access list / gate evaluation runs at sign-in. If you change a list while the user is signed in, the scopes update only on the next [`refreshUser`](https://docs.dynamic.xyz/docs/javascript/authentication-methods/refresh-user) or `refreshAuth`.

## Related

* [Access Lists overview](https://docs.dynamic.xyz/docs/auth/access-control/access-lists.md)
* [Gates (NFT/token gating)](https://docs.dynamic.xyz/docs/auth/access-control/gates.md)
* [Step-Up Authentication](https://docs.dynamic.xyz/docs/javascript/authentication-methods/step-up-auth/overview.md)
* [Refresh Auth](https://docs.dynamic.xyz/docs/javascript/authentication-methods/refresh-auth.md)
* [Subscribing to client events](https://docs.dynamic.xyz/docs/javascript/reference/client/on-event.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.