Web Client Session Validation

Frontend validation of sessions often serves as the first line of defense and is commonly implemented in many web applications. This approach checks the authenticity of user sessions directly from the user's browser or client-side application. The main advantages of this approach are its simplicity and the reduction of server load, as validation happens on the client side.

If you're looking to set up backend validation, check out our Backend Validation page. The session management article gives an overview of the session validation in Descope. This article focuses on the client-side validation of sessions.

If you're using Next.js and looking to protect API routes and protect pages, checkout our blog: Add Descope Authentication to a Next.js 13 App Using NextAuth.

Install SDK

ReactNextJSWebJSVue.jsHTML
npm i --save @descope/react-sdk
npm i --save next-auth
npm i --save @descope/web-js-sdk
npm i --save @descope/vue-sdk
<!-- Not applicable for HTML -->

Import and initialize SDK

ReactNextJSWebJSVue.jsHTML
import { AuthProvider } from '@descope/react-sdk'
import { Descope, useDescope } from '@descope/react-sdk'

const AppRoot = () => {
	return (
		<AuthProvider
			projectId="__ProjectID__"
			// If the Descope project manages the token response in cookies,
            // a custom domain must be configured
            // (e.g., https://auth.app.example.com)
			// and should be set as the baseUrl property.
			// baseUrl = "https://auth.app.example.com"
		>
			<App />
		</AuthProvider>
	);
};
// app/api/auth/[...nextauth]
import { NextAuthOptions } from "next-auth"
import NextAuth from "next-auth/next";

export const authOptions: NextAuthOptions = {
  providers: [
    {
      id: "descope",
      name: "Descope",
      type: "oauth",
      wellKnown: `https://api.descope.com/${process.env.DESCOPE_PROJECT_ID}/.well-known/openid-configuration`,
      authorization: { params: { scope: "openid email profile" } },
      idToken: true,
      clientId: process.env.DESCOPE_PROJECT_ID,
      clientSecret: process.env.DESCOPE_ACCESS_KEY,
      checks: ["pkce", "state"],
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
        }
      },
    },
  ]
}

const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

// app/provider.tsx
'use client'

import { SessionProvider } from "next-auth/react"


export default function NextAuthSessionProvider(
    { children }:
    { children: React.ReactNode }
) {
    return (
        <SessionProvider>
            { children }
        </SessionProvider>
    )
}

// app/layout.tsx
import NextAuthSessionProvider from './provider'


export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <NextAuthSessionProvider>
          <div>
            {children}
          </div>
        </NextAuthSessionProvider>
      </body>
    </html>
  )
}
import DescopeSdk from '@descope/web-js-sdk';
try {
    //  baseUrl="<URL>" // you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize CNAME within your Descope project.
    const descopeSdk = DescopeSdk({ projectId: '__ProjectID__', persistTokens: true, autoRefresh: true });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}
import { createApp } from "vue";
import App from "@/App.vue";
import descope, { getSdk } from "@descope/vue-sdk";

const app = createApp(App);
app.use(router);

app.use(descope, {
  projectId: '__Project_ID__',
  baseUrl: "<base url>", // Optional
});

const sdk = getSdk();
sdk?.onSessionTokenChange((newSession) => {
  // here you can implement custom logic when the session is changing
});
<head>
    <script src="https://unpkg.com/@descope/web-js-sdk@x.x.x/dist/index.umd.js"></script>
    <!-- Please replace `x.x.x` with the latest version of the WebJS SDK, which you can get from [here](https://www.npmjs.com/package/@descope/web-js-sdk) -->
</head>

Sending session token to application server

If you are using Client SDK or using Descope Flows, then your application client must send the session token to you application server. The getSessionToken() function gets the sessionToken from local storage via JS which you can then include in your request.

NOTE: NextAuth does not use Descope's Client SDK but does use JWTs for session management.

ReactNextJSWebJSHTML
import { getSessionToken } from '@descope/react-sdk';

const sessionToken = getSessionToken();

// example fetch call with authentication header
fetch('your_application_server_url', {
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer '+ sessionToken,
  }
})
// server components
import { getServerSession } from 'next-auth'
import { authOptions } from '@/app/_utils/options'

const session = await getServerSession(authOptions)

// client components
'use client'

import { useSession } from "next-auth/react"

const { data: session } = useSession()
import descopeSdk from '@descope/web-js-sdk';

const descopeSdk = descopeSdk({projectId: __ProjectID__});

const sessionToken = descopeSdk.getSessionToken();
// example fetch call with authentication header
fetch('your_application_server_url', {
  headers: {
    Accept: 'application/json',
    Authorization: 'Bearer '+ sessionToken,
  }
})
<script>
  const descopeSdk = Descope({projectId: "__ProjectID__"});

  const sessionToken = descopeSdk.getSessionToken();

  // example fetch call with authentication header
  fetch('your_application_server_url', {
    headers: {
      Accept: 'application/json',
      Authorization: 'Bearer '+ sessionToken,
    }
  })
</script>

At any time, your application client should only send the session token to your application and the application server should validate the session token using Descope Backend SDK.

Logout using Client SDK

If you are integrating using the Descope Client SDK, then you must use the Client SDK to logout. If you are using Descope Flows with React SDK, refer to the Quick Start for details. If you are Descope Client SDK without flows, then refer to the sample code below for logout.

ReactNextJSWebJSHTML
import DescopeSdk from '@descope/web-js-sdk';

const descopeSdk = Descope({projectId: "__ProjectID__"});

// Logout from the current session
const resp = await descopeSdk.logout();

// Logout from all the sessions
const resp = await descopeSdk.logoutAll();
// client component
'use client'

import { signOut } from "next-auth/react"

signOut({callbackUrl: "/"})
const descopeSdk = Descope({projectId: "__ProjectID__"});

// Logout from the current session
const resp = await descopeSdk.logout();

// Logout from all the sessions
const resp = await descopeSdk.logoutAll();
<script>
  const descopeSdk = Descope({projectId: "__ProjectID__"});

  // Logout from the current session
  const resp = await descopeSdk.logout();

  // Logout from all the sessions
  const resp = await descopeSdk.logoutAll();

</script>

Checking token expiration

One important step in validating a session token is to ensure that the token has not expired. Descope SDKs allows you to check if the session is expired using the isJwtExpired function.
ReactNextJSWebJSHTML
import { getSessionToken, useDescope } from '@descope/react-sdk'

const sessionToken = descopeSdk.getSessionToken();

const sdk = useDescope()

if(sdk.isJwtExpired(sessionToken)) {
  console.log('Session token has expired.');
} else {
  console.log('Session token is valid.');
}
'use client'

import { getSessionToken, isJwtExpired } from '@descope/react-sdk'
import { useSession } from "next-auth/react"

const { data: session } = useSession()

if(isJwtExpired(session)) {
  console.log('Session token has expired.');
} else {
  console.log('Session token is valid.');
}
const descopeSdk = descopeSdk({projectId: '__ProjectID__'});
const sessionToken = descopeSdk.getSessionToken();

if(isJwtExpired(sessionToken)) {
  console.log('Session token has expired.');
} else {
  console.log('Session token is valid.');
}
<script>
  const descopeSdk = Descope({projectId: "__ProjectID__"});
  const sessionToken = descopeSdk.getSessionToken();

  if(isJwtExpired(sessionToken)) {
    console.log('Session token has expired.');
  } else {
    console.log('Session token is valid.');
  }
</script>

Roles

The role of a user is determined from their session token in your application. You can extract this information using Descope's SDKs, specifically the getJwtRoles function.
ReactWebJSHTML
import { getSessionToken, getJwtRoles } from '@descope/react-sdk'

const sessionToken = descopeSdk.getSessionToken();
const roles = getJwtRoles(sessionToken);

console.log('User roles:', roles);
const descopeSdk = descopeSdk({projectId: '__ProjectID__'});

const sessionToken = descopeSdk.getSessionToken();
const roles = getJwtRoles(sessionToken);

console.log('User roles:', roles);
<script>
  const descopeSdk = Descope({projectId: "__ProjectID__"});

  const sessionToken = descopeSdk.getSessionToken();
  const roles = getJwtRoles(sessionToken);

  console.log('User roles:', roles);
</script>

Permissions

Permissions granted to a user can also be extracted from the session token using the getJwtPermissions function from Descope's SDKs.
ReactWebJSHTML
import { getSessionToken, getJwtPermissions } from '@descope/react-sdk'

const sessionToken = descopeSdk.getSessionToken();
const permissions = getJwtPermissions(sessionToken);

console.log('User permissions:', permissions);
const descopeSdk = descopeSdk({projectId: '__ProjectID__'});

const sessionToken = descopeSdk.getSessionToken();
const permissions = getJwtPermissions(sessionToken);

console.log('User permissions:', permissions);
<script>
const descopeSdk = Descope({projectId: "__ProjectID__"});

const sessionToken = descopeSdk.getSessionToken();
const permissions = getJwtPermissions(sessionToken);

console.log('User permissions:', permissions);
</script>