Auth Helpers

Introduction

The Auth class is a crucial component of the Descope SDK, handling key user authentication operations. This class is designed to execute essential functions such as fetching user details (me), refreshing a session (refreshSession), and logging out a user (logout). Functions can take an optional refresh token as a parameter.

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 { }

Me

Retrieves information about the currently authenticated user. This method is used when you need to fetch or display user-related data in your application.

This function returns the following details:

  • loginIds: An array of loginIds associated to the user.
  • userId: The user's unique Descope generated userId
  • verifiedEmail: Boolean whether the email address for the user has been verified.
  • verifiedPhone: Boolean whether the phone number for the user has been verified.
  • picture: The base64 encoded image if the user has an image associated to them.
  • roleNames: An array of roles associated to the user.
  • userTenants: An array of tenant names and IDs associated to the user.
  • createTime: The time that the user was created.
  • totp: Boolean wether the user has TOTP login associated with it.
  • saml: Boolean wether the user has SAML login associated with it.
  • oauth: Boolean wether the user has OAuth login associated with it.
ReactWebJSHTML
import { useUser } from '@descope/react-sdk';
import { useCallback } from 'react';

const App = () => {
	// NOTE - `useDescope`, `useSession`, `useUser` should be used inside `AuthProvider` context,
	// and will throw an exception if this requirement is not met
	const { user, isUserLoading } = useUser();

  { ... } // Include `isSessionLoading`, `isUserLoading` and `isAuthenticated` checks here to prevent rendering before the user is loaded

  return (
    <>
      <p>Hello {user.name}</p>
    </>
  );
	};
await descopeSdk.me();
<script>
let descopeSdk = createSdk({projectId: "__ProjectID__"});

await descopeSdk.me();
</script>

Refresh Session

For a case that the browser has a valid refresh token on storage/cookie, the user should get a valid session token (e.i. user should be logged-in). For that purpose, it is common to call the refresh function after sdk initialization. Note: Refresh return a session token, so if the autoRefresh was provided, the sdk will automatically continue to refresh the token
ReactWebJSHTMLAngular
import { useSession, useUser, useDescope } from '@descope/react-sdk';
import { useCallback } from 'react';

const App = () => {
	// NOTE - `useDescope`, `useSession`, `useUser` should be used inside `AuthProvider` context,
	// and will throw an exception if this requirement is not met
	const { isAuthenticated, isSessionLoading } = useSession();
	const { user, isUserLoading } = useUser();

	if (isSessionLoading || isUserLoading) {
		return <p>Loading...</p>;
	}

	if (isAuthenticated) {
		return (
			<>
				<p>Hello {user.name}</p>
			</>
		);
	}

	return <p>You are not logged in</p>;
};

// Note: `useSession` triggers a single request to the Descope backend to attempt to refresh the session. If you don't useSession on your app, the session will not be refreshed automatically. If your app does not require useSession, you can trigger the refresh manually by calling refresh from useDescope hook. Example:

// const { refresh } = useDescope();
// useEffect(() => {
// 	refresh();
// }, [refresh]);
await descopeSdk.refresh();
<script>
let descopeSdk = createSdk({projectId: "__ProjectID__"});

await descopeSdk.refresh();
</script>
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { DescopeAuthModule, DescopeAuthService } from '@descope/angular-sdk';
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
		}
	],
	bootstrap: [AppComponent]
})
export class AppModule {}

Logout

Logs out the currently authenticated user. This method invalidates the user's current JWT tokens and ends their session. This function is typically used when the user chooses to log out of your application.

ReactNextAuthWebJSHTMLNextJSAngular
import { useDescope } from '@descope/react-sdk';
import { useCallback } from 'react';

const App = () => {
	const { logout } = useDescope();

	const handleLogout = useCallback(() => {
		logout();
	}, [logout]);

  { ... } // Include `isSessionLoading`, `isUserLoading` and `isAuthenticated` checks here to render the proper UI

  return (
    <>
      <button onClick={handleLogout}>Logout</button>
    </>
  );
};
// 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>
  );
}
await descopeSdk.logout();
<script>
let descopeSdk = createSdk({projectId: "__ProjectID__"});

await descopeSdk.logout();
</script>
If you're using Next.js
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();
	}
}

Logout All

This will sign the user out of all the devices they are currently signed-in with. Successfully executing this endpoint will invalidate all user's refresh tokens. Response will include all user tokens and fields empty, so client will remove cookies as well.

ReactWebJSHTML
import { useDescope } from '@descope/react-sdk';
import { useCallback } from 'react';

const App = () => {
	const { logoutAll } = useDescope();

	const handleLogout = useCallback(() => {
		logoutAll();
	}, [logoutAll]);

  { ... } // Include `isSessionLoading`, `isUserLoading` and `isAuthenticated` checks here to render the proper UI

  return (
    <>
      <button onClick={handleLogout}>Logout All</button>
    </>
  );
};
await descopeSdk.logoutAll();
<script>
let descopeSdk = createSdk({projectId: "__ProjectID__"});

await descopeSdk.logoutAll();
</script>

React SDK Methods (useDescope Hook)

The useDescope hook exposes the underlying Descope SDK instance, which can be used to call any of the SDK methods directly.

List of a few methods available:

  • refresh
  • logout
  • logoutAll
  • me
  • oauth
  • otp

For a full list, check out the definitions here.

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

const App = () => {
	// NOTE - `useDescope`, `useSession`, `useUser` should be used inside `AuthProvider` context,
	// and will throw an exception if this requirement is not met
  const sdk = useDescope();

  const testHook = () => {
    // Examples of how to access methods via the hook:
    const sdk = useDescope(); sdk.logout();
    const { logout } = useDescope();
  }

  return (
    <>
      <p>Hello</p>
    </>
  );
};

Tenants

Providing tenant will initialize the flow with the tenant associated. This is crucial for B2B(Business To Business) implementation as Descopers need to segregate their customers to tenant specific configuration such as SSO and attribute mapping.

ReactNextJSVue.jsHTMLAngular
import { Descope } from '@descope/react-sdk'
const App = () => {
    return (
        {...}
        <Descope
            flowId="my-flow-id"
            // tenant ID for SSO (SAML) login. 
            //If not provided, Descope will use the domain of available email to choose the tenant
            tenant="tenantId"
        />
    )
}
// Login page, e.g. src/app/sign-in.tsx
import { Descope } from '@descope/nextjs-sdk';
// you can choose flow to run from the following without `flowId` instead
// import { SignInFlow, SignUpFlow, SignUpOrInFlow } from '@descope/nextjs-sdk'

const Page = () => {
	return (
		<Descope
			flowId="sign-up-or-in"
			onSuccess={(e) => console.log('Logged in!')}
			onError={(e) => console.log('Could not logged in!')}
            // redirectAfterError="/error-page"
			redirectAfterSuccess="/"
            // tenant ID for SSO (SAML) login. 
            //If not provided, Descope will use the domain of available email to choose the tenant
            tenant="tenantId"
		/>
	);
};
import { createApp } from 'vue';
import App from './App.vue';
import descope from '@descope/vue-sdk';

const app = createApp(App);
app.use(descope, {
	projectId: 'my-project-id'
	// 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'
    // tenant ID for SSO (SAML) login. 
    //If not provided, Descope will use the domain of available email to choose the tenant
    tenant="tenantId"

});
app.mount('#app');
<script>
let descopeSdk = createSdk({projectId: "<ProjectId>", tenant: "<tenantId>"});

await descopeSdk.me();
</script>
// 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',
      tenant: 'TENANT_ID'
    })
  ],
  providers: [
    {
        provide: APP_INITIALIZER,
        useFactory: initializeApp,
        deps: [DescopeAuthService],
        multi: true
      },
      provideHttpClient(withInterceptors([descopeInterceptor]))
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }