Passkeys (WebAuthn)
Passkeys provide a highly secure, phishing-resistant alternative to passwords by utilizing public-key cryptography (WebAuthn). Authkestra supports Passkeys out of the box through the WebAuthnAuthMethod which is directly integrated into the Engine for seamless multi-factor and primary authentication flows.
Enabling Passkeys
Section titled “Enabling Passkeys”To use Passkeys, you must first enable the webauthn feature in your Cargo.toml:
[dependencies]authkestra-engine = { version = "0.9", features = ["webauthn"] }Configuration
Section titled “Configuration”Authkestra uses webauthn-rs to power passkey support. You’ll need to instantiate a WebauthnBuilder, configure your Relaying Party (RP) ID and Origin, and pass it into a WebAuthnAuthMethod instance when building your Engine.
use std::sync::Arc;use webauthn_rs::prelude::WebauthnBuilder;use url::Url;use authkestra::Authkestra;
let rp_id = "example.com";let origin = Url::parse("https://example.com").unwrap();
let webauthn = WebauthnBuilder::new(rp_id, &origin) .unwrap() .rp_name("Authkestra Demo") .build() .unwrap();
// `my_store` implements the `CredentialStore` trait.// `with_webauthn` is shorthand for `.with_auth_method(WebAuthnAuthMethod::new(..))`// and registers the method under the name `"webauthn"`.let engine = Authkestra::builder() .with_webauthn(Arc::new(webauthn), my_store) .build();A runnable example combining passkeys with TOTP lives in the repository:
cargo run -p authkestra --example axum_mfa_server --all-featuresRegistration Ceremony
Section titled “Registration Ceremony”Registration is also a two-step ceremony, driven directly on the WebAuthnAuthMethod:
use authkestra_engine::auth::webauthn::WebAuthnAuthMethod;
let method = WebAuthnAuthMethod::new(Arc::new(webauthn), my_store);
// 1. Start: return `challenge` to the browser for `navigator.credentials.create()`,// and stash `reg_state` in the user's session.let (challenge, reg_state) = method.start_register("user_123", "ada")?;
// 2. Finish: verify the attestation and persist the passkey via the CredentialStore.let passkey = method.finish_register("user_123", reg_response, reg_state).await?;User handles
Section titled “User handles”user.id in the creation options is the user handle. The authenticator stores it inside the
credential and returns it as response.userHandle on every later assertion, which is what makes
discoverable (“usernameless”) sign-in able to resolve an account. It must be stable for the lifetime
of the account and map back to exactly one user.
start_register derives the handle from user_id with derive_user_handle:
- a
user_idthat already parses as a UUID is used verbatim; - anything else (CUID, ULID, prefixed id, integer) is hashed into a UUIDv5 over the documented
USER_HANDLE_NAMESPACEconstant.
Because the derivation is a plain UUIDv5, you can reproduce the same handle in any language to index your own users by handle.
If your application allocates handles itself — or needs a display name distinct from the username,
since start_register passes username for both — use start_register_with_handle:
use authkestra_engine::auth::webauthn::derive_user_handle;
let handle = derive_user_handle("user_123"); // or your own stable handlelet (challenge, reg_state) = method.start_register_with_handle(handle, "ada", "Ada Lovelace")?;Authentication Ceremony
Section titled “Authentication Ceremony”WebAuthn uses a two-step “ceremony”:
- Start: The server generates a cryptographic challenge and temporarily stores the session state.
- Finish: The client (browser) signs the challenge with the authenticator and returns the signature to the server. The server verifies the signature and completes the authentication via the unified
Engine::authenticatemethod.
Starting Authentication
Section titled “Starting Authentication”When the user wants to sign in, call start_authentication. You must pass the user’s previously registered Passkey objects (retrieved from your credential store). Engine::start_webauthn is the usual entry point; to call it directly on a WebAuthnAuthMethod you must bring the authkestra_engine::auth::WebAuthnStarter trait into scope, since that is where the method lives:
// `passkeys` is a `Vec<webauthn_rs::prelude::Passkey>` loaded from your storelet (challenge_response, auth_state) = engine.start_webauthn(&passkeys)?;You return the challenge_response (which contains the PublicKeyCredentialRequestOptions) to the client so it can invoke navigator.credentials.get(). You must also temporarily store the auth_state (the internal session state) associated with this login attempt (e.g. in a session or Redis).
Finishing Authentication
Section titled “Finishing Authentication”Once the client returns the signed assertion, pass it to the engine’s authenticate method using the AuthInput::WebAuthnAuthentication variant. The engine expects the auth_state_json (the serialized internal session state created during start_authentication) to be provided.
use authkestra_engine::auth::{AuthInput, AuthResult};
let result = engine.authenticate(AuthInput::WebAuthnAuthentication { // The internal user ID you are trying to authenticate user_id: "user_123".to_string(), // `id` from the PublicKeyCredential response credential_id: "base64_url_credential_id".to_string(), // `response.clientDataJSON` from the PublicKeyCredential response, base64url-encoded client_data_json: "base64_url_client_data".to_string(), // `response.authenticatorData` from the PublicKeyCredential response, base64url-encoded authenticator_data: "base64_url_auth_data".to_string(), // `response.signature` from the PublicKeyCredential response, base64url-encoded signature: "base64_url_signature".to_string(), // `response.userHandle` from the PublicKeyCredential response, base64url-encoded (optional) user_handle: None, // Or Some("user_handle") if returned // The state string stored during `start_webauthn`, retrieved from your session/cache store auth_state_json: Some(auth_state_json_string),}).await?;
match result { AuthResult::Success(identity) => { println!("Successfully authenticated as: {}", identity.external_id); } AuthResult::MfaRequired { mfa_token, allowed_methods, .. } => { // Handle step-up authentication if they need a second factor! }}Authkestra will perform rigorous cryptographic validation of the assertion against the stored public key, update the signature counter in the credential store to detect cloned authenticators, and return the authenticated Identity if successful!