Authenticating Users via Passkey
This guide assumes you have already followed the Setup Guide and have set up the Alchemy Account Provider using this guide. Please refer to the guides above for more information on how to properly setup your project.
For a complete example of how we can setup a project and use the various available authentication methods, please refer to our quickstart example.
Authenticating a user is easy using the useAuthenticate()
hook from the @account-kit/react-native
package. Before we can use that, you’ll need to configure your application to associate it with a domain you control.
Step 1: Set an rpId in createConfig
The rpId (“relaying party ID”) specifies the domain on which passkeys are allowed to function. While passkeys on web applications are automatically associated with the website’s domain, mobile applications must be registered with a domain to prove that they are associated.
In your call to createConfig
, pass an rpId
parameter set to a domain you control. Note that the scheme is always assumed to be “https://” and should be omitted.
Step 2: Host a Site Association JSON
While passkeys on web applications are automatically associated with the website’s domain, mobile applications must be registered on that domain to prove that they are associated. To do so, you will need to host a JSON file referencing your app on your domain. The details of doing so differ on iOS and Android.
iOS configuration
More information in Apple docs
On your webserver, set up the route
This route should serve a static JSON object containing your team id and identifier. You should replace <team-identifier>
and <bundle-id>
in the below snippet, so it might appear as e.g. H123456789.com.yourapp.passkeyExample
.
Next, in XCode under “Signing & Capabilities”, add a new capability of type “Associated Domains”. Now add the following, replacing <yourdomain>
with the domain on which you hosted the JSON (e.g. your-domain.com
):
Android configuration
More information in Android docs
On your webserver, set up the route
This route should serve a static JSON object containing the following information:
You should replace <your-package-name>
with the package name, e.g. com.yourapp.passkeyExample
, and "<sha-hex-value>"
with the SHA256 fingerprints of your app’s [signing certificate], e.g. "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:12:75:91:03:3B:9C"
.
Step 3: Add a Passkey
You have the option of creating an account with passkey and email or with passkey alone.
If you create a passkey without an email associated with the user, you risk your users losing access to their wallets if they lose their device.
Recommended security practice: Proxy authentication requests to your backend server to enforce additional security measures:
- When a user attempts to sign up with both passkey and email, you can first require email verification before allowing the passkey to be created
- Alternatively, you can restrict initial signup to email-based methods only (which inherently verify email ownership), then allow users to add passkeys after their account is established
- This approach gives you greater control over the authentication flow and helps prevent account recovery issues
By implementing server-side verification, you ensure that passkeys are only created for verified identities, reducing the risk of permanent access loss.
Option 1: Creating an account with passkey and email (recommended)
import { function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.
This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.
useAuthenticate } from "@account-kit/react-native";
import React, { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
useState } from "react";
import { type Alert = AlertStatic
const Alert: AlertStaticAlert, class ViewView, class TextText, class TextInputTextInput, class ButtonButton, const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable } from "react-native";
function function CreatePasskeyAndEmail(): JSX.ElementCreatePasskeyAndEmail() {
const { const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>authenticate } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.
This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.
useAuthenticate();
const [const email: stringemail, const setEmail: React.Dispatch<React.SetStateAction<string>>setEmail] = useState<string>(initialState: string | (() => string)): [string, React.Dispatch<React.SetStateAction<string>>] (+1 overload)Returns a stateful value, and a function to update it.
useState("");
const const handleCreatePasskeyAndEmail: () => voidhandleCreatePasskeyAndEmail = () => {
// Important: Validate the email before proceeding
if (!const isValidEmail: (email: string) => booleanisValidEmail(const email: stringemail)) {
// Handle validation error
return;
}
try {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate({
type: "passkey"type: "passkey",
email: stringemail,
});
// Prompt the user to create a passkey, and create an account once they do.
} catch (function (local var) e: unknowne) {
const Alert: AlertStaticAlert.AlertStatic.alert: (title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions) => voidalert("Error creating passkey. Check logs for more details.");
var console: ConsoleThe console
module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream. * A global console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without importing the node:console
module.
Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O
for more information.
Example using the global console
:
const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```
Example using the `Console` class:
```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);
myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)log("Error creating passkey: ", function (local var) e: unknowne);
}
};
return (
<class ViewView>
<class TextText>Enter Your Email to Create Account</class TextText>
<class ViewView>
<class TextInputTextInput
value?: string | undefinedThe value to show for the text input. TextInput is a controlled component, which means the native value will be forced to match this value prop if provided. For most uses this works great, but in some cases this may cause flickering
one common cause is preventing edits by keeping value the same. In addition to simply setting the same value, either set editable=false, or set/update maxLength to prevent unwanted edits without flicker.
value={const email: stringemail}
onChangeText?: ((text: string) => void) | undefinedCallback that is called when the text input's text changes. Changed text is passed as an argument to the callback handler.
onChangeText={(val: stringval) => const setEmail: (value: React.SetStateAction<string>) => voidsetEmail(val: stringval.String.toLowerCase(): stringConverts all the alphabetic characters in a string to lowercase.
toLowerCase())}
placeholder?: string | undefinedThe string that will be rendered before text input has been entered
placeholder="[email protected]"
/>
<const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable PressableProps.onPress?: ((event: GestureResponderEvent) => void) | null | undefinedCalled when a single tap gesture is detected.
onPress={const handleCreatePasskeyAndEmail: () => voidhandleCreatePasskeyAndEmail}>
{({ pressed: booleanpressed }) => (
<class ViewView
style?: StyleProp<ViewStyle>style={[
{
ViewStyle.opacity?: AnimatableNumericValue | undefinedopacity: pressed: booleanpressed ? 0.5 : 1,
TransformsStyle.transform?: string | readonly (({
perspective: AnimatableNumericValue;
} & {
rotate?: undefined;
rotateX?: undefined;
rotateY?: undefined;
rotateZ?: undefined;
scale?: undefined;
scaleX?: undefined;
... 5 more ...;
matrix?: undefined;
}) | ... 11 more ... | ({
...;
} & {
...;
}))[] | undefinedtransform: [
{
scale: AnimatableNumericValuescale: pressed: booleanpressed ? 0.98 : 1,
},
],
},
]}
>
<class TextText>Sign In</class TextText>
</class ViewView>
)}
</const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable>
</class ViewView>
</class ViewView>
);
}
// Simple email validation function
const const isValidEmail: (email: string) => booleanisValidEmail = (email: stringemail: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.RegExp.test(string: string): booleanReturns a Boolean value that indicates whether or not a pattern exists in a searched string.
test(email: stringemail);
};
It’s important that you validate the email before creating an account for the user. This is to prevent users from losing access to their wallets if they lose their device.
Option 2: Creating a New Account
To create an account with a passkey, use the authenticate()
function, with the type
set to "passkey"
and createNew
set to true
.
import { function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.
This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.
useAuthenticate } from "@account-kit/react-native";
import React, { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
useState } from "react";
import { type Alert = AlertStatic
const Alert: AlertStaticAlert, class ViewView, class TextText, class TextInputTextInput, class ButtonButton, const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable } from "react-native";
function function CreatePasskey(): JSX.ElementCreatePasskey() {
const { const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>authenticate } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.
This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.
useAuthenticate();
const const handleCreatePasskey: () => voidhandleCreatePasskey = () => {
try {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate({
type: "passkey"type: "passkey",
createNew: truecreateNew: true,
// This will be the name of the saved passkey on the user's device.
username: stringusername: "Your App user",
});
} catch (function (local var) e: unknowne) {
const Alert: AlertStaticAlert.AlertStatic.alert: (title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions) => voidalert("Error creating passkey. Check logs for more details.");
var console: ConsoleThe console
module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream. * A global console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without importing the node:console
module.
Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O
for more information.
Example using the global console
:
const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```
Example using the `Console` class:
```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);
myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)log("Error creating passkey: ", function (local var) e: unknowne);
}
};
return (
<class ViewView>
<class TextText>Create an account with a passkey</class TextText>
<class ViewView>
<const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable PressableProps.onPress?: ((event: GestureResponderEvent) => void) | null | undefinedCalled when a single tap gesture is detected.
onPress={const handleCreatePasskey: () => voidhandleCreatePasskey}>
{({ pressed: booleanpressed }) => (
<class ViewView
style?: StyleProp<ViewStyle>style={[
{
ViewStyle.opacity?: AnimatableNumericValue | undefinedopacity: pressed: booleanpressed ? 0.5 : 1,
TransformsStyle.transform?: string | readonly (({
perspective: AnimatableNumericValue;
} & {
rotate?: undefined;
rotateX?: undefined;
rotateY?: undefined;
rotateZ?: undefined;
scale?: undefined;
scaleX?: undefined;
... 5 more ...;
matrix?: undefined;
}) | ... 11 more ... | ({
...;
} & {
...;
}))[] | undefinedtransform: [
{
scale: AnimatableNumericValuescale: pressed: booleanpressed ? 0.98 : 1,
},
],
},
]}
>
<class TextText>Create account</class TextText>
</class ViewView>
)}
</const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable>
</class ViewView>
</class ViewView>
);
}
Step 4: Sign In with a Passkey
To sign in with an existing passkey, use the authenticate()
function, with the type
set to "passkey"
.
import { function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.
This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.
useAuthenticate } from "@account-kit/react-native";
import React, { function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)Returns a stateful value, and a function to update it.
useState } from "react";
import { type Alert = AlertStatic
const Alert: AlertStaticAlert, class ViewView, class TextText, class TextInputTextInput, class ButtonButton, const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable } from "react-native";
function function SignInWithPasskey(): JSX.ElementSignInWithPasskey() {
const { const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>authenticate } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.
This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.
useAuthenticate();
const const handleSignIn: () => voidhandleSignIn = () => {
try {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate({
type: "passkey"type: "passkey",
createNew: falsecreateNew: false,
});
} catch (function (local var) e: unknowne) {
const Alert: AlertStaticAlert.AlertStatic.alert: (title: string, message?: string, buttons?: AlertButton[], options?: AlertOptions) => voidalert(
"Error signing in with passkey. Check logs for more details.",
);
var console: ConsoleThe console
module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.
The module exports two specific components:
- A
Console
class with methods such as console.log()
, console.error()
and console.warn()
that can be used to write to any Node.js stream. * A global console
instance configured to write to process.stdout
and process.stderr
. The global console
can be used without importing the node:console
module.
Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O
for more information.
Example using the global console
:
const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```
Example using the `Console` class:
```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);
myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)log("Error signing in with passkey: ", function (local var) e: unknowne);
}
};
return (
<class ViewView>
<class TextText>Sign in with passkey</class TextText>
<class ViewView>
<const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable PressableProps.onPress?: ((event: GestureResponderEvent) => void) | null | undefinedCalled when a single tap gesture is detected.
onPress={const handleSignIn: () => voidhandleSignIn}>
{({ pressed: booleanpressed }) => (
<class ViewView
style?: StyleProp<ViewStyle>style={[
{
ViewStyle.opacity?: AnimatableNumericValue | undefinedopacity: pressed: booleanpressed ? 0.5 : 1,
TransformsStyle.transform?: string | readonly (({
perspective: AnimatableNumericValue;
} & {
rotate?: undefined;
rotateX?: undefined;
rotateY?: undefined;
rotateZ?: undefined;
scale?: undefined;
scaleX?: undefined;
... 5 more ...;
matrix?: undefined;
}) | ... 11 more ... | ({
...;
} & {
...;
}))[] | undefinedtransform: [
{
scale: AnimatableNumericValuescale: pressed: booleanpressed ? 0.98 : 1,
},
],
},
]}
>
<class TextText>Sign In</class TextText>
</class ViewView>
)}
</const Pressable: React.ForwardRefExoticComponent<PressableProps & React.RefAttributes<View>>Pressable>
</class ViewView>
</class ViewView>
);
}