Passkey Authentication with Backend SDKs

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

If you'd like to use Descope Flows, Quick Start should be your starting point. If you'd like to use our Client SDKs, refer to our Client SDK docs.

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
Terminal
pip3 install descope
Terminal
go get github.com/descope/go-sdk
// Include the following in your `pom.xml` (Maven)
<dependency>
    <artifactId>java-sdk</artifactId>
    <groupId>com.descope</groupId>
    <version>sdk-version</version> // Check https://github.com/descope/descope-java/releases for the latest versions
</dependency>
Terminal
gem install descope
Terminal
composer require descope/descope-php
Terminal
dotnet add package descope

Import and initialize SDK

import DescopeClient from '@descope/node-sdk';
try{
    //  baseUrl="<URL>" // When initializing the Descope client, you can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize a custom domain within your Descope project.
    const descopeClient = DescopeClient({ projectId: '__ProjectID__' });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}
from descope import (
    REFRESH_SESSION_TOKEN_NAME,
    SESSION_TOKEN_NAME,
    AuthException,
    DeliveryMethod,
    DescopeClient,
    AssociatedTenant,
    RoleMapping,
    AttributeMapping,
    LoginOptions
)
try:
    # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com  - this is useful when you utilize custom domain within your Descope project."
    descope_client = DescopeClient(project_id='__ProjectID__')
except Exception as error:
    # handle the error
    print ("failed to initialize. Error:")
    print (error)
import "github.com/descope/go-sdk/descope"
import "github.com/descope/go-sdk/descope/client"

// Utilizing the context package allows for the transmission of context capabilities like cancellation
//      signals during the function call. In cases where context is absent, the context.Background()
//      function serves as a viable alternative.
//      Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
import (
	"context"
)

// DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize a custom domain within your Descope project.

descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__"})
if err != nil {
    // handle the error
    log.Println("failed to initialize: " + err.Error())
}
import com.descope.client.Config;
import com.descope.client.DescopeClient;

var descopeClient = new DescopeClient(Config.builder().projectId("__ProjectID__").build());
require 'descope'

@project_id = ENV['__ProjectID__']
@client = Descope::Client.new({ project_id: @project_id})
require 'vendor/autoload.php';
use Descope\SDK\DescopeSDK;
 
$descopeSDK = new DescopeSDK([
    'projectId' => $_ENV['__ProjectID__'],
]);
// appsettings.json

{
  "Descope": {
    "ProjectId": "__ProjectID__",
    "ManagementKey": "DESCOPE_MANAGEMENT_KEY"
  }
}

// Program.cs

using Descope;
using Microsoft.Extensions.Configuration;

// ... In your setup code
var config = new ConfigurationBuilder()
  .AddJsonFile("appsettings.json")
  .Build();

var descopeProjectId = config["Descope:ProjectId"];
var descopeManagementKey = config["Descope:ManagementKey"];

var descopeConfig = new DescopeConfig(projectId: descopeProjectId);
var descopeClient = new DescopeClient(descopeConfig)
{
    ManagementKey = descopeManagementKey,
};

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)
}
# Args:
#    login_id: email or phone - becomes the loginId for the user from here on and also used for delivery
login_id = "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.
origin = "http://example.com"
#    user: Optional user object to populate new user information.
user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"}
#   login_options (LoginOptions): this allows you to configure behavior during the authentication process.
login_options = {
      "stepup": false,
      "mfa": false,
      "custom_claims": {"claim": "Value1"},
      "template_options": {"option": "Value1"}
    }
#   refresh_token (optional): the user's current refresh token in the event of stepup/mfa

try:
  resp = descope_client.webauthn.sign_up_start(login_id=login_id, origin=origin, user=user, login_options=login_options)
  print ("Successfully started webauthn sign-up")
  print(json.dumps(resp, indent=4))
except AuthException as error:
  print ("Unable to start webauthn sign-up")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//    ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//    loginID: email or phone - becomes the loginId for the user from here on and also used for delivery
loginID := "email@company.com"
//    user: Optional user object to populate new user information.
user := &descope.User{Name:"Joe", Email:"email@company.com", Phone:"+15555555555"}
//    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.
origin := "https://example.com"
//    loginOptions: this allows you to configure behavior during the authentication process.
loginOptions := &descope.LoginOptions{
    Stepup: true,
    MFA: true,
    CustomClaims: map[string]any{}{"test": "testClaim"},
    TemplateOptions: map[string]any{"option": "Value1"}
  }

res, err := descopeClient.Auth.WebAuthn().SignUpStart(ctx, loginID, user, origin, nil, loginOptions)
if (err != nil){
  fmt.Println("Unable to start webauthn sign-up: ", err)
} else {
  fmt.Println("Successfully started webauthn sign-up: ", res)
}

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)
}
# Args:
#   transactionID: The transaction ID returned by the sign_up_start function
transactionID = "xxxxxx"
#   response: The response returned by successful biometric authorization in the browser
response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
#   audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim 
audience = "xxxx"

try:
  resp = descope_client.webauthn.sign_up_finish(transactionID=transactionID, response=response, audience=audience)
  print ("Successfully finished webauthn sign-up")
  print(resp)
except AuthException as error:
  print ("Unable to finish webauthn sign-up")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//  ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//  r: HttpRequest for the action -  built by successful biometric authorization in the browser
//  w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation

res, err := descopeClient.Auth.WebAuthn().SignUpFinish(ctx, r, w)
if (err != nil){
  fmt.Println("Unable to finish webauthn sign-up: ", err)
} else {
  fmt.Println("Successfully finished webauthn sign-up: ", res)
}

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)
}
# Args:
#    login_id: email or phone - the loginId for the user
login_id = "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.
origin = "http://example.com"
#    loginOptions: (optional) - see login options - /api/overview#user-login-options
loginOptions = None
#   refreshToken: (optional) - refresh token of user logging in
refreshToken = None

try:
  resp = descope_client.webauthn.sign_in_start(login_id=login_id, origin=origin, loginOptions=loginOptions, refreshToken=refreshToken)
  print ("Successfully started webauthn sign-in")
  print(json.dumps(resp, indent=4))
except AuthException as error:
  print ("Unable to start webauthn sign-in")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//    ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//    loginID: email or phone - the loginId for the user
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.
origin := "https://example.com"

res, err := descopeClient.Auth.WebAuthn().SignInStart(ctx, loginID, origin)
if (err != nil){
  fmt.Println("Unable to start webauthn sign-in: ", err)
} else {
  fmt.Println("Successfully started webauthn sign-in: ", res)
}

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)
}
# Args:
#   transactionID: The transaction ID returned by the sign_in_start function
transactionID = "xxxxxx"
#   response: The response returned by successful biometric authorization in the browser
response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
#   audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim 
audience = "xxxx"

try:
  resp = descope_client.webauthn.sign_up_finish(transactionID=transactionID, response=response, audience=audience)
  print ("Successfully finished webauthn sign-in")
  print(resp)
except AuthException as error:
  print ("Unable to finish webauthn sign-in")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//  ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//  r: HttpRequest for the action. - built by successful biometric authorization in the browser
//  w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation

res, err := descopeClient.Auth.WebAuthn().SignInFinish(ctx, r, w)
if (err != nil){
  fmt.Println("Unable to finish webauthn sign-in: ", err)
} else {
  fmt.Println("Successfully finished webauthn sign-in: ", res)
}

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 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)
}
# Args:
#    login_id: email or phone - the loginId for the user
login_id = "email@company.com"
#     refresh_token: a refresh token for the user you are wanting to add a device for
refresh_token = "xxxxxx"
#    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.
origin = "http://example.com"

try:
  resp = descope_client.webauthn.update_start(login_id=login_id, refresh_token=refresh_token, origin=origin)
  print ("Successfully started webauthn update")
  print(resp)
except AuthException as error:
  print ("Unable to start webauthn update")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//    ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//    loginID: email or phone - the loginId for the user
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.
origin := "https://example.com"
//    r: HttpRequest for the action. This request should contain refresh token for the authenticated user.

res, error := descopeClient.Auth.WebAuthn().UpdateUserDeviceStart(ctx, loginID, origin, r)
if (err != nil){
  fmt.Println("Unable to start webauthn update: ", err)
} else {
  fmt.Println("Successfully stared webauthn update: ", res)
}

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)
}
# Args:
#   transactionID: The transaction ID returned by the sign_in_start function
transactionID = "xxxxxx"
#   response: The response returned by successful biometric authorization in the browser
response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'

try:
  resp = descope_client.webauthn.update_finish(transactionID=transactionID, response=response)
  print ("Successfully finished webauthn update")
  print(resp)
except AuthException as error:
  print ("Unable to finish webauthn update")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// Args:
//    ctx: context.Context - Application context for the transmission of context capabilities like
//        cancellation signals during the function call. In cases where context is absent, the context.Background()
//        function serves as a viable alternative.
//        Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
ctx := context.Background()
//    r: HttpRequest for the action. - built by successful biometric authorization in the browser

_, err := descopeClient.Auth.WebAuthn().UpdateUserDeviceFinish(ctx, r)
if (err != nil){
  fmt.Println("Unable to finish webauthn update: ", err)
} else {
  fmt.Println("Successfully finished webauthn update: ", res)
}

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