better_auth_api/plugins/passkey/
mod.rs1use better_auth_core::adapters::DatabaseAdapter;
2use better_auth_core::{AuthContext, AuthError, AuthResult};
3use better_auth_core::{AuthRequest, AuthResponse};
4
5use better_auth_core::utils::cookie_utils::create_session_cookie;
6
7pub(super) mod handlers;
8pub(super) mod types;
9
10#[cfg(test)]
11mod tests;
12
13use handlers::*;
14use types::*;
15
16pub 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 = "localhost".to_string())]
35 pub rp_id: String,
36 #[config(default = "Better Auth".to_string())]
37 pub rp_name: String,
38 #[config(default = "http://localhost:3000".to_string())]
39 pub origin: String,
40 #[config(default = 300)]
41 pub challenge_ttl_secs: i64,
42 #[config(default = false)]
47 pub allow_insecure_unverified_assertion: bool,
48}
49
50impl PasskeyPlugin {
53 async fn handle_generate_register_options<DB: DatabaseAdapter>(
57 &self,
58 req: &AuthRequest,
59 ctx: &AuthContext<DB>,
60 ) -> AuthResult<AuthResponse> {
61 let (user, _session) = ctx.require_session(req).await?;
62 let authenticator_attachment = req.query.get("authenticatorAttachment").map(|s| s.as_str());
63 let result =
64 generate_register_options_core(&user, authenticator_attachment, &self.config, ctx)
65 .await?;
66 AuthResponse::json(200, &result).map_err(AuthError::from)
67 }
68
69 async fn handle_verify_registration<DB: DatabaseAdapter>(
71 &self,
72 req: &AuthRequest,
73 ctx: &AuthContext<DB>,
74 ) -> AuthResult<AuthResponse> {
75 let (user, _session) = ctx.require_session(req).await?;
76 let body: VerifyRegistrationRequest = match better_auth_core::validate_request_body(req) {
77 Ok(v) => v,
78 Err(resp) => return Ok(resp),
79 };
80 let result = verify_registration_core(&body, &user, &self.config, ctx).await?;
81 AuthResponse::json(200, &result).map_err(AuthError::from)
82 }
83
84 async fn handle_generate_authenticate_options<DB: DatabaseAdapter>(
86 &self,
87 req: &AuthRequest,
88 ctx: &AuthContext<DB>,
89 ) -> AuthResult<AuthResponse> {
90 let maybe_user = ctx.require_session(req).await.ok().map(|(u, _)| u);
91 let result =
92 generate_authenticate_options_core(maybe_user.as_ref(), &self.config, ctx).await?;
93 AuthResponse::json(200, &result).map_err(AuthError::from)
94 }
95
96 async fn handle_verify_authentication<DB: DatabaseAdapter>(
98 &self,
99 req: &AuthRequest,
100 ctx: &AuthContext<DB>,
101 ) -> AuthResult<AuthResponse> {
102 let body: VerifyAuthenticationRequest = match better_auth_core::validate_request_body(req) {
103 Ok(v) => v,
104 Err(resp) => return Ok(resp),
105 };
106 let ip_address = req.headers.get("x-forwarded-for").cloned();
107 let user_agent = req.headers.get("user-agent").cloned();
108 let (response, token) =
109 verify_authentication_core(&body, &self.config, ip_address, user_agent, ctx).await?;
110 let cookie_header = create_session_cookie(&token, &ctx.config);
111 Ok(AuthResponse::json(200, &response)?.with_header("Set-Cookie", cookie_header))
112 }
113
114 async fn handle_list_user_passkeys<DB: DatabaseAdapter>(
116 &self,
117 req: &AuthRequest,
118 ctx: &AuthContext<DB>,
119 ) -> AuthResult<AuthResponse> {
120 let (user, _session) = ctx.require_session(req).await?;
121 let result = list_user_passkeys_core(&user, ctx).await?;
122 AuthResponse::json(200, &result).map_err(AuthError::from)
123 }
124
125 async fn handle_delete_passkey<DB: DatabaseAdapter>(
127 &self,
128 req: &AuthRequest,
129 ctx: &AuthContext<DB>,
130 ) -> AuthResult<AuthResponse> {
131 let (user, _session) = ctx.require_session(req).await?;
132 let body: DeletePasskeyRequest = match better_auth_core::validate_request_body(req) {
133 Ok(v) => v,
134 Err(resp) => return Ok(resp),
135 };
136 let result = delete_passkey_core(&body, &user, ctx).await?;
137 AuthResponse::json(200, &result).map_err(AuthError::from)
138 }
139
140 async fn handle_update_passkey<DB: DatabaseAdapter>(
142 &self,
143 req: &AuthRequest,
144 ctx: &AuthContext<DB>,
145 ) -> AuthResult<AuthResponse> {
146 let (user, _session) = ctx.require_session(req).await?;
147 let body: UpdatePasskeyRequest = match better_auth_core::validate_request_body(req) {
148 Ok(v) => v,
149 Err(resp) => return Ok(resp),
150 };
151 let result = update_passkey_core(&body, &user, ctx).await?;
152 AuthResponse::json(200, &result).map_err(AuthError::from)
153 }
154}
155
156better_auth_core::impl_auth_plugin! {
157 PasskeyPlugin, "passkey";
158 routes {
159 get "/passkey/generate-register-options" => handle_generate_register_options, "passkey_generate_register_options";
160 post "/passkey/verify-registration" => handle_verify_registration, "passkey_verify_registration";
161 post "/passkey/generate-authenticate-options" => handle_generate_authenticate_options, "passkey_generate_authenticate_options";
162 post "/passkey/verify-authentication" => handle_verify_authentication, "passkey_verify_authentication";
163 get "/passkey/list-user-passkeys" => handle_list_user_passkeys, "passkey_list_user_passkeys";
164 post "/passkey/delete-passkey" => handle_delete_passkey, "passkey_delete_passkey";
165 post "/passkey/update-passkey" => handle_update_passkey, "passkey_update_passkey";
166 }
167}
168
169#[cfg(feature = "axum")]
174mod axum_impl {
175 use super::*;
176 use std::sync::Arc;
177
178 use axum::Json;
179 use axum::extract::{Extension, Query, State};
180 use axum::http::HeaderMap;
181 use axum::http::header;
182 use axum::response::IntoResponse;
183 use better_auth_core::error::AuthError;
184 use better_auth_core::extractors::{CurrentSession, OptionalSession, ValidatedJson};
185 use better_auth_core::plugin::AuthState;
186
187 #[derive(Clone)]
188 struct PluginState {
189 config: PasskeyConfig,
190 }
191
192 async fn handle_generate_register_options<DB: DatabaseAdapter>(
193 State(state): State<AuthState<DB>>,
194 Extension(ps): Extension<Arc<PluginState>>,
195 CurrentSession { user, .. }: CurrentSession<DB>,
196 Query(params): Query<RegisterOptionsQuery>,
197 ) -> Result<Json<serde_json::Value>, AuthError> {
198 let ctx = state.to_context();
199 let result = generate_register_options_core(
200 &user,
201 params.authenticator_attachment.as_deref(),
202 &ps.config,
203 &ctx,
204 )
205 .await?;
206 Ok(Json(result))
207 }
208
209 async fn handle_verify_registration<DB: DatabaseAdapter>(
210 State(state): State<AuthState<DB>>,
211 Extension(ps): Extension<Arc<PluginState>>,
212 CurrentSession { user, .. }: CurrentSession<DB>,
213 ValidatedJson(body): ValidatedJson<VerifyRegistrationRequest>,
214 ) -> Result<Json<PasskeyView>, AuthError> {
215 let ctx = state.to_context();
216 let result = verify_registration_core(&body, &user, &ps.config, &ctx).await?;
217 Ok(Json(result))
218 }
219
220 async fn handle_generate_authenticate_options<DB: DatabaseAdapter>(
221 State(state): State<AuthState<DB>>,
222 Extension(ps): Extension<Arc<PluginState>>,
223 OptionalSession(maybe): OptionalSession<DB>,
224 ) -> Result<Json<serde_json::Value>, AuthError> {
225 let ctx = state.to_context();
226 let maybe_user = maybe.as_ref().map(|s| &s.user);
227 let result = generate_authenticate_options_core(maybe_user, &ps.config, &ctx).await?;
228 Ok(Json(result))
229 }
230
231 async fn handle_verify_authentication<DB: DatabaseAdapter>(
232 State(state): State<AuthState<DB>>,
233 Extension(ps): Extension<Arc<PluginState>>,
234 headers: HeaderMap,
235 ValidatedJson(body): ValidatedJson<VerifyAuthenticationRequest>,
236 ) -> Result<impl IntoResponse, AuthError> {
237 let ip = headers
238 .get("x-forwarded-for")
239 .and_then(|v| v.to_str().ok())
240 .map(String::from);
241 let ua = headers
242 .get("user-agent")
243 .and_then(|v| v.to_str().ok())
244 .map(String::from);
245 let ctx = state.to_context();
246 let (response, token) = verify_authentication_core(&body, &ps.config, ip, ua, &ctx).await?;
247 let cookie = state.session_cookie(&token);
248 Ok(([(header::SET_COOKIE, cookie)], Json(response)))
249 }
250
251 async fn handle_list_user_passkeys<DB: DatabaseAdapter>(
252 State(state): State<AuthState<DB>>,
253 CurrentSession { user, .. }: CurrentSession<DB>,
254 ) -> Result<Json<Vec<PasskeyView>>, AuthError> {
255 let ctx = state.to_context();
256 let result = list_user_passkeys_core(&user, &ctx).await?;
257 Ok(Json(result))
258 }
259
260 async fn handle_delete_passkey<DB: DatabaseAdapter>(
261 State(state): State<AuthState<DB>>,
262 CurrentSession { user, .. }: CurrentSession<DB>,
263 ValidatedJson(body): ValidatedJson<DeletePasskeyRequest>,
264 ) -> Result<Json<crate::plugins::StatusResponse>, AuthError> {
265 let ctx = state.to_context();
266 let result = delete_passkey_core(&body, &user, &ctx).await?;
267 Ok(Json(result))
268 }
269
270 async fn handle_update_passkey<DB: DatabaseAdapter>(
271 State(state): State<AuthState<DB>>,
272 CurrentSession { user, .. }: CurrentSession<DB>,
273 ValidatedJson(body): ValidatedJson<UpdatePasskeyRequest>,
274 ) -> Result<Json<PasskeyResponse>, AuthError> {
275 let ctx = state.to_context();
276 let result = update_passkey_core(&body, &user, &ctx).await?;
277 Ok(Json(result))
278 }
279
280 impl<DB: DatabaseAdapter> better_auth_core::AxumPlugin<DB> for PasskeyPlugin {
281 fn name(&self) -> &'static str {
282 "passkey"
283 }
284
285 fn router(&self) -> axum::Router<AuthState<DB>> {
286 use axum::routing::{get, post};
287
288 let plugin_state = Arc::new(PluginState {
289 config: self.config.clone(),
290 });
291 axum::Router::new()
292 .route(
293 "/passkey/generate-register-options",
294 get(handle_generate_register_options::<DB>),
295 )
296 .route(
297 "/passkey/verify-registration",
298 post(handle_verify_registration::<DB>),
299 )
300 .route(
301 "/passkey/generate-authenticate-options",
302 post(handle_generate_authenticate_options::<DB>),
303 )
304 .route(
305 "/passkey/verify-authentication",
306 post(handle_verify_authentication::<DB>),
307 )
308 .route(
309 "/passkey/list-user-passkeys",
310 get(handle_list_user_passkeys::<DB>),
311 )
312 .route("/passkey/delete-passkey", post(handle_delete_passkey::<DB>))
313 .route("/passkey/update-passkey", post(handle_update_passkey::<DB>))
314 .layer(Extension(plugin_state))
315 }
316 }
317}