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.
Passkeys let users authenticate with phishing-resistant credentials based on WebAuthn. These credentials can be created and used through built-in device authenticators, such as fingerprint, facial recognition, or device PIN, as well as external security keys, such as YubiKeys or other FIDO-compatible hardware authenticators.
When implementing passkey authentication with Descope Backend SDKs, your application is responsible for coordinating the browser or native app passkey ceremony and then sending the resulting WebAuthn response to Descope.
A typical backend SDK implementation includes the following flows:
- Start and finish passkey sign-up
- Start and finish passkey sign-in
- Add a passkey to an existing user
- Validate the resulting Descope session
Backend SDK
Install SDK
npm i --save @descope/node-sdkpip3 install descopego 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>gem install descopecomposer require descope/descope-phpdotnet add package descopeImport 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
Start the passkey sign-up flow by calling the sign-up start function.
This function requires a unique loginId, such as an email address or phone number. Descope uses this value as the user's login ID and associates the passkey credentials with it.
The function also requires an origin value. This should be the value of window.location.origin from your application client. Descope validates this origin against the domain configured in the Descope console. The origin must match the configured domain or be a valid subdomain.
// 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)
}// Args:
// loginId: email or phone - becomes the loginId for the user from here on and also used for delivery
String 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.
String origin = "https://example.com";
// user: Optional user object to populate new user information.
User user = User.builder()
.name("Joe Person")
.phone("+15555555555")
.email(loginId)
.build();
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
WebAuthnTransactionResponse resp = webAuthn.signUpStart(loginId, user, origin);
// Return resp.getTransactionId() and resp.getOptions() to your client
} catch (DescopeException de) {
// Handle the error
}// Args:
// LoginId (string): The login ID for the user — becomes their permanent login ID
// Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting
// User (SignUpUser) optional: Additional user metadata
// PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options (Attestation, AuthenticatorSelection, UserVerification, ExtensionsJSON)
var response = await client.Auth.V1.Auth.Webauthn.Signup.Start.PostAsync(
new WebauthnSignUpStartRequest
{
LoginId = "email@company.com",
Origin = "https://example.com",
User = new SignUpUser
{
Name = "Joe Person",
Email = "email@company.com"
},
PasskeyOptions = new PasskeyOptions { /* ... */ }
});
// response.TransactionId — pass to Finish Sign-Up along with the browser's credential responseFinish Sign-Up
After starting the sign-up flow, Descope returns a transactionId. Your frontend must complete the passkey creation ceremony in the browser or native app, then send the resulting WebAuthn credential response back to your backend.
Use the transactionId and the credential response to finish sign-up.
// 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)
}// Args:
// transactionId: The transaction ID returned by the signUpStart function
String transactionId = "xxxxxx";
// response: The response returned by successful biometric authorization in the browser
String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}";
WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder()
.transactionId(transactionId)
.response(response)
.build();
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
AuthenticationInfo authInfo = webAuthn.signUpFinish(finishRequest);
String sessionJwt = authInfo.getToken().getJwt();
String refreshJwt = authInfo.getRefreshToken().getJwt();
} catch (DescopeException de) {
// Handle the error
}// Args:
// TransactionId (string): The transaction ID returned by Start Sign-Up
// Response (string): The JSON credential response from the browser's WebAuthn API
var response = await client.Auth.V1.Auth.Webauthn.Signup.Finish.PostAsync(
new WebauthnSignUpFinishRequest
{
TransactionId = "xxxxxx",
Response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"attestationObject\":\"\",\"clientDataJSON\":\"\"}}"
});Start Sign-In
Start the passkey sign-in flow by calling the sign-in start function.
This function requires the user's loginId, such as their email address or phone number. It also requires an origin value, which should be the value of window.location.origin from your application client.
Descope validates the origin against the domain configured in the Descope Console. The origin must match the configured domain or be a valid subdomain.
// 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)
}// Args:
// loginId: email or phone - the loginId for the user
String loginId = "email@company.com";
// origin: This is the origin of the sign-in 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.
String origin = "https://example.com";
// token: (optional) the user's current session token in the event of step-up/mfa
String token = null;
// loginOptions: (optional) configure behavior during the authentication process
LoginOptions loginOptions = null;
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
WebAuthnTransactionResponse resp = webAuthn.signInStart(loginId, origin, token, loginOptions);
// Return resp.getTransactionId() and resp.getOptions() to your client
} catch (DescopeException de) {
// Handle the error
}
// For MFA or step-up authentication:
LoginOptions mfaOptions = LoginOptions.builder()
.mfa(true)
.build();
WebAuthnTransactionResponse mfaResp = webAuthn.signInStart(loginId, origin, sessionJwt, mfaOptions);// Args:
// LoginId (string): The login ID of the user
// Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting
// LoginOptions (LoginOptions) optional: Configure step-up, MFA, or custom claims
// PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options (Attestation, AuthenticatorSelection, UserVerification, ExtensionsJSON)
var response = await client.Auth.V1.Auth.Webauthn.Signin.Start.PostAsync(
new WebauthnSignInStartRequest
{
LoginId = "email@company.com",
Origin = "https://example.com",
LoginOptions = new LoginOptions { /* ... */ },
PasskeyOptions = new PasskeyOptions { /* ... */ }
});
// response.TransactionId — pass to Finish Sign-In along with the browser's credential responseFinish Sign-In
After starting the sign-in flow, Descope returns a transactionId. Your frontend must complete the passkey authentication ceremony in the browser or native app, then send the resulting WebAuthn credential response back to your backend.
Use the transactionId and credential response to finish sign-in.
// 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)
}// Args:
// transactionId: The transaction ID returned by the signInStart function
String transactionId = "xxxxxx";
// response: The response returned by successful biometric authorization in the browser
String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}";
WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder()
.transactionId(transactionId)
.response(response)
.build();
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
AuthenticationInfo authInfo = webAuthn.signInFinish(finishRequest);
String sessionJwt = authInfo.getToken().getJwt();
String refreshJwt = authInfo.getRefreshToken().getJwt();
} catch (DescopeException de) {
// Handle the error
}// Args:
// TransactionId (string): The transaction ID returned by Start Sign-In
// Response (string): The JSON credential response from the browser's WebAuthn API
var response = await client.Auth.V1.Auth.Webauthn.Signin.Finish.PostAsync(
new WebauthnSignInFinishRequest
{
TransactionId = "xxxxxx",
Response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}"
});Sign-Up or Sign-In
Use SignUpOrInStart when you want a single passkey flow and do not need to decide upfront whether the user is signing up or signing in. Descope checks whether a user with the given loginID already has a passkey registered and returns the appropriate WebAuthn ceremony options.
The response includes a create field that tells your application how to complete the flow:
create: true— the user is new. On the client, callnavigator.credentials.createwith the returnedoptions, then callSignUpFinish.create: false— the user already exists. On the client, callnavigator.credentials.getwith the returnedoptions, then callSignInFinish.
There is no SignUpOrInFinish method. The finish step always uses SignUpFinish or SignInFinish, depending on the value of create.
// 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 request and the value should be window.location.origin from the client.
const origin = "https://example.com"
// loginOptions (LoginOptions): (optional) configure behavior during the authentication process, such as tenantId for tenant user isolation.
const loginOptions = {
"tenantId": "tenant1",
}
const resp = await descopeClient.auth.webauthn.signUpOrIn.start(loginId, origin, undefined, loginOptions);
if (!resp.ok) {
console.log("Unable to start webauthn sign-up or 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-up or sign-in")
console.log(resp)
}
// Return resp to your client. The client should:
// - parse resp.data.options as the publicKey option for navigator.credentials.create or .get, based on resp.data.create
// - POST the resulting credential back to your server
// ---- Complete the flow (signUp.finish or signIn.finish, depending on resp.data.create) ----
// transactionId comes from signUpOrIn.start; response is the credential response from the browser.
const transactionId = resp.data.transactionId
const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
let authInfo
if (resp.data.create) {
authInfo = await descopeClient.auth.webauthn.signUp.finish(transactionId, response)
} else {
authInfo = await descopeClient.auth.webauthn.signIn.finish(transactionId, response)
}
if (!authInfo.ok) {
console.log("Unable to finish webauthn sign-up or sign-in")
console.log("Status Code: " + authInfo.code)
console.log("Error Code: " + authInfo.error.errorCode)
console.log("Error Description: " + authInfo.error.errorDescription)
console.log("Error Message: " + authInfo.error.errorMessage)
}
else {
console.log("Successfully finished webauthn sign-up or sign-in")
console.log(authInfo)
}# 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 request and the value should be window.location.origin from the client.
origin = "https://example.com"
try:
resp = descope_client.webauthn.sign_up_or_in_start(login_id=login_id, origin=origin)
print ("Successfully started webauthn sign-up or sign-in")
print(json.dumps(resp, indent=4))
except AuthException as error:
print ("Unable to start webauthn sign-up or sign-in")
print ("Status Code: " + str(error.status_code))
print ("Error: " + str(error.error_message))
# Return resp to your client. The client should:
# - parse resp["options"] as the publicKey option for navigator.credentials.create or .get, based on resp["create"]
# - POST the resulting credential back to your server
# ---- Complete the flow (sign_up_finish or sign_in_finish, depending on resp["create"]) ----
# transaction_id comes from sign_up_or_in_start; response is the credential response from the browser.
transaction_id = resp["transactionId"]
response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}'
try:
if resp["create"]:
auth_info = descope_client.webauthn.sign_up_finish(transaction_id=transaction_id, response=response)
else:
auth_info = descope_client.webauthn.sign_in_finish(transaction_id=transaction_id, response=response)
print ("Successfully finished webauthn sign-up or sign-in")
print(auth_info)
except AuthException as error:
print ("Unable to finish webauthn sign-up or 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 - becomes the loginId for the user from here on and also used for delivery
loginID := "email@company.com"
// origin: This is the origin of the request and the value should be window.location.origin from the client.
origin := "https://example.com"
// loginOptions: (optional) configure behavior during the authentication process, such as TenantID for tenant user isolation.
loginOptions := &descope.LoginOptions{
TenantID: "tenant1",
}
res, err := descopeClient.Auth.WebAuthn().SignUpOrInStart(ctx, loginID, origin, loginOptions)
if err != nil {
fmt.Println("Unable to start webauthn sign-up or sign-in: ", err)
return
}
fmt.Println("Successfully started webauthn sign-up or sign-in: ", res)
// Return res to your client. The client should:
// - parse res.Options as the publicKey option for navigator.credentials.create or .get, based on res.Create
// - POST the resulting credential back to your server as a WebAuthnFinishRequest
// ---- Complete the flow (SignUpFinish or SignInFinish, depending on res.Create) ----
// finishRequest contains the transactionId from SignUpOrInStart and the credential response from the browser.
finishRequest := &descope.WebAuthnFinishRequest{
TransactionID: res.TransactionID,
Response: credentialResponseFromBrowser,
}
var authInfo *descope.AuthenticationInfo
if res.Create {
authInfo, err = descopeClient.Auth.WebAuthn().SignUpFinish(ctx, finishRequest, w)
} else {
authInfo, err = descopeClient.Auth.WebAuthn().SignInFinish(ctx, finishRequest, w)
}
if err != nil {
fmt.Println("Unable to finish webauthn sign-up or sign-in: ", err)
return
}
fmt.Println("Successfully finished webauthn sign-up or sign-in: ", authInfo)// Args:
// loginId: email or phone - becomes the loginId for the user from here on and also used for delivery
String loginId = "email@company.com";
// origin: This is the origin of the request and the value should be window.location.origin from the client.
String origin = "https://example.com";
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
WebAuthnTransactionResponse resp = webAuthn.signUpOrInStart(loginId, origin);
// Return resp.getTransactionId(), resp.getOptions(), and resp.isCreate() to your client
} catch (DescopeException de) {
// Handle the error
}
// Return resp to your client. The client should:
// - parse resp.getOptions() as the publicKey option for navigator.credentials.create or .get, based on resp.isCreate()
// - POST the resulting credential back to your server
// ---- Complete the flow (signUpFinish or signInFinish, depending on resp.isCreate()) ----
// transactionId comes from signUpOrInStart; response is the credential response from the browser.
String transactionId = resp.getTransactionId();
String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}";
WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder()
.transactionId(transactionId)
.response(response)
.build();
try {
AuthenticationInfo authInfo;
if (resp.isCreate()) {
authInfo = webAuthn.signUpFinish(finishRequest);
} else {
authInfo = webAuthn.signInFinish(finishRequest);
}
String sessionJwt = authInfo.getToken().getJwt();
String refreshJwt = authInfo.getRefreshToken().getJwt();
} catch (DescopeException de) {
// Handle the error
}Start Add User Device
Use Start Add User Device to add a new passkey or authenticator to an existing user account.
This flow is useful when a user has already authenticated with another method and wants to register a passkey for future sign-ins. The function requires a valid refresh token for the authenticated user.
// 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)
}// Args:
// loginId: email or phone - the loginId for the user
String loginId = "email@company.com";
// origin: This is the origin of the 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.
String origin = "https://example.com";
// refreshToken: Valid refresh token for this user from another authentication method. This is required.
String refreshToken = "xxxxx";
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
WebAuthnTransactionResponse resp = webAuthn.updateUserDeviceStart(loginId, origin, refreshToken);
// Return resp.getTransactionId() and resp.getOptions() to your client
} catch (DescopeException de) {
// Handle the error
}// Args:
// LoginId (string): The login ID of the authenticated user
// Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting
// PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options (Attestation, AuthenticatorSelection, UserVerification, ExtensionsJSON)
// refreshJwt (string): The session's refresh token (required — user must be authenticated)
var response = await client.Auth.V1.Auth.Webauthn.Update.Start.PostWithJwtAsync(
new WebauthnAddDeviceStartRequest
{
LoginId = "email@company.com",
Origin = "https://example.com",
PasskeyOptions = new PasskeyOptions { /* ... */ }
},
refreshJwt);
// response.TransactionId — pass to Finish Add User Device along with the browser's credential responseFinish Add User Device
After starting the add-device flow, Descope returns a transactionId. Your frontend must complete the passkey registration ceremony in the browser or native app, then send the resulting WebAuthn credential response back to your backend.
Use the transactionId and credential response to finish adding the passkey to the user's account.
// 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)
}// Args:
// transactionId: The transaction ID returned by the updateUserDeviceStart function
String transactionId = "xxxxxx";
// response: The response returned by successful biometric authorization in the browser
String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}";
WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder()
.transactionId(transactionId)
.response(response)
.build();
WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService();
try {
webAuthn.updateUserDeviceFinish(finishRequest);
} catch (DescopeException de) {
// Handle the error
}// Args:
// TransactionId (string): The transaction ID returned by Start Add User Device
// Response (string): The JSON credential response from the browser's WebAuthn API
await client.Auth.V1.Auth.Webauthn.Update.Finish.PostAsync(
new WebauthnAddDeviceFinishRequest
{
TransactionId = "xxxxxx",
Response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"attestationObject\":\"\",\"clientDataJSON\":\"\"}}"
});Session Validation
After completing passkey sign-up or sign-in, validate the user session on your backend.
Descope provides session management capabilities, including session validation, configurable session timeouts, and logout support.
For backend session validation examples, see Session Validation with Backend SDKs.
Checkpoint
Your application is now integrated with Descope. Please test with sign-up or sign-in use case.