Authenticator Apps (TOTP) via Client SDKs

This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods.

If you'd like to use Descope Flows, Quick Start should be your starting point.

Descope supports validating sign-up and sign-ins via Authenticator Applications which provide a Time-based One-time Password (TOTP). Google Authenticator, Microsoft Authenticator, and Authy are examples of authenticator apps. Descope generates the required QR code or key (also called a secret or seed) in order to configure new a new Authenticator.

Client SDK

Install SDK

Terminal
npm i --save @descope/react-sdk
Terminal
npm i --save @descope/nextjs-sdk
Terminal
npm i --save @descope/web-js-sdk
Terminal
npm i --save @descope/vue-sdk
Terminal
npm i --save @descope/angular-sdk

Import and initialize SDK

For more information about the baseUrl, baseStaticUrl, and baseCdnUrl parameters, refer to the Base URL Configuration section.

Parameters:

  • baseUrl: Custom domain that must be configured to manage token response in cookies. This makes sure every request to our service is through your custom domain, preventing accidental domain blockages.
  • baseStaticUrl: Custom domain to override the base URL that is used to fetch static files.
  • baseCdnUrl: Custom domain to override the base URL used to load external script assets (e.g., SDKs or widgets) dynamically at runtime.
  • persistTokens: Controls whether session tokens are stored in browser localStorage. Enabled by default and accessible via getToken(). Set to false to avoid client-side storage of tokens to reduce XSS risk.
  • autoRefresh: Controls whether the session is automatically refreshed when the token is expired. Enabled by default. Set to false to disable automatic refresh of the session.
  • sessionTokenViaCookie: Controls whether the session token is stored in a cookie instead of localStorage. If persistTokens is true, then by default, the token is stored in localStorage. Set this to true to store the token in a JS cookie instead.
  • storeLastAuthenticatedUser: Determines if the last authenticated user's info is saved in localStorage. Enabled by default and accessible via getUser(). Set to false to disable this behavior.
  • keepLastAuthenticatedUserAfterLogout: Controls whether user info is kept after logout. Disabled by default. Set to true to store user data on logout.
import { AuthProvider } from '@descope/react-sdk'
import { Descope, useDescope } from '@descope/react-sdk'

const AppRoot = () => {
	return (
      <AuthProvider
          projectId="__ProjectID__"
          baseUrl="https://auth.app.example.com"
          baseCdnUrl="https://assets.app.example.com" // specify a custom CDN URL for fetching external scripts and resources
          persistTokens={true} // set to `false` to disable token storage in browser to prevent XSS
          autoRefresh={true} // set to `false` to disable automatic refresh of the session
          sessionTokenViaCookie={false} // set to `true` to store the session token in a JS cookie instead of localStorage
          storeLastAuthenticatedUser={true} // set to `false` to disable storing last user
          keepLastAuthenticatedUserAfterLogout={false} // set to `true` to persist user info after logout
        >
        <App />
      </AuthProvider>
	);
};
import { AuthProvider } from '@descope/nextjs-sdk';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <AuthProvider
      projectId="__ProjectID__"
      baseUrl="<URL>"
      baseCdnUrl="https://assets.app.example.com" // specify a custom CDN URL for fetching external scripts and resources
      persistTokens={true} // set to `false` to disable token storage in browser to prevent XSS
      autoRefresh={true} // set to `false` to disable automatic refresh of the session
      sessionTokenViaCookie={false} // set to `true` to store the session token in a JS cookie instead of localStorage
      storeLastAuthenticatedUser={true} // set to `false` to disable storing last user
      keepLastAuthenticatedUserAfterLogout={false} // set to `true` to persist user info after logout
    >
      <html lang="en">
        <body>{children}</body>
      </html>
    </AuthProvider>
  );
}
import DescopeSdk from '@descope/web-js-sdk';
try {
  const descopeSdk = DescopeSdk({
    projectId: '__ProjectID__',
    baseUrl: 'https://auth.app.example.com',
    baseCdnUrl="https://assets.app.example.com", // specify a custom CDN URL for fetching external scripts and resources
    persistTokens: true, // set to `false` to disable token storage in browser to prevent XSS
    autoRefresh: true, // set to `false` to disable automatic refresh of the session
    sessionTokenViaCookie: false, // set to `true` to store the session token in a JS cookie instead of localStorage
    storeLastAuthenticatedUser: true, // set to `false` to disable storing last user
    keepLastAuthenticatedUserAfterLogout: false, // set to `true` to persist user info after logout
  });
} 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: '__ProjectID__',
  baseUrl: "<base url>",
  baseCdnUrl: "https://assets.app.example.com", // specify a custom CDN URL for fetching external scripts and resources
  persistTokens: true, // set to `false` to disable token storage in browser to prevent XSS
  autoRefresh: true, // set to `false` to disable automatic refresh of the session
  sessionTokenViaCookie: false, // set to `true` to store the session token in a JS cookie instead of localStorage
  storeLastAuthenticatedUser: true, // set to `false` to disable storing last user
  keepLastAuthenticatedUserAfterLogout: false, // set to `true` to persist user info after logout
});

const sdk = getSdk();
sdk?.onSessionTokenChange((newSession) => {
  // here you can implement custom logic when the session is changing
});
// 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',
      baseUrl: '<URL>',
      baseCdnUrl: "https://assets.app.example.com", // specify a custom CDN URL for fetching external scripts and resources
      persistTokens: true, // set to `false` to disable token storage in browser to prevent XSS
      autoRefresh: true, // set to `false` to disable automatic refresh of the session
      sessionTokenViaCookie: false, // set to `true` to store the session token in a JS cookie instead of localStorage
      storeLastAuthenticatedUser: true, // set to `false` to disable storing last user
      keepLastAuthenticatedUserAfterLogout: false // set to `true` to persist user info after logout
    })
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [DescopeAuthService],
      multi: true
    },
    provideHttpClient(withInterceptors([descopeInterceptor]))
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

OIDC Configuration

If you're using our SDK as an OIDC client with our Federated Apps, you can initialize the oidcConfig parameter with the following items:

  • applicationId: This is the application id, that can be found within the settings of your Federated Application
  • redirectUri: This is the url that will be redirected to if the user is unauthenticated. The default redirect URI will be used if not provided.
  • scope: This is a string of the scopes that the OIDC client will request from Descope. This should be one string value with spaces in between each scope. The default scopes are: 'openid email roles descope.custom_claims offline_access'

User Sign-Up

The first step for implementing TOTP authentication is sign-up. In this step the user registers their TOTP app with the authentication service. Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's authenticator app so that TOTP codes can be successfully verified. The new end user will be registered after the full TOTP sign-up flow has been successfully completed.

// Args:
//    user: Optional user object to populate new user information.
const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"}
//    loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery
const loginId = "email@company.com"

const descopeSdk = useDescope();
const resp = await descopeSdk.totp.signUp(loginId, user);
if (!resp.ok) {
  console.log("Failed to initialize TOTP signup")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized TOTP signup.")
  console.log(resp.data)
}
// Args:
//    user: Optional user object to populate new user information.
const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"}
//    loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery
const loginId = "email@company.com"

const resp = await descopeSdk.totp.signUp(loginId, user);
if (!resp.ok) {
  console.log("Failed to initialize TOTP signup")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized TOTP signup.")
  console.log(resp.data)
}
<script>
  let descopeSdk = Descope({projectId: '__ProjectID__', persistTokens: true, autoRefresh: true });

  async function totpSignUp() {
    // Args:
    //    user: Optional user object to populate new user information.
    const user = { "name": document.getElementById("name").value, "phone": document.getElementById("phone").value, "email": document.getElementById("email").value}
    //    loginId: email or phone - becomes the loginId for the user from here on and also used for delivery
    const loginId = document.getElementById("email").value

    const resp = await descopeSdk.totp.signUp(loginId, user);
    if (!resp.ok) {
      window.alert("Failed to initialize signup flow\nStatus Code: " + resp.code
        + "\nError Code: " + resp.error.errorCode + "\nError Description: " + resp.error.errorDescription + "\nError Message: " + resp.error.errorMessage)
    }
    else {
      console.log("Successfully initialized signup flow")
      window.location.replace("./totpDisplay?image=" + encodeURIComponent(resp.data.image) + "&key=" + encodeURIComponent(resp.data.key) + "&provisioningURL=" + encodeURIComponent(resp.data.provisioningURL) + "&loginId=" + encodeURIComponent(loginId))
    }
  }
</script>

User Sign-In / Verify

For signing in, your application client must prompt the user for loginId, such as email or phone, and the code from the authenticator application. Your client will then call the verify function. Upon successful verification, the user will be logged in and the response will include the JWT information.

// Args:
//    loginId: email or phone - must be same as provided at the time of signup.
const loginId = "email@company.com"
//     code: code entered by the user from the authenticator application.
const code = "xxxx"
//    loginOptions (LoginOptions): this allows you to configure behavior during the authentication process.
const loginOptions = {
      "stepup": false,
      "mfa": false,
      "customClaims": {"claim": "Value1"},
      "templateOptions": {"option": "Value1"}
    }
//    refreshToken (optional): the user's current refresh token in the event of stepup/mfa

const descopeSdk = useDescope();
const resp = await descopeSdk.totp.verify(loginId, code, loginOptions);
if (!resp.ok) {
  console.log("Failed to Sign-In via TOTP")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully signed in via TOTP. " + JSON.stringify(resp.data))
}
// Args:
//    loginId: email or phone - must be same as provided at the time of signup.
const loginId = "email@company.com"
//     code: code entered by the user from the authenticator application.
const code = "xxxx"
//    loginOptions (LoginOptions): this allows you to configure behavior during the authentication process.
const loginOptions = {
      "stepup": false,
      "mfa": false,
      "customClaims": {"claim": "Value1"},
      "templateOptions": {"option": "Value1"}
    }
//    refreshToken (optional): the user's current refresh token in the event of stepup/mfa

const resp = await descopeSdk.totp.verify(loginId, code, loginOptions);
if (!resp.ok) {
  console.log("Failed to Sign-In via TOTP")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully signed in via TOTP. " + JSON.stringify(resp.data))
}
<script>
  let descopeSdk = Descope({projectId: '__ProjectID__', persistTokens: true, autoRefresh: true });

  async function totpVerify(thisLoginId) {
    // Args:
    //   loginId (str): The loginId of the user being validated
    const loginId = thisLoginId
    //   code (str): The authorization code enter by the end user during signup/signin
    const code = document.getElementById("totpCode").value
    //    loginOptions (LoginOptions): this allows you to configure behavior during the authentication process.
    const loginOptions = {
          "stepup": false,
          "mfa": false,
          "customClaims": {"claim": "Value1"},
          "templateOptions": {"option": "Value1"}
        }
    //    refreshToken (optional): the user's current refresh token in the event of stepup/mfa

    const resp = await descopeSdk.totp.verify(loginId, code, loginOptions);
    if (!resp.ok) {
      window.alert("Failed to verify TOTP code\nStatus Code: " + resp.code
        + "\nError Code: " + resp.error.errorCode + "\nError Description: " + resp.error.errorDescription + "\nError Message: " + resp.error.errorMessage)
    }
    else {
      console.log("Successfully verified TOTP ")
      window.location.replace("../loggedIn.html?userId=" + encodeURIComponent(loginId) + "&sessionJwt=" + resp.data.sessionJwt)
    }
  }
</script>

Update User

The update user call is used when you would like to associate a new authenticator method with an existing and authenticated user. You need to pass the refresh token or http request of an authenticated user. The update will work only if the user is authenticated.

// Args:
//    loginId: email, phone or username of the authenticated user
const loginId = "email@company.com"
//    refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query.
const refreshToken = "xxxxxxxx"

const descopeSdk = useDescope();
const resp = await descopeSdk.totp.update(loginId, refreshToken)
if (!resp.ok) {
  console.log("Failed to initialized updating user's TOTP")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized updating user's TOTP: " + resp.data)
}
// Args:
//    loginId: email, phone or username of the authenticated user
const loginId = "email@company.com"
//    refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query.
const refreshToken = "xxxxxxxx"

const resp = await descopeSdk.totp.update(loginId, refreshToken)
if (!resp.ok) {
  console.log("Failed to initialized updating user's TOTP")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized updating user's TOTP: " + resp.data)
}
<script>
  let descopeSdk = Descope({projectId: '__ProjectID__', persistTokens: true, autoRefresh: true });

  async function updateTOTP() {
    // Args:
    //    loginId: email, phone or username of the authenticated user
    const loginId = document.getElementById("loginId").value
    //    refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query.
    const refreshToken = document.getElementById("refreshToken").value

    const resp = await descopeSdk.totp.update(loginId, refreshToken)
    if (!resp.ok) {
      window.alert("Failed to initialize TOTP update\nStatus Code: " + resp.code
        + "\nError Code: " + resp.error.errorCode + "\nError Description: " + resp.error.errorDescription + "\nError Message: " + resp.error.errorMessage)
    }
    else {
      console.log("Successfully initialized TOTP update")
      window.location.replace("./totpDisplay?image=" + encodeURIComponent(resp.data.image) + "&key=" + encodeURIComponent(resp.data.key) + "&provisioningURL=" + encodeURIComponent(resp.data.provisioningURL) + "&loginId=" + encodeURIComponent(loginId))
    }
  }
</script>

Session Validation

The final step of completing the authentication with Descope is to validate the user session. Descope provides rich session management capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation here.

Checkpoint

Your application is now integrated with Descope. Please test with sign-up or sign-in use case.

Need help?
Was this helpful?

On this page