1use std::{future::Future, pin::Pin, sync::Arc};
2
3use better_auth_core::utils::password::{hash_password, verify_password};
4use better_auth_core::wire::UserView;
5use better_auth_core::{
6 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, AuthSchema, AuthSession,
7 AuthUser, CreateAccount, CreateUser, CreateVerification, RequestMeta, UpdateAccount,
8 UpdateUser,
9};
10use chrono::{Duration, Utc};
11use rand::Rng;
12use serde_json::{Value, json};
13
14use super::helpers::{SessionIssueError, get_credential_account, issue_user_session_with_lifetime};
15
16mod callbacks;
17mod native;
18mod request;
19use crate::plugins::endpoint_context::EndpointContext;
20use callbacks::Delivery;
21pub use callbacks::{PhoneCallbackFuture, PhoneNumberCallbacks};
22pub use native::PhoneNumberApi;
23
24type CallbackFuture<T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send>>;
25type OtpSender = dyn Fn(PhoneOtp, AuthRequest) -> CallbackFuture<()> + Send + Sync;
26type OtpVerifier = dyn Fn(PhoneOtp, AuthRequest) -> CallbackFuture<bool> + Send + Sync;
27type Validator = dyn Fn(String) -> CallbackFuture<bool> + Send + Sync;
28type VerifiedCallback = dyn Fn(PhoneVerification, AuthRequest) -> CallbackFuture<()> + Send + Sync;
29type TempField = dyn Fn(&str) -> String + Send + Sync;
30
31#[derive(Debug, Clone, serde::Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct PhoneOtp {
35 pub phone_number: String,
37 pub code: String,
39}
40#[derive(Debug, Clone)]
42pub struct PhoneVerification {
43 pub phone_number: String,
45 pub user: UserView,
47}
48
49#[derive(Clone)]
51pub struct PhoneNumberPlugin {
52 send_otp: Option<Arc<OtpSender>>,
53 verify_otp: Option<Arc<OtpVerifier>>,
54 send_password_reset_otp: Option<Arc<OtpSender>>,
55 phone_number_validator: Option<Arc<Validator>>,
56 callback_on_verification: Option<Arc<VerifiedCallback>>,
57 temp_email: Option<Arc<TempField>>,
58 temp_name: Option<Arc<TempField>>,
59 expires_in: f64,
60 otp_length: usize,
61 allowed_attempts: u64,
62 require_verification: bool,
63}
64impl Default for PhoneNumberPlugin {
65 fn default() -> Self {
66 Self {
67 send_otp: None,
68 verify_otp: None,
69 send_password_reset_otp: None,
70 phone_number_validator: None,
71 callback_on_verification: None,
72 temp_email: None,
73 temp_name: None,
74 expires_in: 300.0,
75 otp_length: 6,
76 allowed_attempts: 3,
77 require_verification: false,
78 }
79 }
80}
81impl PhoneNumberPlugin {
82 pub fn new() -> Self {
84 Self::default()
85 }
86 pub fn otp_length(mut self, length: usize) -> Self {
88 self.otp_length = length;
89 self
90 }
91 pub fn expires_in(mut self, seconds: f64) -> Self {
94 self.expires_in = seconds;
95 self
96 }
97 pub fn allowed_attempts(mut self, attempts: u64) -> Self {
99 self.allowed_attempts = attempts;
100 self
101 }
102 pub fn require_verification(mut self, require: bool) -> Self {
104 self.require_verification = require;
105 self
106 }
107 pub fn sign_up_on_verification(
109 mut self,
110 email: impl Fn(&str) -> String + Send + Sync + 'static,
111 ) -> Self {
112 self.temp_email = Some(Arc::new(email));
113 self
114 }
115 pub fn temporary_name(mut self, name: impl Fn(&str) -> String + Send + Sync + 'static) -> Self {
117 self.temp_name = Some(Arc::new(name));
118 self
119 }
120 pub fn send_otp<F, Fut>(mut self, callback: F) -> Self
122 where
123 F: Fn(PhoneOtp, AuthRequest) -> Fut + Send + Sync + 'static,
124 Fut: Future<Output = AuthResult<()>> + Send + 'static,
125 {
126 self.send_otp = Some(Arc::new(move |otp, req| Box::pin(callback(otp, req))));
127 self
128 }
129 pub fn send_password_reset_otp<F, Fut>(mut self, callback: F) -> Self
131 where
132 F: Fn(PhoneOtp, AuthRequest) -> Fut + Send + Sync + 'static,
133 Fut: Future<Output = AuthResult<()>> + Send + 'static,
134 {
135 self.send_password_reset_otp = Some(Arc::new(move |otp, req| Box::pin(callback(otp, req))));
136 self
137 }
138 pub fn verify_otp<F, Fut>(mut self, callback: F) -> Self
140 where
141 F: Fn(PhoneOtp, AuthRequest) -> Fut + Send + Sync + 'static,
142 Fut: Future<Output = AuthResult<bool>> + Send + 'static,
143 {
144 self.verify_otp = Some(Arc::new(move |otp, req| Box::pin(callback(otp, req))));
145 self
146 }
147 pub fn phone_number_validator<F, Fut>(mut self, callback: F) -> Self
149 where
150 F: Fn(String) -> Fut + Send + Sync + 'static,
151 Fut: Future<Output = AuthResult<bool>> + Send + 'static,
152 {
153 self.phone_number_validator = Some(Arc::new(move |phone| Box::pin(callback(phone))));
154 self
155 }
156 pub fn callback_on_verification<F, Fut>(mut self, callback: F) -> Self
158 where
159 F: Fn(PhoneVerification, AuthRequest) -> Fut + Send + Sync + 'static,
160 Fut: Future<Output = AuthResult<()>> + Send + 'static,
161 {
162 self.callback_on_verification =
163 Some(Arc::new(move |data, req| Box::pin(callback(data, req))));
164 self
165 }
166
167 async fn validate(&self, phone: &str) -> AuthResult<()> {
168 if let Some(validate) = &self.phone_number_validator
169 && !validate(phone.to_owned()).await?
170 {
171 return Err(error(400, "INVALID_PHONE_NUMBER", "Invalid phone number"));
172 }
173 Ok(())
174 }
175 async fn save_otp(
176 &self,
177 ctx: &AuthContext<impl AuthSchema>,
178 identifier: String,
179 with_attempts: bool,
180 ) -> AuthResult<String> {
181 let code: String = (0..self.otp_length)
182 .map(|_| char::from(b'0' + rand::thread_rng().gen_range(0..10)))
183 .collect();
184 let _ = ctx
185 .database
186 .create_verification(CreateVerification {
187 identifier: (identifier).into(),
188 value: (if with_attempts {
189 format!("{code}:0")
190 } else {
191 code.clone()
192 })
193 .into(),
194 expires_at: better_auth_core::utils::date::from_milliseconds(
195 Utc::now().timestamp_millis() as f64 + self.expires_in * 1000.0,
196 )
197 .ok_or_else(|| AuthError::config("Phone OTP expiry is out of range"))?
198 .into(),
199 ..Default::default()
200 })
201 .await?;
202 Ok(code)
203 }
204 async fn verify_stored_otp(
205 &self,
206 endpoint: &EndpointContext<'_, impl AuthSchema>,
207 identifier: &str,
208 code: &str,
209 ) -> AuthResult<()> {
210 let ctx = endpoint.auth;
211 let existing = match endpoint.transaction {
212 Some(transaction) => {
213 transaction
214 .get_verification_including_expired(identifier)
215 .await?
216 }
217 None => {
218 ctx.database
219 .get_verification_including_expired(identifier)
220 .await?
221 }
222 }
223 .ok_or_else(|| error(400, "OTP_NOT_FOUND", "OTP not found"))?;
224 if existing.expires_at.is_before(Utc::now())? {
225 native::delete_verification(endpoint, identifier).await?;
226 return Err(error(400, "OTP_EXPIRED", "OTP expired"));
227 }
228 if attempts(existing.value.typed()?) >= self.allowed_attempts {
229 native::delete_verification(endpoint, identifier).await?;
230 return Err(error(403, "TOO_MANY_ATTEMPTS", "Too many attempts"));
231 }
232 let consumed = match endpoint.transaction {
233 Some(transaction) => {
234 transaction
235 .consume_verification_by_identifier(identifier)
236 .await?
237 }
238 None => {
239 ctx.database
240 .consume_verification_by_identifier(identifier)
241 .await?
242 }
243 }
244 .ok_or_else(|| error(400, "INVALID_OTP", "Invalid OTP"))?;
245 let count = attempts(consumed.value.typed()?);
246 if count >= self.allowed_attempts {
247 return Err(error(403, "TOO_MANY_ATTEMPTS", "Too many attempts"));
248 }
249 let expected = consumed
250 .value
251 .typed()?
252 .split(':')
253 .next()
254 .unwrap_or_default();
255 if expected != code {
256 let input = CreateVerification {
257 identifier: (identifier.to_owned()).into(),
258 value: (format!("{expected}:{}", count + 1)).into(),
259 expires_at: consumed.expires_at.clone(),
260 ..Default::default()
261 };
262 let _ = match endpoint.transaction {
263 Some(transaction) => transaction.create_verification(input).await?,
264 None => ctx.database.create_verification(input).await?,
265 };
266 return Err(error(400, "INVALID_OTP", "Invalid OTP"));
267 }
268 Ok(())
269 }
270 pub async fn consume_otp(
272 &self,
273 ctx: &AuthContext<impl AuthSchema>,
274 req: &AuthRequest,
275 otp: PhoneOtp,
276 ) -> AuthResult<()> {
277 let endpoint = EndpointContext::new(
278 Some(req),
279 better_auth_core::FieldValue::from_json(
280 json!({"phoneNumber":otp.phone_number,"code":otp.code}),
281 )?,
282 ctx,
283 );
284 self.consume_with_context(&endpoint, otp).await
285 }
286 async fn consume_with_context<S: AuthSchema>(
287 &self,
288 endpoint: &EndpointContext<'_, S>,
289 otp: PhoneOtp,
290 ) -> AuthResult<()> {
291 let ctx = endpoint.auth;
292 let verified = if let Some(verify) = ctx
293 .extensions
294 .get::<Arc<PhoneNumberCallbacks<S>>>()
295 .and_then(|callbacks| callbacks.verify.as_ref())
296 {
297 Some(verify(&otp, endpoint).await?)
298 } else if let Some(verify) = &self.verify_otp {
299 let request = endpoint
300 .request
301 .ok_or_else(|| AuthError::config("Legacy phone callbacks require a request"))?;
302 Some(verify(otp.clone(), request.clone()).await?)
303 } else {
304 None
305 };
306 match verified {
307 Some(false) => Err(error(400, "INVALID_OTP", "Invalid OTP")),
308 Some(true) => native::delete_verification(endpoint, &otp.phone_number).await,
309 None => {
310 self.verify_stored_otp(endpoint, &otp.phone_number, &otp.code)
311 .await
312 }
313 }
314 }
315 async fn send(
316 &self,
317 req: &AuthRequest,
318 ctx: &AuthContext<impl AuthSchema>,
319 ) -> AuthResult<AuthResponse> {
320 let body = match request::read(req) {
321 Ok(body) => body,
322 Err(response) => return Ok(response),
323 };
324 let phone = string(&body, "phoneNumber");
325 if !self.has_sender(ctx, Delivery::Verification) {
326 return Err(error(
327 501,
328 "SEND_OTP_NOT_IMPLEMENTED",
329 "sendOTP not implemented",
330 ));
331 }
332 let endpoint = EndpointContext::new(
333 Some(req),
334 better_auth_core::FieldValue::from_json(body.clone())?,
335 ctx,
336 );
337 self.validate(phone).await?;
338 let code = self.save_otp(ctx, phone.to_owned(), true).await?;
339 let task = self.delivery(
340 PhoneOtp {
341 phone_number: phone.to_owned(),
342 code,
343 },
344 &endpoint,
345 Delivery::Verification,
346 )?;
347 if ctx.config.advanced.background_tasks.is_some() {
348 better_auth_core::background::run_or_await(
349 task,
350 ctx.config.advanced.background_tasks.as_ref(),
351 &ctx.config.logger,
352 )
353 .await;
354 } else if let Some(task) = task {
355 task.await?;
357 }
358 Ok(AuthResponse::json(200, &json!({"message":"code sent"}))?)
359 }
360 async fn verify(
361 &self,
362 req: &AuthRequest,
363 ctx: &AuthContext<impl AuthSchema>,
364 ) -> AuthResult<AuthResponse> {
365 let body = match request::read(req) {
366 Ok(body) => body,
367 Err(response) => return Ok(response),
368 };
369 let phone = string(&body, "phoneNumber");
370 let mut endpoint = EndpointContext::new(
371 Some(req),
372 better_auth_core::FieldValue::from_json(body.clone())?,
373 ctx,
374 );
375 self.consume_with_context(
376 &endpoint,
377 PhoneOtp {
378 phone_number: phone.to_owned(),
379 code: string(&body, "code").to_owned(),
380 },
381 )
382 .await?;
383 let update_phone = body.get("updatePhoneNumber") == Some(&Value::Bool(true));
384 let existing_session = if update_phone {
385 Some(ctx.require_session(req).await.map_err(|error| {
386 if matches!(error, AuthError::Unauthenticated) {
387 self::error(401, "USER_NOT_FOUND", "User not found")
388 } else {
389 error
390 }
391 })?)
392 } else {
393 None
394 };
395 let found = ctx.database.get_user_by_phone_number(phone).await?;
396 let user = if let Some((user, _)) = &existing_session {
397 if found.is_some() {
398 return Err(error(
399 400,
400 "PHONE_NUMBER_EXIST",
401 "Phone number already exists",
402 ));
403 }
404 ctx.database
405 .update_user(
406 user.id.typed()?,
407 UpdateUser {
408 phone_number: Some(Some(phone.to_owned())),
409 phone_number_verified: Some(true),
410 ..Default::default()
411 },
412 )
413 .await?
414 } else if let Some(user) = found {
415 ctx.database
416 .update_user(
417 user.id().typed()?,
418 UpdateUser {
419 phone_number_verified: Some(true),
420 ..Default::default()
421 },
422 )
423 .await?
424 } else if let Some(email) = &self.temp_email {
425 let mut create = CreateUser::new().with_email(email(phone)).with_name(
426 self.temp_name
427 .as_ref()
428 .map_or_else(|| phone.to_owned(), |name| name(phone)),
429 );
430 let rest = body
431 .as_object()
432 .into_iter()
433 .flatten()
434 .filter(|(key, _)| {
435 !["phoneNumber", "code", "disableSession", "updatePhoneNumber"]
436 .contains(&key.as_str())
437 })
438 .map(|(key, value)| (key.clone(), value.clone()))
439 .collect();
440 super::helpers::apply_user_create_fields(ctx, &rest, &mut create)?;
441 create.phone_number = Some(phone.to_owned());
442 create.phone_number_verified = Some(true);
443
444 super::user_admission::create_user(create, "phone-number", &endpoint).await?
445 } else {
446 return Err(error(500, "FAILED_TO_UPDATE_USER", "Failed to update user"));
447 };
448 endpoint.session = existing_session.clone();
449 self.notify_verified(
450 PhoneVerification {
451 phone_number: phone.to_owned(),
452 user: ctx.internal_user_view(&user).await?,
453 },
454 &endpoint,
455 )
456 .await?;
457 if let Some((_, session)) = existing_session {
458 return Ok(AuthResponse::json(
459 200,
460 &json!({"status":true,"token":session.token,"user":ctx.user_view(&user).await?}),
461 )?);
462 }
463 if body.get("disableSession") == Some(&Value::Bool(true)) {
464 return Ok(AuthResponse::json(
465 200,
466 &json!({"status":true,"token":null,"user":ctx.user_view(&user).await?}),
467 )?);
468 }
469 self.session_response(ctx, req, &user, false, true).await
470 }
471 async fn sign_in(
472 &self,
473 req: &AuthRequest,
474 ctx: &AuthContext<impl AuthSchema>,
475 ) -> AuthResult<AuthResponse> {
476 let body = match request::read(req) {
477 Ok(body) => body,
478 Err(response) => return Ok(response),
479 };
480 let phone = string(&body, "phoneNumber");
481 let password = string(&body, "password");
482 self.validate(phone).await?;
483 check_password_length(ctx, password, false)?;
484 let user = ctx
485 .database
486 .get_user_by_phone_number(phone)
487 .await?
488 .ok_or_else(invalid_credentials)?;
489 if self.require_verification && user.phone_number_verified() != Some(true) {
490 let code = self.save_otp(ctx, phone.to_owned(), false).await?;
491 if self.has_sender(ctx, Delivery::Verification) {
492 let endpoint = EndpointContext::new(
493 Some(req),
494 better_auth_core::FieldValue::from_json(body.clone())?,
495 ctx,
496 );
497 let task = self.delivery(
498 PhoneOtp {
499 phone_number: phone.to_owned(),
500 code,
501 },
502 &endpoint,
503 Delivery::Verification,
504 )?;
505 better_auth_core::background::run_or_await(
506 task,
507 ctx.config.advanced.background_tasks.as_ref(),
508 &ctx.config.logger,
509 )
510 .await;
511 }
512 return Err(error(
513 401,
514 "PHONE_NUMBER_NOT_VERIFIED",
515 "Phone number not verified",
516 ));
517 }
518 let account = get_credential_account(ctx, user.id().into_owned())
519 .await?
520 .ok_or_else(invalid_credentials)?;
521 if account.password.is_absent() {
522 return Err(error(401, "UNEXPECTED_ERROR", "Unexpected error"));
523 }
524 let hash = account
525 .password
526 .typed()?
527 .as_deref()
528 .ok_or_else(|| error(401, "UNEXPECTED_ERROR", "Unexpected error"))?;
529 match verify_password(ctx.password_policy.hasher.as_ref(), password, hash).await {
530 Err(AuthError::InvalidCredentials) => return Err(invalid_credentials()),
531 result => result?,
532 }
533 self.session_response(
534 ctx,
535 req,
536 &user,
537 body.get("rememberMe") == Some(&Value::Bool(false)),
538 false,
539 )
540 .await
541 }
542 async fn session_response<S: AuthSchema>(
543 &self,
544 ctx: &AuthContext<S>,
545 req: &AuthRequest,
546 user: &better_auth_core::wire::UserView,
547 dont_remember: bool,
548 status: bool,
549 ) -> AuthResult<AuthResponse> {
550 let meta = RequestMeta::from_request_with_config(req, &ctx.config.advanced.ip_address);
551 let lifetime = if dont_remember {
552 Duration::days(1)
553 } else {
554 ctx.config.session.expires_in()
555 };
556 let issued = issue_user_session_with_lifetime(
557 ctx,
558 user.id().typed()?,
559 meta.ip_address,
560 meta.user_agent,
561 lifetime,
562 )
563 .await
564 .map_err(SessionIssueError::into_auth_error)?;
565 let mut output = serde_json::Map::from_iter([
566 (String::from("token"), json!(issued.session.token())),
567 (
568 String::from("user"),
569 json!(ctx.user_view(&issued.user).await?),
570 ),
571 ]);
572 if status {
573 let _ = output.insert("status".into(), json!(true));
574 }
575 let manager = ctx.session_manager();
576 manager
577 .set_session_cookie(
578 req,
579 manager.internal_data(&issued.user, &issued.session).await?,
580 Some(dont_remember),
581 )
582 .await?;
583 Ok(AuthResponse::json(200, &output)?)
584 }
585 async fn request_reset(
586 &self,
587 req: &AuthRequest,
588 ctx: &AuthContext<impl AuthSchema>,
589 ) -> AuthResult<AuthResponse> {
590 let body = match request::read(req) {
591 Ok(body) => body,
592 Err(response) => return Ok(response),
593 };
594 let phone = string(&body, "phoneNumber");
595 let user = ctx.database.get_user_by_phone_number(phone).await?;
596 let code = self
597 .save_otp(ctx, format!("{phone}-request-password-reset"), true)
598 .await?;
599 if user.is_some() && self.has_sender(ctx, Delivery::PasswordReset) {
600 let endpoint = EndpointContext::new(
601 Some(req),
602 better_auth_core::FieldValue::from_json(body.clone())?,
603 ctx,
604 );
605 let task = self.delivery(
606 PhoneOtp {
607 phone_number: phone.to_owned(),
608 code,
609 },
610 &endpoint,
611 Delivery::PasswordReset,
612 )?;
613 better_auth_core::background::run_or_await(
614 task,
615 ctx.config.advanced.background_tasks.as_ref(),
616 &ctx.config.logger,
617 )
618 .await;
619 }
620 Ok(AuthResponse::json(200, &json!({"status":true}))?)
621 }
622 async fn reset(
623 &self,
624 req: &AuthRequest,
625 ctx: &AuthContext<impl AuthSchema>,
626 ) -> AuthResult<AuthResponse> {
627 let body = match request::read(req) {
628 Ok(body) => body,
629 Err(response) => return Ok(response),
630 };
631 let phone = string(&body, "phoneNumber");
632 let password = string(&body, "newPassword");
633 self.verify_stored_otp(
634 &EndpointContext::new(
635 Some(req),
636 better_auth_core::FieldValue::from_json(body.clone())?,
637 ctx,
638 ),
639 &format!("{phone}-request-password-reset"),
640 string(&body, "otp"),
641 )
642 .await?;
643 let user = ctx
644 .database
645 .get_user_by_phone_number(phone)
646 .await?
647 .ok_or_else(|| error(400, "UNEXPECTED_ERROR", "Unexpected error"))?;
648 check_password_length(ctx, password, true)?;
649 let hash = hash_password(ctx.password_policy.hasher.as_ref(), password).await?;
650 if let Some(account) = get_credential_account(ctx, user.id().into_owned()).await? {
651 let _ = ctx
652 .database
653 .update_account(
654 account.id.typed()?,
655 UpdateAccount {
656 password: (Some(hash))
657 .map(|value| better_auth_core::SchemaValue::Typed(Some(value)))
658 .unwrap_or_default(),
659 ..Default::default()
660 },
661 )
662 .await?;
663 } else {
664 let _ = ctx
665 .database
666 .create_account(CreateAccount {
667 user_id: user.id().into_owned(),
668 account_id: user.id().into_owned(),
669 provider_id: "credential".into(),
670 password: (Some(hash))
671 .map(|value| better_auth_core::SchemaValue::Typed(Some(value)))
672 .unwrap_or_default(),
673 access_token: Default::default(),
674 refresh_token: Default::default(),
675 id_token: Default::default(),
676 access_token_expires_at: Default::default(),
677 refresh_token_expires_at: Default::default(),
678 scope: Default::default(),
679 ..Default::default()
680 })
681 .await?;
682 }
683 if let Some(callback) = &ctx.password_policy.on_password_reset {
684 callback(better_auth_core::utils::password::PasswordResetEvent {
685 user: ctx.internal_user_view(&user).await?,
686 request: Some(req.clone()),
687 })
688 .await?;
689 }
690 if ctx.password_policy.revoke_sessions_on_password_reset {
691 ctx.database
692 .delete_user_sessions(user.id().typed()?)
693 .await?;
694 }
695 Ok(AuthResponse::json(200, &json!({"status":true}))?)
696 }
697}
698
699fn check_password_length(
700 ctx: &AuthContext<impl AuthSchema>,
701 password: &str,
702 check_min: bool,
703) -> AuthResult<()> {
704 if check_min && password.encode_utf16().count() < ctx.password_policy.min_length {
705 return Err(error(400, "PASSWORD_TOO_SHORT", "Password too short"));
706 }
707 if password.encode_utf16().count() > ctx.password_policy.max_length {
708 return Err(error(400, "PASSWORD_TOO_LONG", "Password too long"));
709 }
710 Ok(())
711}
712fn attempts(value: &str) -> u64 {
713 value
714 .split(':')
715 .nth(1)
716 .and_then(|value| value.parse().ok())
717 .filter(|count| *count <= 9_007_199_254_740_991)
718 .unwrap_or(0)
719}
720fn invalid_credentials() -> AuthError {
721 error(
722 401,
723 "INVALID_PHONE_NUMBER_OR_PASSWORD",
724 "Invalid phone number or password",
725 )
726}
727fn error(status: u16, code: &'static str, message: &'static str) -> AuthError {
728 AuthError::Upstream {
729 status,
730 code,
731 message,
732 }
733}
734fn string<'a>(body: &'a Value, key: &str) -> &'a str {
735 body.get(key).and_then(Value::as_str).unwrap_or_default()
736}
737
738better_auth_core::impl_auth_plugin!(PhoneNumberPlugin, "phone-number";
739 routes {
740 post "/sign-in/phone-number" => sign_in, "signInPhoneNumber", body = request::validate;
741 post "/phone-number/send-otp" => send, "sendPhoneNumberOTP", body = request::validate;
742 post "/phone-number/verify" => verify, "verifyPhoneNumber", body = request::validate;
743 post "/phone-number/request-password-reset" => request_reset, "requestPasswordResetPhoneNumber", body = request::validate;
744 post "/phone-number/reset-password" => reset, "resetPasswordPhoneNumber", body = request::validate;
745 }
746 extra {
747 async fn on_init(
748 &self,
749 ctx: &mut better_auth_core::AuthInitContext<S>,
750 ) -> AuthResult<()> {
751 S::User::require_plugin_fields(
752 "phone-number",
753 &["phone_number", "phone_number_verified"],
754 )?;
755 ctx.set_metadata("phone-number.enabled", json!(true));
756 ctx.extensions.insert(self.clone());
757 Ok(())
758 }
759 fn rate_limits(
760 &self,
761 ) -> AuthResult<Vec<better_auth_core::middleware::PluginRateLimit>> {
762 Ok(vec![better_auth_core::middleware::PluginRateLimit::prefix(
763 "/phone-number",
764 better_auth_core::middleware::EndpointRateLimit {
765 window: 60.0,
766 max_requests: 10.0,
767 },
768 )])
769 }
770 async fn before_request(
771 &self,
772 req: &AuthRequest,
773 _ctx: &AuthContext<S>,
774 ) -> AuthResult<Option<better_auth_core::BeforeRequestAction>> {
775 if req.path() == "/update-user" {
776 let body = match super::json_body::parse(req) {
777 Ok(body) => body,
778 Err(response) => {
779 return Ok(Some(better_auth_core::BeforeRequestAction::Respond(response)));
780 }
781 };
782 if body
783 .as_ref()
784 .and_then(|body| body.get("phoneNumber"))
785 .is_some_and(|value| !value.is_null())
786 {
787 return better_auth_core::observability::instrumentation::with_endpoint_hook(&_ctx.config, req, "before", "plugin:phone-number", async {
788 Err(error(400, "PHONE_NUMBER_CANNOT_BE_UPDATED", "Phone number cannot be updated"))
789 }).await;
790 }
791 }
792 Ok(None)
793 }
794 }
795);