Set Up Passkey Authentication
Integrate standalone WebAuthn passkey authentication with the frontend and backend SDKs, Session recipe, and authentication routes.
Add passkey authentication to an existing application.
Add SuperTokens passkey authentication to this application. Inspect the existing stack and authentication setup, confirm that the backend SDK supports WebAuthn, and determine the deployment origin and relying-party configuration. Configure the frontend and backend WebAuthn and Session recipes, auth routes, HTTPS requirements, and fallback authentication where appropriate. Preserve existing conventions, do not commit secrets, and validate registration, authentication, cancellation, and unsupported-browser behavior.
Overview
This page shows you how to add the Passkeys authentication method to your project. The tutorial creates a login flow, rendered by either the Prebuilt UI components or by your own Custom UI.
Before you start
Passkeys may be unavailable because of browser, device, or authenticator support. Keep another authentication method or an account-recovery path available. A user can also cancel the browser or platform prompt; treat cancellation as an interrupted attempt, let the user retry, and do not report it as a successful sign-in or sign-up.
WebAuthn is available only in a secure context,
so serve the frontend over HTTPS in production. Browsers also allow http://localhost for local development.
The relying party (RP) ID must equal the frontend hostname or be a registrable suffix of it. The expected origin must exactly match the frontend origin, including its scheme and non-default port. If the frontend and API use different hostnames, configure these values explicitly instead of relying on values derived from the
Steps
1. Initialize the frontend SDK
import React from "react";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import WebAuthn from "supertokens-auth-react/recipe/webauthn";
import Session from "supertokens-auth-react/recipe/session";
SuperTokens.init({
appInfo: {
apiDomain: "...",
websiteDomain: "...",
appName: "...",
},
recipeList: [WebAuthn.init(), Session.init()],
});1.2 Include the pre-built UI components in your application.
In order for the pre-built UI to render inside your application, you have to specify which routes show the authentication components. The React SDK uses React Router under the hood to achieve this. Based on whether you already use this package or not in your project, there are two different ways of configuring the routes.
import React from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import * as reactRouterDom from "react-router-dom";
class App extends React.Component {
render() {
return (
<SuperTokensWrapper>
<BrowserRouter>
<Routes>
{/*This renders the login UI on the /auth route*/}
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI])}
{/*Your app routes*/}
</Routes>
</BrowserRouter>
</SuperTokensWrapper>
);
}
}import React from "react";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui";
import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui";
class App extends React.Component {
render() {
if (canHandleRoute([WebauthnPreBuiltUI])) {
// This renders the login UI on the /auth route
return getRoutingComponent([WebauthnPreBuiltUI]);
}
return <SuperTokensWrapper>{/*Your app*/}</SuperTokensWrapper>;
}
}import React from "react";
import { BrowserRouter, useRoutes } from "react-router-dom";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { WebauthnPreBuiltUI } from "supertokens-auth-react/recipe/webauthn/prebuiltui";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import * as reactRouterDom from "react-router-dom";
function AppRoutes() {
const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [WebauthnPreBuiltUI]);
const routes = useRoutes([
...authRoutes.map((route) => route.props),
// Include the rest of your app routes
]);
return routes;
}
function App() {
return (
<SuperTokensWrapper>
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
</SuperTokensWrapper>
);
}Call the SDK init function at the start of your application. The invocation includes the main configuration details, as well as the recipes that you use in your setup.
import SuperTokens from "supertokens-web-js";
import Session from "supertokens-web-js/recipe/session";
import WebAuthn from "supertokens-web-js/recipe/webauthn";
SuperTokens.init({
appInfo: {
apiDomain: "...",
apiBasePath: "...",
appName: "...",
},
recipeList: [Session.init(), WebAuthn.init()],
});import SuperTokens from "supertokens-react-native";
SuperTokens.init({
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth",
});import android.app.Application
import com.supertokens.session.SuperTokens
class MainApplication: Application() {
override fun onCreate() {
super.onCreate()
SuperTokens.Builder(this, "<YOUR_API_DOMAIN>")
.apiBasePath("/auth")
.build()
}
}import UIKit
import SuperTokensIOS
fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try SuperTokens.initialize(
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth"
)
} catch SuperTokensError.initError(let message) {
// TODO: Handle initialization error
} catch {
// Some other error
}
return true
}
}import 'package:supertokens_flutter/supertokens.dart';
void main() {
SuperTokens.init(
apiDomain: "<YOUR_API_DOMAIN>",
apiBasePath: "/auth",
);
}2. Add the passkeys UI
2.1 Add the sign up form
Create a form in which the user can input their email address.
When the user submits the form, call the registerCredentialWithSignUp method like in the next code snippet.
Under the hood, the method communicates with the backend SDK to fetch the registration options. Once the backend responds, it uses the browser’s APIs to begin the registration process. For a more detailed overview of the sign-up flow check the Important Concepts page.
import { registerCredentialWithSignUp } from "supertokens-web-js/recipe/webauthn";
async function signUp(email: string) {
try {
let response = await registerCredentialWithSignUp({
email,
userContext: {},
});
if (response.status === "SIGN_UP_NOT_ALLOWED" || response.status === "INVALID_AUTHENTICATOR_ERROR") {
// the reason string is a user friendly message
// about what went wrong. It can also contain a support code which users
// can tell you so you know why their sign in / up was not allowed.
window.alert(response.reason);
} else if (response.status === "INVALID_EMAIL_ERROR" || response.status === "EMAIL_ALREADY_EXISTS_ERROR") {
window.alert("Invalid email");
} else if (
response.status === "INVALID_CREDENTIALS_ERROR" ||
response.status === "OPTIONS_NOT_FOUND_ERROR" ||
response.status === "INVALID_OPTIONS_ERROR" ||
response.status === "AUTHENTICATOR_ALREADY_REGISTERED" ||
response.status === "FAILED_TO_REGISTER_USER" ||
response.status === "WEBAUTHN_NOT_SUPPORTED"
) {
// These errors represent various issues with the authenticator, credential or the flow itself.
// These should be handled individually by you.
// The user should be informed that they should retry the sign up process or get in touch with you.
window.alert("Please try again");
} else if (response.status === "INVALID_GENERATED_OPTIONS_ERROR") {
window.alert("The registration request expired. Please try again.");
} else if (response.status === "GENERAL_ERROR") {
window.alert(response.message);
} else if (response.status === "OK") {
// User signed up successfully.
window.alert("You have been signed up successfully");
} else {
window.alert("Sign up could not be completed. Please try another authentication method.");
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you,
// or if the input email / phone number is not valid.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}The requests in this standalone authentication flow omit shouldTryLinkingWithSessionUser, so it defaults to false.
Set it to true only for an authenticated add-factor or account-linking flow where your backend policy permits linking
to the session user.
Get the email address from the user
Add a form where the user can input their email address.
Fetch the registration options from the backend SDK
When the user submits the form, call the register options API.
Save the response to use it in the next step.
curl --location --request POST '<YOUR_API_DOMAIN>/auth/webauthn/options/register' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
"email": "johndoe@gmail.com",
"displayName": "John Doe"
}'Register a new credential authenticator API
Use the received options generate a new credential. The implementation will vary based on the platform you are using.
- React Native: You can use the
react-native-passkeylibrary. - iOS: Use the
Authentication Servicesframework. - Android: Use the
Android Credential Manager API. - Flutter: Use platform channels to access the native APIs.
Call the sign up API
Using the newly generated credential, call the sign up API to save the new authentication method.
curl -X POST "<YOUR_API_DOMAIN>/auth/public/webauthn/signup" \
-H "Content-Type: application/json" \
-d '{
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRkbm9uZQ",
"transports": [
"internal",
"hybrid"
]
},
"type": "public-key"
}
}'const response = await fetch("<YOUR_API_DOMAIN>/auth/public/webauthn/signup", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRkbm9uZQ",
"transports": [
"internal",
"hybrid"
]
},
"type": "public-key"
}
})
});package main
import (
"net/http"
"strings"
)
func main() {
req, err := http.NewRequest("POST", "<YOUR_API_DOMAIN>/auth/public/webauthn/signup", strings.NewReader(`{
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRkbm9uZQ",
"transports": [
"internal",
"hybrid"
]
},
"type": "public-key"
}
}`))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer response.Body.Close()
}import requests
response = requests.post(
"<YOUR_API_DOMAIN>/auth/public/webauthn/signup",
headers={
"Content-Type": "application/json"
},
json={
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0",
"attestationObject": "o2NmbXRkbm9uZQ",
"transports": [
"internal",
"hybrid"
]
},
"type": "public-key"
}
},
)Encode id, rawId, clientDataJSON, and attestationObject as unpadded Base64URL. Include transports when the
authenticator supplies it.
2.2 Add the login form
Add a button that can trigger the sign in flow.
This is all that you need in terms of UI.
When the user clicks it, call the authenticateCredentialWithSignIn method to handle the whole process.
The function uses the backend authentication options to trigger the challenge signing action through the browser API. Then, it forwards the result to the backend for validation. For a more detailed overview of the login flow check the Important Concepts page.
import { authenticateCredentialWithSignIn } from "supertokens-web-js/recipe/webauthn";
async function signIn() {
try {
let response = await authenticateCredentialWithSignIn({ userContext: {} });
if (response.status === "SIGN_IN_NOT_ALLOWED") {
// the reason string is a user friendly message
// about what went wrong. It can also contain a support code which users
// can tell you so you know why their sign in / up was not allowed.
window.alert(response.reason);
} else if (response.status === "WEBAUTHN_NOT_SUPPORTED") {
// the user's browser does not support the WebAuthn standard
window.alert("Login method not supported");
} else if (
response.status === "INVALID_CREDENTIALS_ERROR" ||
response.status === "INVALID_OPTIONS_ERROR" ||
response.status === "FAILED_TO_AUTHENTICATE_USER"
) {
// These errors represent various issues with the authenticator, credential or the flow itself.
// FAILED_TO_AUTHENTICATE_USER can also indicate that the user cancelled the authenticator prompt.
// These should be handled individually by you.
// The user should be informed that they should retry the sign in process or get in touch with you.
window.alert("Please try again");
} else if (response.status === "GENERAL_ERROR") {
window.alert(response.message);
} else if (response.status === "OK") {
// User signed in successfully.
window.alert("You have been signed in successfully");
} else {
window.alert("Sign in could not be completed. Please try another authentication method.");
}
} catch (err: any) {
if (err.isSuperTokensGeneralError === true) {
// this may be a custom error message sent from the API by you,
// or if the input email / phone number is not valid.
window.alert(err.message);
} else {
window.alert("Oops! Something went wrong.");
}
}
}Add a button that can trigger the sign in flow
Get the sign in options from the backend SDK
When the user taps the sign in button, call the backend API to fetch the sign in options.
curl --location --request POST '<YOUR_API_DOMAIN>/auth/webauthn/options/signin' \
--header 'Content-Type: application/json; charset=utf-8'Use the authenticator to sign the challenge
With the received options, invoke the authenticator to sign the challenge. The implementation will vary based on the platform you are using.
Call the sign in API
Send the signed challenge to the backend for validation.
curl -X POST "<YOUR_API_DOMAIN>/auth/public/webauthn/signin" \
-H "Content-Type: application/json" \
-d '{
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4Y",
"signature": "MEUCIQDxV_LS8qk"
},
"type": "public-key"
}
}'const response = await fetch("<YOUR_API_DOMAIN>/auth/public/webauthn/signin", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4Y",
"signature": "MEUCIQDxV_LS8qk"
},
"type": "public-key"
}
})
});package main
import (
"net/http"
"strings"
)
func main() {
req, err := http.NewRequest("POST", "<YOUR_API_DOMAIN>/auth/public/webauthn/signin", strings.NewReader(`{
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4Y",
"signature": "MEUCIQDxV_LS8qk"
},
"type": "public-key"
}
}`))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer response.Body.Close()
}import requests
response = requests.post(
"<YOUR_API_DOMAIN>/auth/public/webauthn/signin",
headers={
"Content-Type": "application/json"
},
json={
"webauthnGeneratedOptionsId": "opt_123...",
"credential": {
"id": "AbCdEf0123_-",
"rawId": "AbCdEf0123_-",
"authenticatorAttachment": "platform",
"clientExtensionResults": {},
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0In0",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4Y",
"signature": "MEUCIQDxV_LS8qk"
},
"type": "public-key"
}
},
)Encode id, rawId, clientDataJSON, authenticatorData, signature, and an optional userHandle as unpadded
Base64URL. Include userHandle in credential.response when the authenticator returns it.
Initialize the backend SDK and include the WebAuthn recipe.
The init call includes configuration details for your app.
It specifies how the backend connects to the SuperTokens Core, as well as the Recipes used in your setup.
The recipe exposes the required endpoints that get accessed by the frontend code, and communicates with the SuperTokens Core to complete the authentication flow. You can configure different aspects of the recipe’s behavior but, for the completion of this guide, use the default values. After you confirm that the flow works as expected, you can explore more advanced customization options.
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import WebAuthN from "supertokens-node/recipe/webauthn";
supertokens.init({
// Replace this with the framework you are using
framework: "express",
supertokens: {
// We use try.supertokens for demo purposes.
// At the end of the tutorial we will show you how to create
// your own SuperTokens core instance and then update your config.
connectionURI: "https://try.supertokens.io",
// apiKey: <YOUR_API_KEY>
},
appInfo: {
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth",
},
recipeList: [WebAuthN.init(), Session.init()],
});import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/webauthn"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
err := supertokens.Init(supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
ConnectionURI: "https://try.supertokens.io",
// APIKey: "<YOUR_API_KEY>",
},
AppInfo: supertokens.AppInfo{
AppName: "<YOUR_APP_NAME>",
APIDomain: "<YOUR_API_DOMAIN>",
WebsiteDomain: "<YOUR_WEBSITE_DOMAIN>",
},
RecipeList: []supertokens.Recipe{
webauthn.Init(nil),
session.Init(nil),
},
})
if err != nil {
panic(err)
}
}from supertokens_python import InputAppInfo, SupertokensConfig, init
from supertokens_python.recipe import session, webauthn
init(
app_info=InputAppInfo(
app_name="<YOUR_APP_NAME>",
api_domain="<YOUR_API_DOMAIN>",
website_domain="<YOUR_WEBSITE_DOMAIN>",
api_base_path="/auth",
website_base_path="/auth"
),
supertokens_config=SupertokensConfig(
# We use try.supertokens for demo purposes.
# At the end of the tutorial we will show you how to create
# your own SuperTokens core instance and then update your config.
connection_uri="https://try.supertokens.io",
# api_key="<YOUR_API_KEY>"
),
framework='flask', # Replace this with the framework you are using
recipe_list=[
webauthn.init(),
session.init()
]
)Next steps
Having completed the main setup, you can explore more advanced topics related to the WebAuthn recipe.
Backend Recipe Configuration
Read through the configuration options for the backend recipe.
Customize Credentials Generation
See how you can adjust the process that generates credentials.
Customize Credentials Validation
Discover how to customize the validation process.
Email Delivery
Customize the email sending process.