Getting started
This guide will walk through the steps required to implement passkeys in your app.
Pre-built UI vs Client SDKs
There are two ways to integrate passkeys within your app:
Using Authsignal's pre-built UI. If you're already using this, then supporting passkeys is very simple. You just need to configure a custom domain and follow the integration steps for pre-built UI flows.
Using Authsignal's Client SDKs. This option provides the flexibility to embed passkey functionality directly into your app, enhancing an existing login page or step-up authentication flow.
You can use one or both approaches in your app. For example, you can use the pre-built UI to allow passkeys for MFA along with other methods (e.g. SMS, Email Magic Link, Authenticator App) and also use client SDKs to add passwordless login to your existing UI.
Configuration
The key steps required to configure your app for passkeys are as follows:
- Enable passkeys in the Authsignal Portal
- Set your Relying Party ID to match your app domain
- Set your Expected Origins
For more detail on how configure passkeys, including steps required for local development, check out our configuration docs.
Creating a passkey
If using the pre-built UI, the integration steps for passkeys are the same as for other authentication methods.
If using a client SDK, the steps to allow users to create a passkey for your app are outlined below.
1. Backend - Track an action
In your app's backend, track an action using one of our Server SDKs or with a REST call to our Server API.
- Node.js
- C#
- Java
- Go
- Ruby
- PHP
- Python
const result = await authsignal.track({
userId: user.id,
action: "enrollPasskey",
scope: "add:authenticators",
});
const token = result.token;
var request = new TrackRequest(UserId: user.Id, Action: "enrollPasskey", Scope: "add:authenticators");
var response = await authsignal.Track(request);
var token = response.Token;
TrackRequest request = new TrackRequest();
request.userId = user.id;
request.action = "enrollPasskey";
request.scope = "add:authenticators";
CompletableFuture<String> token = authsignal.track(request).thenApply(response -> response.token);
response, err := authsignal.TrackAction(
client.TrackRequest{UserId: user.Id, Action: "enrollPasskey", Scope: "add:authenticators"},
)
token := response.Token;
result = Authsignal.track({
user_id: user_id,
action: "enrollPasskey",
})
token = result[:token]
$result = Authsignal::track(userId: $userId, action: "enrollPasskey");
$token = $result["token"]
result = authsignal_client.track_action(
user_id=user_id,
action="enrollPasskey"
)
token = result["token"]
When tracking an action to enroll an authenticator, the scope add:authenticators
must be explicitly specified if the user is already enrolled with at least one authentication method (e.g. passkey).
In such cases you should ensure users are strongly authenticated with one of their existing methods before they can enroll an additional authenticator.
2. Frontend - Use a client SDK
In your app's frontend, call the signUp
function in one of our Client SDKs, passing the token returned in step 1.
- Web
- React Native
- Flutter
- Swift
- Kotlin
const resultToken = await authsignal.passkey.signUp({ token, userName });
const { data: resultToken, error } = await authsignal.passkey.signUp({ token, userName });
var result = await authsignal.passkey.signUp(token, userName);
var resultToken = result.data;
let result = await authsignal.passkey.signUp(token: token, userName: userName)
let resultToken = result.data
val result = authsignalPasskey.signUp(token, userName)
val resultToken = result.data
3. Backend - Validate the result
Pass the result token returned by the client SDK in step 2 to your backend, validating the result of the enrollment server-side.
- Node.js
- C#
- Java
- Go
- Ruby
- PHP
- Python
const { success } = await authsignal.validateChallenge({ token });
if (success) {
// The user enrolled successfully
}
var request = new ValidateChallengeRequest(Token: token);
var response = await authsignal.ValidateChallenge(request);
if (response.Success == UserActionState.CHALLENGE_SUCCEEDED) {
// The user enrolled successfully
}
ValidateChallengeRequest request = new ValidateChallengeRequest();
request.token = token;
authsignal.validateChallenge(request).thenAccept(response -> {
if (response.state == UserActionState.CHALLENGE_SUCCEEDED) {
// The user enrolled successfully
}
});
response, err := authsignal.ValidateChallenge(
client.ValidateChallengeRequest{
Token: token,
},
)
if err == nil && response.Success {
// The user enrolled successfully
}
response = Authsignal.validate_challenge(token: token)
if response[:success]
# The user enrolled successfully
end
$result = Authsignal::validateChallenge(token: $token);
if ($result["success"]) {
# The user enrolled successfully
}
result = client.validate_challenge(token=token)
if result["success"]:
# The user authenticated successfully
Authenticating with an existing passkey
1. Frontend - Use a client SDK
In your app's frontend, call the signIn
function in one of our Client SDKs:
- Web
- React Native
- Flutter
- Swift
- Kotlin
const resultToken = await authsignal.passkey.signIn();
const { data: resultToken, error } = await authsignal.passkey.signIn();
var result = await authsignal.passkey.signIn();
var resultToken = result.data;
let result = await authsignal.passkey.signIn()
let resultToken = result.data
val result = authsignalPasskey.signIn()
val resultToken = result.data
2. Backend - Validate the result
Pass the token returned by the client SDK in step 1 to your backend, validating the result of the enrollment server-side.
- Node.js
- C#
- Java
- Go
- Ruby
- PHP
- Python
const { state } = await authsignal.validateChallenge({ token });
if (state === "CHALLENGE_SUCCEEDED") {
// The user authenticated successfully
}
var request = new ValidateChallengeRequest(Token: token);
var response = await authsignal.ValidateChallenge(request);
if (response.State == UserActionState.CHALLENGE_SUCCEEDED) {
// The user authenticated successfully
}
ValidateChallengeRequest request = new ValidateChallengeRequest();
request.token = token;
authsignal.validateChallenge(request).thenAccept(response -> {
if (response.state == UserActionState.CHALLENGE_SUCCEEDED) {
// The user authenticated successfully
}
});
response, err := authsignal.ValidateChallenge(
client.ValidateChallengeRequest{
Token: token,
},
)
if err == nil && response.State == "CHALLENGE_SUCCEEDED" {
// The user authenticated successfully
}
response = Authsignal.validate_challenge(token: token)
if response[:state] == "CHALLENGE_SUCCEEDED"
# The user authenticated successfully
end
$result = Authsignal::validateChallenge(token: $token);
if ($result["state"] === "CHALLENGE_SUCCEEDED") {
# The user authenticated successfully
}
result = client.validate_challenge(token=token)
if result["state"] == "CHALLENGE_SUCCEEDED":
# The user authenticated successfully
On iOS, if the user has no passkeys available on their device, calling signIn
will display a QR code.
To avoid this UX you can instead use autofill to only show the existing passkeys that are available.
Using autofill (Web and iOS only)
This requires you to have an input field on your web page or app screen for the identifier (e.g. email address) which is used to login. When the input field is focused, the user will be able to select an existing passkey if one is available on their device.
- Web
- React Native
- Swift
<input placeholder="Email address" autocomplete="username webauthn" />
<TextInput placeholder="Email address" value="email" onChangeText={setEmail} textContentType={"username"} />
TextField("Email address", text: $email).textContentType(.username)
1. Frontend - Use a client SDK
In your app's frontend, call the signIn
function in one of our Client SDKs and set the autofill
param to true.
- Web
- React Native
- Swift
authsignal.passkey.signIn({ autofill: true }).then((token) => {
if (token) {
// The user has selected a passkey via autofill
}
});
authsignal.passkey.signIn({ autofill: true }).then(({ data: token, error }) => {
if (!error && token) {
// The user has selected a passkey via autofill
}
});
let result = await authsignal.passkey.signIn(autofill: true)
if let token = result.data {
// The user has selected a passkey via autofill
}
If the user focuses the input field and successfully activates their passkey, the Authsignal client SDK will resolve with a token.
On Android you can achieve a similar UX by showing an input field and calling signIn()
when the field is focused.
2. Backend - Validate the result
Send the token returned by the client SDK to your backend and validate the result of the sign-in attempt server-side.
- Node.js
- C#
- Java
- Go
- Ruby
- PHP
- Python
const { state } = await authsignal.validateChallenge({ token });
if (state === "CHALLENGE_SUCCEEDED") {
// The user authenticated successfully
}
var request = new ValidateChallengeRequest(Token: token);
var response = await authsignal.ValidateChallenge(request);
if (response.State == UserActionState.CHALLENGE_SUCCEEDED) {
// The user authenticated successfully
}
ValidateChallengeRequest request = new ValidateChallengeRequest();
request.token = token;
authsignal.validateChallenge(request).thenAccept(response -> {
if (response.state == UserActionState.CHALLENGE_SUCCEEDED) {
// The user authenticated successfully
}
});
response, err := authsignal.ValidateChallenge(
client.ValidateChallengeRequest{
Token: token,
},
)
if err == nil && response.State == "CHALLENGE_SUCCEEDED" {
// The user authenticated successfully
}
response = Authsignal.validate_challenge(token: token)
if response[:state] == "CHALLENGE_SUCCEEDED"
# The user authenticated successfully
end
$result = Authsignal::validateChallenge(token: $token);
if ($result["state"] === "CHALLENGE_SUCCEEDED") {
# The user authenticated successfully
}
result = client.validate_challenge(token=token)
if result["state"] == "CHALLENGE_SUCCEEDED":
# The user authenticated successfully