Passkeys are cryptographic key pairs generated and stored on secure hardware. Typically this is your Mac's secure enclave, your Android phone's Titan M2 chip, or a security key plugged in via USB.
- Registration ("sign up") creates a new key pair
- Authentication ("sign in") uses that key pair to sign a message with the private key
Webauthn has been a standard for quite a long time but it wasn't until a couple of years ago that "passkeys" came on. What makes them different? At their core they're "just" webauthn-compatible key pairs. One aspect is syncing:
- When creating a passkey and store it on your Chrome profile, you can use that passkey on any device you're logged into (with the same Chrome profile)
- When creating a passkey and store it on your iCloud Keychain, you have access to it from any Apple device you're logged into
- There's also cross-ecosystem bridges: you can use passkeys cross-device with a QR code flow
- There's also cross-ecosystem escape hatches: iOS lets you select the Chrome app as a source of passkeys, and Chrome lets you pick where you want to save your passkey. If you're on Mac you can store your Chrome-created passkey into the system's (iCloud-synced) keychain
What a world we live in!
Creating and using passkeys on the web is straightforward: browsers offer APIs to do it!
navigator.credentials.create
creates a passkeynavigator.credentials.get
prompts the user to select a passkey to sign a message
This doesn't require any backend! Proof/demo: https://passkeyapp.tkhqlabs.xyz/
An important security feature of passkeys: they're domain-bound, to prevent phishing. Passkeys created on passkeyapp.tkhqlabs.xyz
won't be usable on turnkey.com
for example. Browsers prevent this.
Android
On Android the CredentialManager
supports this with CreatePublicKeyCredentialRequest
and GetCredentialRequest
(see https://developer.android.com/training/sign-in/passkeys#sign-in)
iOS On iOS same deal: APIs to create and use passkeys are available.
ASAuthorizationPlatformPublicKeyCredentialProvider(…).createCredentialRegistrationRequest
for passkey creationASAuthorizationPlatformPublicKeyCredentialProvider(…).createCredentialAssertionRequest
for passkey usage
See these docs for more info. And this app for a mini demo.
Turnkey has a pretty cool TypeScript SDK: https://github.com/tkhq/sdk/tree/main/packages/http
It provides a type-safe client to call the Turnkey API. This won't work if you're working in Kotlin (Android) or Swift (iOS). There's no API client for these languages yet.
Enters: React Native, which lets you write your app in Typescript once and compile it into native code automatically!
And enters also: react-native-passkey, a library for react native applications to call the right native methods on ios and Android
If you're writing a React Native app, all you need to do is call the library's unified abstraction: Passkey.register
for passkey creation, and Passkey.authenticate
for usage. One API, nicer names. Win.
Demo: https://github.com/r-n-o/passkeyapp-bare is an app which does just that. It's built with React native, and it uses the library to create/use passkeys. No Turnkey integration though! :(
That's great but how do you call the Turnkey API? Enters (last character): stampers. They're abstract interfaces to sign Turnkey requests. We already have:
@turnkey/api-key-stamper
: to use API keys to sign requests@turnkey/iframe-stamper
: to use credentials within an iframe to sign requests@turnkey/webauthn-stamper
: to use webauthn (passkey) credentials to sign requests
Webauthn-stamper expects to have access to the web APIs (remember: navigator.credentials.create
, navigator.credentials.get
). These APIs aren't available in React Native environments because apps don't run in a browser, they're native apps!
So what do we need? A new stamper which wraps the react-native-passkey
API. Tadaaa: https://www.npmjs.com/package/@turnkey/react-native-passkey-stamper
Demo: let's run https://github.com/r-n-o/passkeyapp on the simulator!
I didn't expect this going in: passkeys on native apps aren't app-bound. They're domain bound as well! This is a bit annoying at first (you have to configure a web domain to use passkeys natively!), but it unlocks very interesting flows.
For example: I can create an app linked to wallet.tx.xyz domain to allow users to log into their account from mobile using the same passkey they used to create the account on the web.
And the other way around: we can use the native app to create a passkey and an associated sub-organization, and this passkey can be available on the web as long as it's synced properly!
Configuration is done separately per ecosystem, but surprisingly similar:
- iOS expects a JSON file at the domain root (
/.well-known /apple-app-site-association
) : example - Android expects a JSON file at the domain root (
/.well-known/assetlinks.json
): example
Demo: let's run https://github.com/r-n-o/passkeyapp on by actual device (unfortunately passkey syncing doesn't work in iOS simulators yet), and open up https://passkeyapp.tkhqlabs.xyz/!