Skip to main content

better_auth_api/plugins/passkey/
mod.rs

1use better_auth_core::{AuthContext, AuthError, AuthResult};
2use better_auth_core::{AuthRequest, AuthResponse};
3
4mod callbacks;
5mod credential;
6mod options;
7mod registration;
8pub use callbacks::*;
9pub use options::*;
10
11pub(super) mod handlers;
12pub(super) mod types;
13pub(super) mod webauthn;
14
15#[cfg(test)]
16mod credential_tests;
17#[cfg(test)]
18mod tests;
19
20use handlers::*;
21use types::*;
22
23/// Passkey / WebAuthn authentication plugin.
24///
25/// Generates WebAuthn-compatible registration and authentication options,
26/// stores challenge state via the auth store, and manages passkey CRUD.
27pub struct PasskeyPlugin {
28    config: PasskeyConfig,
29}
30
31#[derive(Debug, Clone, better_auth_core::PluginConfig)]
32#[plugin(name = "PasskeyPlugin")]
33pub struct PasskeyConfig {
34    #[config(default = String::new())]
35    pub rp_id: String,
36    #[config(default = String::new())]
37    pub rp_name: String,
38    #[config(default = PasskeyOrigins::default(), skip)]
39    pub origin: PasskeyOrigins,
40    #[config(default = AuthenticatorSelection::default())]
41    pub authenticator_selection: AuthenticatorSelection,
42    #[config(default = "better-auth-passkey".to_owned())]
43    pub web_authn_challenge_cookie: String,
44    #[config(default = PasskeyRegistrationOptions::default())]
45    pub registration: PasskeyRegistrationOptions,
46    #[config(default = PasskeyAuthenticationOptions::default())]
47    pub authentication: PasskeyAuthenticationOptions,
48    #[config(default = 300)]
49    pub challenge_ttl_secs: i64,
50}
51
52// -- Plugin --
53
54impl PasskeyPlugin {
55    pub fn origin(mut self, origin: impl Into<PasskeyOrigins>) -> Self {
56        self.config.origin = origin.into();
57        self
58    }
59    // -- Handlers (delegate to core functions) --
60
61    /// GET /passkey/generate-register-options
62    async fn handle_generate_register_options(
63        &self,
64        req: &AuthRequest,
65        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
66    ) -> AuthResult<AuthResponse> {
67        let passkey_name = req.query_string("name")?;
68        let authenticator_attachment = req.query_string("authenticatorAttachment")?;
69        if authenticator_attachment
70            .is_some_and(|value| !matches!(value, "platform" | "cross-platform"))
71        {
72            return Err(AuthError::FieldInput { code: "VALIDATION_ERROR", message: "[query.authenticatorAttachment] Invalid option: expected one of \"platform\"|\"cross-platform\"".into() });
73        }
74        let user = registration::resolve_user(ctx, req, &self.config).await?;
75        let (result, cookie_header) = generate_register_options_core(
76            &user,
77            req,
78            passkey_name,
79            authenticator_attachment,
80            &self.config,
81            ctx,
82        )
83        .await?;
84        Ok(AuthResponse::json(200, &result)?.with_header("Set-Cookie", cookie_header))
85    }
86
87    /// POST /passkey/verify-registration
88    async fn handle_verify_registration(
89        &self,
90        req: &AuthRequest,
91        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
92    ) -> AuthResult<AuthResponse> {
93        let body = match VerifyRegistrationRequest::parse(req) {
94            Ok(v) => v,
95            Err(resp) => return Ok(resp),
96        };
97        let user = if self.config.registration.require_session {
98            registration::registration_session(ctx, req, &self.config).await?
99        } else {
100            None
101        };
102        match registration::verify_registration_core(body, req, user, &self.config, ctx).await? {
103            PasskeyHandlerOutcome::Success((result, token)) => {
104                let response = AuthResponse::json(200, &result)?;
105                if let Some(data) = token {
106                    ctx.session_manager()
107                        .set_session_cookie(req, data, None)
108                        .await?;
109                }
110                Ok(response)
111            }
112            PasskeyHandlerOutcome::Response(response) => Ok(response),
113        }
114    }
115
116    /// GET /passkey/generate-authenticate-options
117    async fn handle_generate_authenticate_options(
118        &self,
119        req: &AuthRequest,
120        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
121    ) -> AuthResult<AuthResponse> {
122        let maybe_user = registration::optional_session(ctx, req).await?;
123        let (result, cookie_header) =
124            generate_authenticate_options_core(maybe_user.as_ref(), req, &self.config, ctx).await?;
125        Ok(AuthResponse::json(200, &result)?.with_header("Set-Cookie", cookie_header))
126    }
127
128    /// POST /passkey/verify-authentication
129    async fn handle_verify_authentication(
130        &self,
131        req: &AuthRequest,
132        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
133    ) -> AuthResult<AuthResponse> {
134        let body = match VerifyAuthenticationRequest::parse(req) {
135            Ok(v) => v,
136            Err(resp) => return Ok(resp),
137        };
138        let ip_address = ctx.config.advanced.ip_address.resolve(req);
139        let user_agent = req.headers.get("user-agent").cloned();
140        match verify_authentication_core(&body, req, &self.config, ip_address, user_agent, ctx)
141            .await?
142        {
143            PasskeyHandlerOutcome::Success((response, data)) => {
144                ctx.session_manager()
145                    .set_session_cookie(req, data, None)
146                    .await?;
147                Ok(AuthResponse::json(200, &response)?)
148            }
149            PasskeyHandlerOutcome::Response(response) => Ok(response),
150        }
151    }
152
153    /// GET /passkey/list-user-passkeys
154    async fn handle_list_user_passkeys(
155        &self,
156        req: &AuthRequest,
157        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
158    ) -> AuthResult<AuthResponse> {
159        let (user, _session) = ctx.require_session(req).await?;
160        let result = list_user_passkeys_core(&user, ctx).await?;
161        AuthResponse::json(200, &result).map_err(AuthError::from)
162    }
163
164    /// POST /passkey/delete-passkey
165    async fn handle_delete_passkey(
166        &self,
167        req: &AuthRequest,
168        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
169    ) -> AuthResult<AuthResponse> {
170        let (user, _session) = ctx.require_session(req).await?;
171        let body: DeletePasskeyRequest = types::read(req, types::deletion_body)?;
172        let result = delete_passkey_core(&body, &user, ctx).await?;
173        AuthResponse::json(200, &result).map_err(AuthError::from)
174    }
175
176    /// POST /passkey/update-passkey
177    async fn handle_update_passkey(
178        &self,
179        req: &AuthRequest,
180        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
181    ) -> AuthResult<AuthResponse> {
182        let (user, _session) = ctx.require_session(req).await?;
183        let body: UpdatePasskeyRequest = types::read(req, types::update_body)?;
184        let result = update_passkey_core(&body, &user, ctx).await?;
185        AuthResponse::json(200, &result).map_err(AuthError::from)
186    }
187}
188
189better_auth_core::impl_auth_plugin! {
190    PasskeyPlugin, "passkey";
191    routes {
192        get  "/passkey/generate-register-options"      => handle_generate_register_options,      "generatePasskeyRegistrationOptions", query = crate::plugins::query_input::passkey_registration;
193        post "/passkey/verify-registration"            => handle_verify_registration,            "passkeyVerifyRegistration", body = types::registration_body;
194        get  "/passkey/generate-authenticate-options"  => handle_generate_authenticate_options,  "passkeyGenerateAuthenticateOptions";
195        post "/passkey/verify-authentication"          => handle_verify_authentication,          "passkeyVerifyAuthentication", body = types::authentication_body;
196        get  "/passkey/list-user-passkeys"             => handle_list_user_passkeys,             "listPasskeys";
197        post "/passkey/delete-passkey"                 => handle_delete_passkey,                 "deletePasskey", body = types::deletion_body;
198        post "/passkey/update-passkey"                 => handle_update_passkey,                 "updatePasskey", body = types::update_body;
199    }
200}