Passkey Authentication with Backend 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.

WebAuthn lets you authenticate end users using the strong authenticators that are now often built right into devices, including biometrics (fingerprint, facial, or iris recognition) and secure hardware keys (for example, Yubico, CryptoTrust, or Thedis). These secure hardware keys, also known as passkeys, can be USB tokens or embedded security features in smartphones or computers. A typical method for implementing WebAuthn has two sets of functionality to program: user onboarding and session validation.

Backend SDK

Install SDK

Terminal
npm i --save @descope/node-sdk

Import and initialize SDK

import DescopeClient from '@descope/node-sdk';
try{
    //  baseUrl="<URL>" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize CNAME within your Descope project.
    const descopeClient = DescopeClient({ projectId: '__ProjectID__' });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}
 
// Note that you can handle async operation failures and capture specific errors to customize errors.
//     An example can be found here: https://github.com/descope/node-sdk?tab=readme-ov-file#error-handling

Start Sign-Up

The first step to start the webauthn signup flow is the start signup process. This function requires a unique loginId which is used as the loginId for the user and the webauthn credentials are associated with this loginId. Another required parameter is origin. The value for this should window.location.origin from your application client. For extra security Descope checks the value against the domain setup for your application in the Descope console. The origin value should be either the same or a subdomain of the domain setting in the console.

// Args:
//    loginId: email or phone - becomes the loginId for the user from here on and also used for delivery
const loginId = "email@company.com"
//    origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain.
const origin = "https://example.com"
//    displayName: Display name to utilize for the user
const displayName = "Joe Person"
//    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 descopeClient.auth.webauthn.signUp.start(loginId, origin, displayName, loginOptions);
if (!resp.ok) {
  console.log("Unable to start webauthn sign-up")
  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 started webauthn sign-up")
  console.log(resp)
}

Finish Sign-Up

Once you have the transactionId after initiating sign-up, you will utilize it paired with the response from successful biometric completion from the browser. These items will be used within the finish function.

// Args:
//   transactionId: The transaction ID returned by the sign_up_start function
const transactionId = "xxxxxx"
//   response: The response returned by successful biometric authorization in the browser
const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
 
const resp = await descopeClient.auth.webauthn.signUp.finish(transactionId, response);
if (!resp.ok) {
  console.log("Unable to finish webauthn sign-up")
  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 finished webauthn sign-up")
  console.log(resp)
}

Start Sign-In

The first step to start the webauthn signin flow is the start signin process. This function requires a unique loginId which is used as the loginId for the user. Another required parameter is origin. The value for this should window.location.origin from your application client. For extra security Descope checks the value against the domain setup for your application in the Descope console. The origin value should be either the same or a subdomain of the domain setting in the console.

// Args:
//    loginId: email or phone - the loginId for the user from here on and also used for delivery
const loginId = "email@company.com"
//    origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain.
const origin = "https://example.com"
 
const resp = await descopeClient.auth.webauthn.signIn.start(loginId, origin);
if (!resp.ok) {
  console.log("Unable to start webauthn sign-in")
  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 started webauthn sign-in")
  console.log(resp)
}

Finish Sign-In

Once you have the transactionId after initiating sign-in, you will utilize it paired with the response from successful biometric completion from the browser. These items will be used within the finish function.

// Args:
//   transactionId: The transaction ID returned by the sign in start function
const transactionId = "xxxxxx"
//   response: The response returned by successful biometric authorization in the browser
const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
 
const resp = await descopeClient.auth.webauthn.signIn.finish(transactionId, response);
if (!resp.ok) {
  console.log("Unable to finish webauthn sign-in")
  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 finished webauthn sign-in")
  console.log(resp)
}

Start Add User Device

The Start Add User Device adds a new biometric signature or a device to an existing user account. You should use this function in scenarios where a user has already authenticated (signup complete) with your service via another method. This function requires a valid [refresh token[(/session-validation) from another authentication method.

// Args:
//    loginId: email or phone - the loginId for the user
const loginId = "email@company.com"
//    origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain.
const origin = "https://example.com"
//    refreshToken: Valid refresh_token for this user from another authentication method. This is required and should be extracted from query.
const refreshToken = "xxxxx"
 
const resp = await descopeClient.auth.webauthn.update.start(loginId, origin, refreshToken);
if (!resp.ok) {
  console.log("Unable to start webauthn update")
  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 started webauthn update")
  console.log(resp)
}

Finish Add User Device

Call Finish Add User Device after the Start Add User Device function always. The finish call requires transaction id and some other information that is returned from the browser.

// Args:
//   transactionId: The transaction ID returned by the sign in start function
const transactionId = "xxxxxx"
//   response: The response returned by successful biometric authorization in the browser
const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
 
const resp = await descopeClient.auth.webauthn.update.finish(transactionId, response);
if (!resp.ok) {
  console.log("Unable to finish webauthn update")
  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 finished webauthn update")
  console.log(resp)
}

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 backend 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