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

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

Import and initialize SDK

ReactNextJSNextAuthWebJSVue.jsHTMLAngular
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>
	);
};
import { AuthProvider } from '@descope/nextjs-sdk';

export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
		<AuthProvider projectId="__ProjectID__">
			<html lang="en">
				<body>{children}</body>
			</html>
		</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,
        }
      },
    },
  ], 
  callbacks: {
    async jwt({token, account, profile}) {
      if (account) {
        return {
          ...token,
          access_token: account.access_token,
          expires_at: Math.floor(Date.now() / 1000 + account.expires_in),
          refresh_token: account.refresh_token,
          profile: {
            name: profile?.name,
            email: profile?.email,
            image: profile?.picture,
            },
          }
        } else if (Date.now() < token.expires_at * 1000) {
          return token
        } else {
          try {
            const response = await fetch("https://api.descope.com/oauth2/v1/token", {
              headers: {"Content-Type": "application/x-www-form-urlencoded"},
              body: new URLSearchParams({
                client_id: "__ProjectID__",
                client_secret: "<Descope Access Key>",
                grant_type: "refresh_token",
                refresh_token: token.refresh_token,
              }),
              method: "POST",
            })
            
            const tokens = await response.json()
            
            if (!response.ok) throw tokens
            
            return {
              ...token,
              access_token: tokens.access_token,
              expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
              refresh_token: tokens.refresh_token ?? token.refresh_token,
            }
          } catch (error) {
            console.error("Error refreshing access token", error)
            return {...token, error: "RefreshAccessTokenError"}
            }
          }
        },
    
        async session({session, token}) {
          if (token.profile) {
            session.user = token.profile;
          }
    
          session.error = token.error
          session.accessToken = token.access_token
          return session
        },
    }
}

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>
// app.module.ts
import { NgModule, APP_INITIALIZER} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { DescopeAuthModule, DescopeAuthService, descopeInterceptor } from '@descope/angular-sdk';
import { AppComponent } from './app.component';
import {
	HttpClientModule,
	provideHttpClient,
	withInterceptors
} from '@angular/common/http';
import { zip } from 'rxjs';

export function initializeApp(authService: DescopeAuthService) {
	return () => zip([authService.refreshSession(), authService.refreshUser()]);
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    DescopeAuthModule.forRoot({
      projectId: 'YOUR_PROJECT_ID'
    })
  ],
  providers: [
    {
        provide: APP_INITIALIZER,
        useFactory: initializeApp,
        deps: [DescopeAuthService],
        multi: true
      },
      provideHttpClient(withInterceptors([descopeInterceptor]))
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

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.

ReactNextAuthWebJSHTMLAngular
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>
import { DescopeAuthService } from '@descope/angular-sdk';

private authService = inject(DescopeAuthService);

token = getSessionToken()

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.

Note: If you're using NextAuth and Next.js, you'll need to also make sure that you're handling the logout using the federated IdP revocation endpoint. You can see this working in a sample app here.

ReactNextAuthWebJSHTMLAngular
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();
// Sign out with NextAuth works a bit differently then you may be expecting. The reason is that, by using NextAuth, you're creating a NextAuth session between NextAuth and your client but also an OIDC session between Descope and NextAuth.
// Therefore, you'll need to make sure both sessions are cleared when logging out, which is detailed below.

---------------
// /app/signout/page.tsx

"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { signOut } from "next-auth/react";

function SignOutCallback() {
  const router = useRouter();

  useEffect(() => {
    const performSignOut = async () => {
      await signOut({ redirect: false });
      router.push("/");
    };

    performSignOut();
  }, [router]);

  return <div>Signing out...</div>;
}

---------------
// /app/api/auth/federated-sign-out

import { authOptions } from "@/app/_utils/options";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";

export const dynamic = "force-dynamic";

const handler = async (req: NextRequest, res: NextResponse) => {
  try {
    const session = await getServerSession(authOptions);
    if (!session) {
      return NextResponse.redirect(process.env.NEXTAUTH_URL!);
    }

    const endSessionURL = `https://api.descope.com/oauth2/v1/logout`;
    const redirectURL = `${process.env.NEXTAUTH_URL}/signout`;
    const endSessionParams = new URLSearchParams({
      // @ts-ignore
      id_token_hint: session.idToken,
      post_logout_redirect_uri: redirectURL,
    });
    const fullUrl = `${endSessionURL}?${endSessionParams.toString()}`;
    return NextResponse.redirect(fullUrl);
  } catch (error) {
    console.error(error);
  }
};
export const GET = handler;

---------------
// app/components/signout-button

"use client";

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

function SignOutButton() {
  return (
      <button
        onClick={async () => {
          window.location.href = `/api/auth/federated-sign-out`;
        }}
      >
        Sign Out
      </button>
    </div>
  );
}
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>
import { Component, OnInit } from '@angular/core';
import { DescopeAuthService } from '@descope/angular-sdk';

@Component({
	selector: 'app-home',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
	isAuthenticated: boolean = false;
	userName: string = '';

	constructor(private authService: DescopeAuthService) {}

	ngOnInit() {
		this.authService.session$.subscribe((session) => {
			this.isAuthenticated = session.isAuthenticated;
		});
		this.authService.user$.subscribe((descopeUser) => {
			if (descopeUser.user) {
				this.userName = descopeUser.user.name ?? '';
			}
		});
	}

	logout() {
		this.authService.descopeSdk.logout();
	}
}

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.
ReactNextAuthWebJSHTML
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 { useSession } from "next-auth/react"

const { data: session } = useSession()

if((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.
ReactWebJSHTMLAngular
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>
import { DescopeAuthService } from '@descope/angular-sdk';

private authService = inject(DescopeAuthService);

this.authService.descopeSdk.getJwtRoles(token = getSessionToken(), tenant = '')

Permissions

Permissions granted to a user can also be extracted from the session token using the getJwtPermissions function from Descope's SDKs.
ReactWebJSHTMLAngular
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>
import { DescopeAuthService } from '@descope/angular-sdk';

private authService = inject(DescopeAuthService);

getJwtPermissions(token = getSessionToken(), tenant = '')