Skip to main content
Docs

Manual JWT verification

Your Clerk-generated session tokens are essentially JWTs which are signed using your instance's private key and can be verified using your instance's public key. Depending on your architecture, these tokens will be in your backend requests either via a cookie named __session or via the Authorization header.

For every request, you must validate the token to ensure it hasn't expired or been tampered with (i.e., it's authentic and secure). If these validations succeed, then the user is authenticated to your application and should be considered signed in. The clerkClient.authenticateRequest() method handles these validations for you. Alternatively, you can manually verify the token without using the SDK. See the following sections for more information.

Use authenticateRequest() to verify a session token

The authenticateRequest() method accepts the request object and authenticates the session token in it.

The following example uses the authenticateRequest() method to verify the session token. It also performs networkless authentication by passing jwtKey. This verifies if the user is signed into the application.

authenticateRequest() requires publishableKey to be set. If you are importing clerkClient from a higher-level SDK, such as Next.js, then clerkClient infers the publishableKey from your environment variables.

import { createClerkClient } from '@clerk/backend'

// Initialize clerkClient
const clerkClient = createClerkClient({
  secretKey: process.env.CLERK_SECRET_KEY,
  publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
})

export async function GET(req: Request) {
  // Use the `authenticateRequest()` method to verify the token
  const { isAuthenticated } = await clerkClient.authenticateRequest(req, {
    authorizedParties: ['https://example.com'],
    jwtKey: process.env.CLERK_JWT_KEY,
  })

  // Protect the route from unauthenticated users
  if (!isAuthenticated) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Add logic to perform protected actions

  return Response.json({ message: 'This is a reply' })
}
app/api/example/route.ts
import { clerkClient } from '@clerk/nextjs/server'

export async function GET(req: Request) {
  // Initialize clerkClient
  const client = await clerkClient()

  // Use the `authenticateRequest()` method to verify the token
  const { isAuthenticated } = await client.authenticateRequest(req, {
    authorizedParties: ['https://example.com'],
    jwtKey: process.env.CLERK_JWT_KEY,
  })

  // Protect the route from unauthenticated users
  if (!isAuthenticated) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Add logic to perform protected actions

  return Response.json({ message: 'This is a reply' })
}
src/api/example.ts
import { clerkClient } from '@clerk/astro/server'
import type { APIRoute } from 'astro'

export const GET: APIRoute = async (context) => {
  // Initialize clerkClient
  // Use the `authenticateRequest()` method to verify the token
  const { isAuthenticated } = await clerkClient(context).authenticateRequest(context.request, {
    authorizedParties: ['https://example.com'],
    jwtKey: process.env.CLERK_JWT_KEY,
  })

  // Protect the route from unauthenticated users
  if (!isAuthenticated) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Add logic to perform protected actions

  return Response.json({ message: 'This is a reply' })
}
index.js
import { clerkClient } from '@clerk/express'
import express from 'express'

const app = express()

app.get('/example', async (req, res) => {
  // Initialize clerkClient
  // Use the `authenticateRequest()` method to verify the token
  const { isAuthenticated } = await clerkClient.authenticateRequest(req, {
    authorizedParties: ['https://example.com'],
    jwtKey: process.env.CLERK_JWT_KEY,
  })

  // Protect the route from unauthenticated users
  if (!isAuthenticated) {
    res.status(401).json({ error: 'User not authenticated' })
  }

  // Add logic to perform protected actions

  return res.json({ message: 'This is a reply' })
})
src/routes/example.ts
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import { clerkClient } from '@clerk/fastify'

export const exampleRoutes = (fastify: FastifyInstance) => {
  fastify.get('/example', async (req: FastifyRequest, res: FastifyReply) => {
    // Initialize clerkClient
    // Use the `authenticateRequest()` method to verify the token
    const { isAuthenticated } = await clerkClient.authenticateRequest(req, {
      authorizedParties: ['https://example.com'],
      jwtKey: process.env.CLERK_JWT_KEY,
    })

    // Protect the route from unauthenticated users
    if (!isAuthenticated) {
      return res.status(401).json({ error: 'User not authenticated' })
    }

    // Add logic to perform protected actions

    return res.json({ message: 'This is a reply' })
  })
}
server/api/example.ts
import { clerkClient } from '@clerk/nuxt/server'

export default defineEventHandler(async (event) => {
  // Initialize clerkClient
  // Use the `authenticateRequest()` method to verify the token
  const { isAuthenticated } = await clerkClient(event).authenticateRequest(event.request, {
    authorizedParties: ['https://example.com'],
    jwtKey: process.env.CLERK_JWT_KEY,
  })

  // Protect the route from unauthenticated users
  if (!isAuthenticated) {
    return createError({ statusCode: 401, statusMessage: 'User not authenticated' })
  }

  // Add logic to perform protected actions

  return { message: 'This is a reply' }
})
app/routes/example.tsx
import { redirect } from 'react-router'
import { clerkClient } from '@clerk/react-router/server'
import type { Route } from './+types/example'

export async function loader(args: Route.LoaderArgs) {
  // Initialize clerkClient
  // Use the `authenticateRequest()` method to verify the token
  const { isAuthenticated } = await clerkClient(args).authenticateRequest(args.request, {
    authorizedParties: ['https://example.com'],
    jwtKey: process.env.CLERK_JWT_KEY,
  })

  // Protect the route from unauthenticated users
  if (!isAuthenticated) {
    return redirect('/sign-in?redirect_url=' + args.request.url)
  }

  // Add logic to perform protected actions

  return { message: 'This is a reply' }
}
app/routes/api/example.tsx
import { createFileRoute } from '@tanstack/react-router'
import { clerkClient } from '@clerk/tanstack-react-start/server'

export const ServerRoute = createFileRoute('/api/example')({
  server: {
    handlers: {
      GET: async ({ request }) => {
        // Initialize clerkClient
        // Use the `authenticateRequest()` method to verify the token
        const { isAuthenticated } = await clerkClient().authenticateRequest(request, {
          authorizedParties: ['https://example.com'],
          jwtKey: process.env.CLERK_JWT_KEY,
        })

        // Protect the route from unauthenticated users
        if (!isAuthenticated) {
          return Response.json({ error: 'Unauthorized' }, { status: 401 })
        }

        // Add logic to perform protected actions

        return Response.json({ message: 'This is a reply' })
      },
    },
  },
})

Retrieve the session token

Retrieve the session token from either __session cookie for a same-origin request or from the Authorization header for cross-origin requests.

Get your instance's public key

Use one of the three ways to obtain your public key:

  1. Use the Backend API in JSON Web Key Set (JWKS) format at the following endpoint https://api.clerk.com/v1/jwks.
  2. Use your in JWKS format, also known as the . The format is your Frontend API URL with /.well-known/jwks.json appended to it.
  3. Use your JWKS Public Key, which can be found on the API keys page in the Clerk Dashboard.

Verify the token signature

To verify the token signature:

  1. Verify that the token's algorithm has the expected value.
  2. Use your instance's public key to verify the token's signature.
  3. Validate that the token isn't expired by checking the exp (expiration time) and nbf (not before) claims.
  4. Validate that the azp (authorized parties) claim equals any of your known origins permitted to generate those tokens. For better security, it's highly recommended to explicitly set the authorizedParties option when authorizing requests. The value should be a list of domains allowed to make requests to your application. Not setting this value can open your application to CSRF attacks. For example, if you're permitting tokens retrieved from http://localhost:3000, then the azp claim should equal http://localhost:3000. You can also pass an array of strings, such as ['http://localhost:4003', 'https://clerk.dev']. If the azp claim doesn't exist, you can skip this step.

Optional: Check for a sts claim

If you are using Clerk's Organizations feature and , users are required to be part of an Organization before accessing your application. If the user has completed registration, but is not yet part of an Organization, a valid session token will be created, but the token will contain a sts (status) claim set to pending. You may want to reject requests to your backend with pending statuses to ensure that users are not able to work around the Organization requirement.

Finished

If the above process succeeds, the user is considered signed in to your application and authenticated. You can also retrieve the session ID and user ID from of the token's claims.

Example

The following example manually verifies a session token.

import Cookies from 'cookies'
import jwt from 'jsonwebtoken'

export default async function (req: Request, res: Response) {
  // Your public key should be set as an environment variable
  const publicKey = process.env.CLERK_PEM_PUBLIC_KEY
  // Retrieve session token from either `__session` cookie for a same-origin request
  // or from the `Authorization` header for cross-origin requests
  const cookies = new Cookies(req, res)
  const tokenSameOrigin = cookies.get('__session')
  const tokenCrossOrigin = req.headers.authorization

  if (!tokenSameOrigin && !tokenCrossOrigin) {
    res.status(401).json({ error: 'Not signed in' })
    return
  }

  try {
    let decoded
    const options = { algorithms: ['RS256'] } // The algorithm used to sign the token.
    const permittedOrigins = ['http://localhost:3000', 'https://example.com'] // Replace with your permitted origins

    if (tokenSameOrigin) {
      decoded = jwt.verify(tokenSameOrigin, publicKey, options)
    } else {
      decoded = jwt.verify(tokenCrossOrigin, publicKey, options)
    }

    // Validate the token's expiration (exp) and not before (nbf) claims
    const currentTime = Math.floor(Date.now() / 1000)
    if (decoded.exp < currentTime || decoded.nbf > currentTime) {
      throw new Error('Token is expired or not yet valid')
    }

    // Validate the token's authorized party (azp) claim
    if (decoded.azp && !permittedOrigins.includes(decoded.azp)) {
      throw new Error("Invalid 'azp' claim")
    }

    res.status(200).json({ sessionToken: decoded })
  } catch (error) {
    res.status(400).json({
      error: error.message,
    })
  }
}

Feedback

What did you think of this content?

Last updated on