better_auth_api/plugins/passkey/
mod.rs1use 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
23pub 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
52impl PasskeyPlugin {
55 pub fn origin(mut self, origin: impl Into<PasskeyOrigins>) -> Self {
56 self.config.origin = origin.into();
57 self
58 }
59 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 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 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 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 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 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 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}