βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β π shadcn/directory/clerk/clerk-docs/guides/sessions/session-tokens β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
When a user is authenticated in your application, Clerk generates a short-lived session token that you can use to authenticate requests to your backend. This token is a JSON Web Token (JWT) that contains information about the user and their session.
Read more about Clerk session tokens and how they work in the guide on how Clerk works.
<Tabs items={["Version 2", "Version 1"]}> <Tab> > [!IMPORTANT] > You are reading about version 2 of Clerk's session token claims. To read about version 1, select the respective tab above.
Every generated token has default claims that cannot be overridden by templates. Clerk's default claims include:
| Claim | Abbreviation expanded | Description | Example |
| - | - | - | - |
| `azp` | authorized party | The `Origin` header that was included in the original Frontend API request made from the user. Most commonly, it will be the URL of the application. This claim could be omitted if, for privacy-related reasons, `Origin` is empty or null. | `https://example.com` |
| `exp` | expiration time | The time after which the token will expire, as a Unix timestamp. Determined using the **Token lifetime** JWT template setting in the [Clerk Dashboard](https://dashboard.clerk.com/~/jwt-templates). See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) for more information. | `1713158400` |
| `fva` | factor verification age | Each item represents the minutes that have passed since the last time a first or second factor, respectively, was verified. | `[7, -1]` which means it has been 7 minutes since the first factor was verified, and there either is not a second factor or the second factor has never been verified. |
| `iat` | issued at | The time at which the token was issued as a Unix timestamp. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6) for more information. | `1713158400` |
| `iss` | issuer | The Frontend API URL of your instance. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) for more information. | `https://clerk.your-site.com` for a production instance, `https://your-site.clerk.accounts.dev` for a development instance |
| `jti` | JWT ID | The unique identifier for the token. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7) for more information. | `1234567890` |
| `nbf` | not before | The time before which the token is considered invalid, as a Unix timestamp. Determined using the **Allowed Clock Skew** JWT template setting in the [Clerk Dashboard](https://dashboard.clerk.com/~/jwt-templates). See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5) for more information. | `1713158400` |
| `sid` | session ID | The ID of the current session. | `sess_123` |
| `sub` | subject | The ID of the current user of the session. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2) for more information. | `user_123` |
| `v` | version | The version number of the session token. | `2` |
| `pla` | plan | The plan that is active. The value is in the format `scope:planslug`, where `scope` can be `u` or `o` representing user or org-level plans, respectively. The `u:` prefix is used if no org is active, and the `o:` prefix appears if an org is active. | `u:free`,`o:pro` |
| `fea` | features | A list of enabled features and their scope. The scope can either be `o` for org-level features if there is an [active org](!active-organization), or `u` for user-level features or if there is no active org. | `o:dashboard,o:impersonation` |
| `sts` | session status | The status of the current session | `pending` |
### Organization claim
The **`o` claim**, or organization claim, is only included if the user is part of an [organization](/docs/guides/organizations/overview) and that organization is [active](!active-organization). Its value is an object that contains the following properties:
| Claim | Abbreviation expanded | Description | Example | |
| - | - | - | - | - |
| `id` | ID | The ID of the organization. | `org_123` | |
| `slg` | slug | The slug of the organization. | `org-slug` | |
| `rol` | role | The role of the user in the organization, without the `org:` prefix. | `admin` | |
| `per` | permissions | The names of the permissions the user has in the organization. | `read,manage` | |
| `fpm` | feature-permission map | feature-permission map | The mapping of features with permissions. [Learn how this value is constructed](#decode-o-fpm-manually). | `3,2` |
> [!WARNING]
> The organization claims above are intentionally designed to be as compact as possible to keep JWT tokens small.
> As a result, they can be difficult to decode manually. We strongly recommend using one of our SDKs that support API version [2025-04-10](/docs/guides/development/upgrading/versioning#2025-04-10) to handle decoding reliably.
#### Decode `o.fpm` manually
For those that need to decode the `o.fpm` claim manually, here is how the claim value is computed.
The `o.fpm` value is a list of comma-separated integers where the position of the integer corresponds to a feature in the same position in the `fea` claim. So, the first feature in the `fea` claim corresponds to the first integer in the `o.fpm` claim, and so on.
Each integer, when converted to binary, represents a bitmask where each bit's position corresponds to a permission in the `o.per` list, where `0` = `not-allowed` and `1` = `allowed`. The first bit position in the bitmask is the rightmost, least significant bit. It corresponds to the first permission in the `o.per` list, which is the leftmost element. This results in a sort of "reverse" mapping, seen in the example below.
#### Example
Consider a user with an active organization has role `admin`. This role has the following feature permissions: `dashboard:read`, `dashboard:manage`, `teams:read`. Therefore, the user's claims are as follows:
- `fea` claim has value `o:dashboard,o:teams`
- `o.per` claim has value `manage,read`
- `o.fpm` claim has value `3,2`
Now, let's manually decode the `o.fpm` claim.
The first integer `3` represents the feature-permission mapping for the `dashboard` feature, since it's the first feature in the `fea` claim. Then, converting `3` to binary, you get `11`, which means both permissions are allowed (`dashboard:read` and `dashboard:manage`).
The second integer `2` represents the feature-permission mapping for the `teams` feature, since it's the second feature in the `fea` claim. Then, converting `2` to binary, you get `10`, which means the _second_ permission is allowed (`teams:read`). This is because **you read bitmask from right to left**, so the first bit, `0`, corresponds to the first permission in the `o.per` list (`manage`) and the second bit, `1`, corresponds to the second permission in the `o.per` list (`read`).
### Actor claim
The **`act` (actor) claim** is only included if the user is [impersonating](/docs/guides/users/impersonation) another user. It's value is an object that contains the following properties:
| Claim | Abbreviation expanded | Description | Example |
| - | - | - | - |
| `iss` | issuer | The referrer of the token. | `https://dashboard.clerk.com` |
| `sid` | session ID | The session ID of the impersonated session. | `sess_456` |
| `sub` | subject | The ID of the impersonator. | `user_456` |
</Tab>
<Tab>
> [!IMPORTANT]
> Version 1 was deprecated on April 14, 2025. To upgrade to version 2, go to the [**Updates**](https://dashboard.clerk.com/~/updates) page in the Clerk Dashboard.
Every generated token has default claims that cannot be overridden by templates. Clerk's default claims include:
| Claim | Abbreviation expanded | Description | Example |
| - | - | - | - |
| `azp` | authorized party | The `Origin` header that was included in the original Frontend API request made from the user. Most commonly, it will be the URL of the application. This claim could be omitted if, for privacy-related reasons, `Origin` is empty or null. | `https://example.com` |
| `exp` | expiration time | The time after which the token will expire, as a Unix timestamp. Determined using the **Token lifetime** JWT template setting in the [Clerk Dashboard](https://dashboard.clerk.com/~/jwt-templates). See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) for more information. | `1713158400` |
| `fva` | factor verification age | Each item represents the minutes that have passed since the last time a first or second factor, respectively, was verified. | `[7, -1]` which means it has been 7 minutes since the first factor was verified, and there either is not a second factor or the second factor has never been verified. |
| `iat` | issued at | The time at which the token was issued as a Unix timestamp. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6) for more information. | `1713158400` |
| `iss` | issuer | The Frontend API URL of your instance. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) for more information. | `https://clerk.your-site.com` for a production instance, `https://your-site.clerk.accounts.dev` for a development instance |
| `nbf` | not before | The time before which the token is considered invalid, as a Unix timestamp. Determined using the **Allowed Clock Skew** JWT template setting in the [Clerk Dashboard](https://dashboard.clerk.com/~/jwt-templates). See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5) for more information. | `1713158400` |
| `sid` | session ID | The ID of the current session. | `sess_123` |
| `sub` | subject | The ID of the current user of the session. See [RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2) for more information. | `user_123` |
The following claims are only included if the user is part of an organization and that organization is [active](!active-organization):
| Claim | Abbreviation expanded | Description | Example |
| - | - | - | - |
| `org_id` | organization ID | The ID of the [active organization](!active-organization) that the user belongs to. | `org_123` |
| `org_permissions` | organization permissions | The permissions of the user in the currently [active organization](!active-organization). System permissions are not included in the session token. | `["org:admin:example_permission", "org:member:example_permission"]` |
| `org_slug` | organization slug | The slug of the currently [active organization](!active-organization) that the user belongs to. | `org-slug` |
| `org_role` | organization role | The role of the user in the currently [active organization](!active-organization). | `org:admin` |
The **`act` (actor) claim** is only included if the user is [impersonating](/docs/guides/users/impersonation) another user. It's value is an object that contains the following properties:
| Claim | Abbreviation expanded | Description | Example |
| - | - | - | - |
| `iss` | issuer | The referrer of the token. | `https://dashboard.clerk.com` |
| `sid` | session ID | The session ID of the impersonated session. | `sess_456` |
| `sub` | subject | The ID of the impersonator. | `user_456` |
</Tab>
</Tabs>
If you would like to add custom claims to your session token, you can customize it.
You can also create custom tokens using a JWT template.
Clerk stores the session token in a cookie, and most browsers limit cookie size to 4KB. Exceeding this limit will cause the cookie to not be set, which will break your app as Clerk depends on cookies to work properly.
A session token with the default session claims won't run into this issue, as this configuration produces a cookie significantly smaller than 4kb. However, this limitation becomes relevant when implementing a custom session token. In this case, it's recommended to move particularly large claims out of the token and fetch these using a separate API call from your backend.
Claims to monitor for size limits:
user.organizationsuser.public_metadatauser.unsafe_metadataorg.public_metadataorg_membership.public_metadataIf you add any of these custom claims in your token, use caution to ensure the stored data doesn't exceed the size limit. It's highly recommended to store the extra data in your own database instead of storing it in metadata in the session token. If this isn't an option, you can move particularly large claims like these out of the token and fetch them using a separate API call from your backend.
[!NOTE] If your application encounters this issue, the Clerk Dashboard will display a warning: "Some users are exceeding cookie size limits". To resolve this, update your custom session token.
It's recommended to keep the total size of custom claims in the session token under 1.2KB. The following example shows how to move particularly large claims out of the session token and fetch them using a separate API call from your backend. The limitations of this approach are that if you make this call to Clerk's Backend API frequently, you risk hitting rate limits and it's also slower than making a database query. We highly recommend storing the extra data in your own database instead of storing it in metadata in the session token.
For example, if you were storing several fields in user.public_metadata, like this:
// user.public_metadata
{
onboardingComplete: true,
birthday: '2000-01-01',
country: 'Canada',
bio: 'This is a bio -- imagine it is 6kb of written info',
}
Instead of storing all of that data in the session token, and possibly exceeding the 1.2KB limit, like this:
// Custom claims in the session token
{
"metadata": "{{user.public_metadata}}"
}
You could store only the necessary data in the session token - for example, just the onboardingComplete field - like this:
// Custom claims in the session token
{
"onboardingComplete": "{{user.public_metadata.onboardingComplete}}"
}
Then, when you need to access the other metadata fields, you can fetch them using a separate API call from your backend. The following example uses the getUser() method to access the current user's Backend User object, which includes the publicMetadata field.
<Tabs items={["Next.js", "Astro", "Express", "React Router", "Remix", "Tanstack React Start"]}> <Tab> <CodeBlockTabs options={["App Router", "Pages Router"]}> ```tsx {{ filename: 'app/api/example/route.ts' }} import { auth, clerkClient } from '@clerk/nextjs/server'
export async function GET() {
// Use `auth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = await auth()
// Protect the route by checking if the user is signed in
if (!isAuthenticated) {
return new NextResponse('Unauthorized', { status: 401 })
}
const client = await clerkClient()
// Use the JS Backend SDK's `getUser()` method to get the Backend User object
const user = await client.users.getUser(userId)
// Return the Backend User object
return NextResponse.json({ user: user }, { status: 200 })
}
```
<Include src="_partials/nextjs/get-auth" />
</CodeBlockTabs>
</Tab>
<Tab>
```tsx {{ filename: 'src/api/example.ts' }}
import { clerkClient } from '@clerk/astro/server'
export async function GET(context) {
// Use `locals.auth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = context.locals.auth()
// Protect the route by checking if the user is signed in
if (!isAuthenticated) {
return new Response('Unauthorized', { status: 401 })
}
// Use the JS Backend SDK's `getUser()` method to get the Backend User object
const user = await clerkClient(context).users.getUser(userId)
// Return the Backend User object
return new Response(JSON.stringify({ user }))
}
```
</Tab>
<Tab>
```js {{ filename: 'index.js' }}
import { createClerkClient, getAuth } from '@clerk/express'
import express from 'express'
const app = express()
const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY })
app.get('/user', async (req, res) => {
// Use `getAuth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = getAuth(req)
// Protect the route by checking if the user is signed in
if (!isAuthenticated) {
res.status(401).json({ error: 'User not authenticated' })
}
// Use the JS Backend SDK's `getUser()` method to get the Backend User object
const user = await clerkClient.users.getUser(userId)
// Return the Backend User object
res.json(user)
})
```
</Tab>
<Tab>
```tsx {{ filename: 'app/routes/profile.tsx' }}
import { redirect } from 'react-router'
import { clerkClient, getAuth } from '@clerk/react-router/server'
import type { Route } from './+types/profile'
export async function loader(args: Route.LoaderArgs) {
// Use `getAuth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = await getAuth(args)
// Protect the route by checking if the user is signed in
if (!isAuthenticated) {
return redirect('/sign-in?redirect_url=' + args.request.url)
}
// Get the Backend User object
const user = await clerkClient(args).users.getUser(userId)
// Return the Backend User object
return {
user: JSON.stringify(user),
}
}
```
</Tab>
<Tab>
<CodeBlockTabs options={["Loader Function", "Action Function"]}>
```tsx {{ filename: 'routes/profile.tsx' }}
import { LoaderFunction, redirect } from '@remix-run/node'
import { getAuth } from '@clerk/remix/ssr.server'
import { createClerkClient } from '@clerk/remix/api.server'
export const loader: LoaderFunction = async (args) => {
// Use `getAuth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = await getAuth(args)
// If there is no userId, then redirect to sign-in route
if (!isAuthenticated) {
return redirect('/sign-in?redirect_url=' + args.request.url)
}
// Use the JS Backend SDK's `getUser()` method to get the Backend User object
const user = await createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY }).users.getUser(
userId,
)
// Return the Backend User object
return { serialisedUser: JSON.stringify(user) }
}
```
```tsx {{ filename: 'routes/profile.tsx' }}
import { ActionFunction, redirect } from '@remix-run/node'
import { getAuth } from '@clerk/remix/ssr.server'
import { createClerkClient } from '@clerk/remix/api.server'
export const action: ActionFunction = async (args) => {
// Use `getAuth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = await getAuth(args)
// If there is no userId, then redirect to sign-in route
if (!isAuthenticated) {
return redirect('/sign-in?redirect_url=' + args.request.url)
}
// Prepare the data for the mutation
const params = { firstName: 'John', lastName: 'Wicker' }
// // Use the JS Backend SDK's `updateUser()` method to update the Backend User object
const updatedUser = await createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
}).users.updateUser(userId, params)
// Return the updated user
return { serialisedUser: JSON.stringify(updatedUser) }
}
```
</CodeBlockTabs>
</Tab>
<Tab>
```tsx {{ filename: 'app/routes/api/example.tsx' }}
import { createClerkClient } from '@clerk/backend'
import { json } from '@tanstack/react-start'
import { createAPIFileRoute } from '@tanstack/react-start/api'
import { getAuth } from '@clerk/tanstack-react-start/server'
export const Route = createAPIFileRoute('/api/example')({
GET: async ({ request, params }) => {
// Use `getAuth()` to access `isAuthenticated` and the user's ID
const { isAuthenticated, userId } = await getAuth(req)
// Protect the route by checking if the user is signed in
if (!isAuthenticated) {
return json({ error: 'Unauthorized' }, { status: 401 })
}
// Instantiate the JS Backend SDK
const clerkClient = createClerkClient({ secretKey: import.meta.env.CLERK_SECRET_KEY })
// Use the JS Backend SDK's `getUser()` method to get the Backend User object
const user = await clerkClient.users.getUser(userId)
// Return the Backend User object
return json({ user })
},
})
```
</Tab>
</Tabs>
If you're using the middleware provided by our Clerk SDKs, validating session tokens is handled automatically in every request. If you're not using the middleware, you can still use the respective helpers provided by the SDKs to validate the tokens.
To learn how to manually verify a session token, refer to the dedicated guide.
β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ