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
4use better_auth_core::utils::cookie_utils::create_session_cookie;
5
6pub(super) mod handlers;
7pub(super) mod types;
8pub(super) mod webauthn;
9
10#[cfg(test)]
11mod tests;
12
13use handlers::*;
14use types::*;
15
16/// Passkey / WebAuthn authentication plugin.
17///
18/// Generates WebAuthn-compatible registration and authentication options,
19/// stores challenge state via the auth store, and manages passkey CRUD.
20pub struct PasskeyPlugin {
21    config: PasskeyConfig,
22}
23
24#[derive(Debug, Clone, better_auth_core::PluginConfig)]
25#[plugin(name = "PasskeyPlugin")]
26pub struct PasskeyConfig {
27    #[config(default = String::new())]
28    pub rp_id: String,
29    #[config(default = "Better Auth".to_string())]
30    pub rp_name: String,
31    #[config(default = String::new())]
32    pub origin: String,
33    #[config(default = 300)]
34    pub challenge_ttl_secs: i64,
35}
36
37// -- Plugin --
38
39impl PasskeyPlugin {
40    // -- Handlers (delegate to core functions) --
41
42    /// GET /passkey/generate-register-options
43    async fn handle_generate_register_options(
44        &self,
45        req: &AuthRequest,
46        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
47    ) -> AuthResult<AuthResponse> {
48        let (user, _session) = ctx.require_session(req).await?;
49        let passkey_name = req.query.get("name").map(|s| s.as_str());
50        let authenticator_attachment = req.query.get("authenticatorAttachment").map(|s| s.as_str());
51        let (result, cookie_header) = generate_register_options_core(
52            &user,
53            passkey_name,
54            authenticator_attachment,
55            &self.config,
56            ctx,
57        )
58        .await?;
59        Ok(AuthResponse::json(200, &result)?.with_header("Set-Cookie", cookie_header))
60    }
61
62    /// POST /passkey/verify-registration
63    async fn handle_verify_registration(
64        &self,
65        req: &AuthRequest,
66        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
67    ) -> AuthResult<AuthResponse> {
68        let (user, _session) = ctx.require_session(req).await?;
69        let body: VerifyRegistrationRequest = match better_auth_core::validate_request_body(req) {
70            Ok(v) => v,
71            Err(resp) => return Ok(resp),
72        };
73        match verify_registration_core(&body, req, &user, &self.config, ctx).await? {
74            PasskeyHandlerOutcome::Success(result) => {
75                AuthResponse::json(200, &result).map_err(AuthError::from)
76            }
77            PasskeyHandlerOutcome::Response(response) => Ok(response),
78        }
79    }
80
81    /// GET /passkey/generate-authenticate-options
82    async fn handle_generate_authenticate_options(
83        &self,
84        req: &AuthRequest,
85        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
86    ) -> AuthResult<AuthResponse> {
87        let maybe_user = ctx.require_session(req).await.ok().map(|(u, _)| u);
88        let (result, cookie_header) =
89            generate_authenticate_options_core(maybe_user.as_ref(), &self.config, ctx).await?;
90        Ok(AuthResponse::json(200, &result)?.with_header("Set-Cookie", cookie_header))
91    }
92
93    /// POST /passkey/verify-authentication
94    async fn handle_verify_authentication(
95        &self,
96        req: &AuthRequest,
97        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
98    ) -> AuthResult<AuthResponse> {
99        let body: VerifyAuthenticationRequest = match better_auth_core::validate_request_body(req) {
100            Ok(v) => v,
101            Err(resp) => return Ok(resp),
102        };
103        let ip_address = req.headers.get("x-forwarded-for").cloned();
104        let user_agent = req.headers.get("user-agent").cloned();
105        match verify_authentication_core(&body, req, &self.config, ip_address, user_agent, ctx)
106            .await?
107        {
108            PasskeyHandlerOutcome::Success((response, token)) => {
109                let cookie_header = create_session_cookie(&token, &ctx.config);
110                Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
111            }
112            PasskeyHandlerOutcome::Response(response) => Ok(response),
113        }
114    }
115
116    /// GET /passkey/list-user-passkeys
117    async fn handle_list_user_passkeys(
118        &self,
119        req: &AuthRequest,
120        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
121    ) -> AuthResult<AuthResponse> {
122        let (user, _session) = ctx.require_session(req).await?;
123        let result = list_user_passkeys_core(&user, ctx).await?;
124        AuthResponse::json(200, &result).map_err(AuthError::from)
125    }
126
127    /// POST /passkey/delete-passkey
128    async fn handle_delete_passkey(
129        &self,
130        req: &AuthRequest,
131        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
132    ) -> AuthResult<AuthResponse> {
133        let (user, _session) = ctx.require_session(req).await?;
134        let body: DeletePasskeyRequest = match better_auth_core::validate_request_body(req) {
135            Ok(v) => v,
136            Err(resp) => return Ok(resp),
137        };
138        let result = delete_passkey_core(&body, &user, ctx).await?;
139        AuthResponse::json(200, &result).map_err(AuthError::from)
140    }
141
142    /// POST /passkey/update-passkey
143    async fn handle_update_passkey(
144        &self,
145        req: &AuthRequest,
146        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
147    ) -> AuthResult<AuthResponse> {
148        let (user, _session) = ctx.require_session(req).await?;
149        let body: UpdatePasskeyRequest = match better_auth_core::validate_request_body(req) {
150            Ok(v) => v,
151            Err(resp) => return Ok(resp),
152        };
153        let result = update_passkey_core(&body, &user, ctx).await?;
154        AuthResponse::json(200, &result).map_err(AuthError::from)
155    }
156}
157
158better_auth_core::impl_auth_plugin! {
159    PasskeyPlugin, "passkey";
160    routes {
161        get  "/passkey/generate-register-options"      => handle_generate_register_options,      "passkey_generate_register_options";
162        post "/passkey/verify-registration"            => handle_verify_registration,            "passkey_verify_registration";
163        get  "/passkey/generate-authenticate-options"  => handle_generate_authenticate_options,  "passkey_generate_authenticate_options";
164        post "/passkey/verify-authentication"          => handle_verify_authentication,          "passkey_verify_authentication";
165        get  "/passkey/list-user-passkeys"             => handle_list_user_passkeys,             "passkey_list_user_passkeys";
166        post "/passkey/delete-passkey"                 => handle_delete_passkey,                 "passkey_delete_passkey";
167        post "/passkey/update-passkey"                 => handle_update_passkey,                 "passkey_update_passkey";
168    }
169}