1use crate::{
13 error::{Error, Result},
14 types::{SupabaseConfig, Timestamp},
15};
16use chrono::Utc;
17use reqwest::Client as HttpClient;
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::sync::{Arc, RwLock, Weak};
21use tracing::{debug, info, warn};
22use uuid::Uuid;
23
24use base32;
26use phonenumber::Mode;
27use qrcode::QrCode;
28use totp_rs::{Algorithm, TOTP};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum AuthEvent {
33 SignedIn,
35 SignedOut,
37 TokenRefreshed,
39 UserUpdated,
41 PasswordReset,
43 MfaChallengeRequired,
45 MfaChallengeCompleted,
47 MfaEnabled,
49 MfaDisabled,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub enum MfaMethod {
56 #[serde(rename = "totp")]
58 Totp,
59 #[serde(rename = "sms")]
61 Sms,
62 #[serde(rename = "email")]
64 Email,
65}
66
67impl std::fmt::Display for MfaMethod {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 MfaMethod::Totp => write!(f, "TOTP"),
71 MfaMethod::Sms => write!(f, "SMS"),
72 MfaMethod::Email => write!(f, "Email"),
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub enum MfaChallengeStatus {
80 #[serde(rename = "pending")]
82 Pending,
83 #[serde(rename = "completed")]
85 Completed,
86 #[serde(rename = "expired")]
88 Expired,
89 #[serde(rename = "cancelled")]
91 Cancelled,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct MfaFactor {
97 pub id: Uuid,
98 pub factor_type: MfaMethod,
99 pub friendly_name: String,
100 pub status: String, pub created_at: Timestamp,
102 pub updated_at: Timestamp,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub phone: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct TotpSetupResponse {
110 pub secret: String,
111 pub qr_code: String,
112 pub uri: String,
113 pub factor_id: Uuid,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct MfaChallenge {
119 pub id: Uuid,
120 pub factor_id: Uuid,
121 pub status: MfaChallengeStatus,
122 pub challenge_type: MfaMethod,
123 pub expires_at: Timestamp,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub masked_phone: Option<String>,
126}
127
128#[derive(Debug, Serialize)]
130pub struct MfaVerificationRequest {
131 pub factor_id: Uuid,
132 pub challenge_id: Uuid,
133 pub code: String,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct EnhancedPhoneNumber {
139 pub raw: String,
140 pub formatted: String,
141 pub country_code: String,
142 pub national_number: String,
143 pub is_valid: bool,
144}
145
146impl EnhancedPhoneNumber {
147 pub fn new(phone: &str, _default_region: Option<&str>) -> Result<Self> {
149 let parsed = phonenumber::parse(None, phone)
152 .map_err(|e| Error::auth(format!("Invalid phone number: {}", e)))?;
153
154 let formatted = phonenumber::format(&parsed)
155 .mode(Mode::International)
156 .to_string();
157
158 let country_code = parsed.code().value().to_string();
160 let national_number = parsed.national().to_string();
161
162 Ok(Self {
163 raw: phone.to_string(),
164 formatted,
165 country_code,
166 national_number,
167 is_valid: phonenumber::is_valid(&parsed),
168 })
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct TokenMetadata {
175 pub issued_at: Timestamp,
176 pub expires_at: Timestamp,
177 pub refresh_count: u32,
178 pub last_refresh_at: Option<Timestamp>,
179 pub scopes: Vec<String>,
180 pub device_id: Option<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EnhancedSession {
186 pub access_token: String,
187 pub refresh_token: String,
188 pub expires_in: i64,
189 pub expires_at: Timestamp,
190 pub token_type: String,
191 pub user: User,
192 pub token_metadata: Option<TokenMetadata>,
193 pub mfa_verified: bool,
194 pub active_factors: Vec<MfaFactor>,
195}
196
197pub type AuthStateCallback = Box<dyn Fn(AuthEvent, Option<Session>) + Send + Sync + 'static>;
199
200#[derive(Debug, Clone)]
202pub struct AuthEventHandle {
203 id: Uuid,
204 auth: Weak<Auth>,
205}
206
207impl AuthEventHandle {
208 pub fn remove(&self) {
210 if let Some(auth) = self.auth.upgrade() {
211 auth.remove_auth_listener(self.id);
212 }
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub enum OAuthProvider {
219 #[serde(rename = "google")]
221 Google,
222 #[serde(rename = "github")]
224 GitHub,
225 #[serde(rename = "discord")]
227 Discord,
228 #[serde(rename = "apple")]
230 Apple,
231 #[serde(rename = "twitter")]
233 Twitter,
234 #[serde(rename = "facebook")]
236 Facebook,
237 #[serde(rename = "azure")]
239 Microsoft,
240 #[serde(rename = "linkedin_oidc")]
242 LinkedIn,
243}
244
245impl OAuthProvider {
246 pub fn as_str(&self) -> &'static str {
248 match self {
249 OAuthProvider::Google => "google",
250 OAuthProvider::GitHub => "github",
251 OAuthProvider::Discord => "discord",
252 OAuthProvider::Apple => "apple",
253 OAuthProvider::Twitter => "twitter",
254 OAuthProvider::Facebook => "facebook",
255 OAuthProvider::Microsoft => "azure",
256 OAuthProvider::LinkedIn => "linkedin_oidc",
257 }
258 }
259}
260
261#[derive(Debug, Clone, Default)]
263pub struct OAuthOptions {
264 pub redirect_to: Option<String>,
266 pub scopes: Option<Vec<String>>,
268 pub query_params: Option<std::collections::HashMap<String, String>>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct OAuthResponse {
275 pub url: String,
277}
278
279#[derive(Debug, Serialize)]
281struct PhoneSignUpRequest {
282 phone: String,
283 password: String,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 data: Option<serde_json::Value>,
286}
287
288#[derive(Debug, Serialize)]
290struct PhoneSignInRequest {
291 phone: String,
292 password: String,
293}
294
295#[derive(Debug, Serialize)]
297struct OTPVerificationRequest {
298 phone: String,
299 token: String,
300 #[serde(rename = "type")]
301 verification_type: String,
302}
303
304#[derive(Debug, Serialize)]
306struct MagicLinkRequest {
307 email: String,
308 #[serde(skip_serializing_if = "Option::is_none")]
309 redirect_to: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
311 data: Option<serde_json::Value>,
312}
313
314#[derive(Debug, Serialize)]
316struct AnonymousSignInRequest {
317 #[serde(skip_serializing_if = "Option::is_none")]
318 data: Option<serde_json::Value>,
319}
320
321pub struct Auth {
323 http_client: Arc<HttpClient>,
324 config: Arc<SupabaseConfig>,
325 session: Arc<RwLock<Option<Session>>>,
326 event_listeners: Arc<RwLock<HashMap<Uuid, AuthStateCallback>>>,
327}
328
329impl Clone for Auth {
330 fn clone(&self) -> Self {
331 Self {
332 http_client: self.http_client.clone(),
333 config: self.config.clone(),
334 session: self.session.clone(),
335 event_listeners: Arc::new(RwLock::new(HashMap::new())),
336 }
337 }
338}
339
340impl std::fmt::Debug for Auth {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 f.debug_struct("Auth")
343 .field("http_client", &"HttpClient")
344 .field("config", &self.config)
345 .field("session", &self.session)
346 .field(
347 "event_listeners",
348 &format!(
349 "HashMap<Uuid, Callback> with {} listeners",
350 self.event_listeners.read().map(|l| l.len()).unwrap_or(0)
351 ),
352 )
353 .finish()
354 }
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct User {
360 pub id: Uuid,
361 pub email: Option<String>,
362 pub phone: Option<String>,
363 pub email_confirmed_at: Option<Timestamp>,
364 pub phone_confirmed_at: Option<Timestamp>,
365 pub created_at: Timestamp,
366 pub updated_at: Timestamp,
367 pub last_sign_in_at: Option<Timestamp>,
368 pub app_metadata: serde_json::Value,
369 pub user_metadata: serde_json::Value,
370 pub aud: String,
371 pub role: Option<String>,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct Session {
377 pub access_token: String,
378 pub refresh_token: String,
379 pub expires_in: i64,
380 pub expires_at: Timestamp,
381 pub token_type: String,
382 pub user: User,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct AuthResponse {
388 pub user: Option<User>,
389 pub session: Option<Session>,
390}
391
392#[derive(Debug, Serialize)]
394struct SignUpRequest {
395 email: String,
396 password: String,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 data: Option<serde_json::Value>,
399 #[serde(skip_serializing_if = "Option::is_none")]
400 redirect_to: Option<String>,
401}
402
403#[derive(Debug, Serialize)]
405struct SignInRequest {
406 email: String,
407 password: String,
408}
409
410#[derive(Debug, Serialize)]
412struct PasswordResetRequest {
413 email: String,
414 #[serde(skip_serializing_if = "Option::is_none")]
415 redirect_to: Option<String>,
416}
417
418#[derive(Debug, Serialize)]
420struct RefreshTokenRequest {
421 refresh_token: String,
422}
423
424#[derive(Debug, Serialize)]
426struct UpdateUserRequest {
427 #[serde(skip_serializing_if = "Option::is_none")]
428 email: Option<String>,
429 #[serde(skip_serializing_if = "Option::is_none")]
430 password: Option<String>,
431 #[serde(skip_serializing_if = "Option::is_none")]
432 data: Option<serde_json::Value>,
433}
434
435impl Auth {
436 pub fn new(config: Arc<SupabaseConfig>, http_client: Arc<HttpClient>) -> Result<Self> {
438 debug!("Initializing Auth module");
439
440 Ok(Self {
441 http_client,
442 config,
443 session: Arc::new(RwLock::new(None)),
444 event_listeners: Arc::new(RwLock::new(HashMap::new())),
445 })
446 }
447
448 pub async fn sign_up_with_email_and_password(
450 &self,
451 email: &str,
452 password: &str,
453 ) -> Result<AuthResponse> {
454 self.sign_up_with_email_password_and_data(email, password, None, None)
455 .await
456 }
457
458 pub async fn sign_up_with_email_password_and_data(
460 &self,
461 email: &str,
462 password: &str,
463 data: Option<serde_json::Value>,
464 redirect_to: Option<String>,
465 ) -> Result<AuthResponse> {
466 debug!("Signing up user with email: {}", email);
467
468 let payload = SignUpRequest {
469 email: email.to_string(),
470 password: password.to_string(),
471 data,
472 redirect_to,
473 };
474
475 let response = self
476 .http_client
477 .post(format!("{}/auth/v1/signup", self.config.url))
478 .json(&payload)
479 .send()
480 .await?;
481
482 if !response.status().is_success() {
483 let status = response.status();
484 let error_msg = match response.text().await {
485 Ok(text) => text,
486 Err(_) => format!("Sign up failed with status: {}", status),
487 };
488 return Err(Error::auth(error_msg));
489 }
490
491 let auth_response: AuthResponse = response.json().await?;
492
493 if let Some(ref session) = auth_response.session {
494 self.set_session(session.clone()).await?;
495 self.trigger_auth_event(AuthEvent::SignedIn);
496 info!("User signed up successfully");
497 }
498
499 Ok(auth_response)
500 }
501
502 pub async fn sign_in_with_email_and_password(
504 &self,
505 email: &str,
506 password: &str,
507 ) -> Result<AuthResponse> {
508 debug!("Signing in user with email: {}", email);
509
510 let payload = SignInRequest {
511 email: email.to_string(),
512 password: password.to_string(),
513 };
514
515 let response = self
516 .http_client
517 .post(format!(
518 "{}/auth/v1/token?grant_type=password",
519 self.config.url
520 ))
521 .json(&payload)
522 .send()
523 .await?;
524
525 if !response.status().is_success() {
526 let status = response.status();
527 let error_msg = match response.text().await {
528 Ok(text) => text,
529 Err(_) => format!("Sign in failed with status: {}", status),
530 };
531 return Err(Error::auth(error_msg));
532 }
533
534 let auth_response: AuthResponse = response.json().await?;
535
536 if let Some(ref session) = auth_response.session {
537 self.set_session(session.clone()).await?;
538 self.trigger_auth_event(AuthEvent::SignedIn);
539 info!("User signed in successfully");
540 }
541
542 Ok(auth_response)
543 }
544
545 pub async fn sign_out(&self) -> Result<()> {
547 debug!("Signing out user");
548
549 let session = self.get_session()?;
550
551 let response = self
552 .http_client
553 .post(format!("{}/auth/v1/logout", self.config.url))
554 .header("Authorization", format!("Bearer {}", session.access_token))
555 .send()
556 .await?;
557
558 if !response.status().is_success() {
559 warn!("Sign out request failed with status: {}", response.status());
560 }
561
562 self.clear_session().await?;
563 self.trigger_auth_event(AuthEvent::SignedOut);
564 info!("User signed out successfully");
565
566 Ok(())
567 }
568
569 pub async fn reset_password_for_email(&self, email: &str) -> Result<()> {
571 self.reset_password_for_email_with_redirect(email, None)
572 .await
573 }
574
575 pub async fn reset_password_for_email_with_redirect(
577 &self,
578 email: &str,
579 redirect_to: Option<String>,
580 ) -> Result<()> {
581 debug!("Requesting password reset for email: {}", email);
582
583 let payload = PasswordResetRequest {
584 email: email.to_string(),
585 redirect_to,
586 };
587
588 let response = self
589 .http_client
590 .post(format!("{}/auth/v1/recover", self.config.url))
591 .json(&payload)
592 .send()
593 .await?;
594
595 if !response.status().is_success() {
596 let status = response.status();
597 let error_msg = match response.text().await {
598 Ok(text) => text,
599 Err(_) => format!("Password reset failed with status: {}", status),
600 };
601 return Err(Error::auth(error_msg));
602 }
603
604 info!("Password reset email sent successfully");
605 Ok(())
606 }
607
608 pub async fn update_user(
610 &self,
611 email: Option<String>,
612 password: Option<String>,
613 data: Option<serde_json::Value>,
614 ) -> Result<AuthResponse> {
615 debug!("Updating user information");
616
617 let session = self.get_session()?;
618
619 let payload = UpdateUserRequest {
620 email,
621 password,
622 data,
623 };
624
625 let response = self
626 .http_client
627 .put(format!("{}/auth/v1/user", self.config.url))
628 .header("Authorization", format!("Bearer {}", session.access_token))
629 .json(&payload)
630 .send()
631 .await?;
632
633 if !response.status().is_success() {
634 let status = response.status();
635 let error_msg = match response.text().await {
636 Ok(text) => text,
637 Err(_) => format!("User update failed with status: {}", status),
638 };
639 return Err(Error::auth(error_msg));
640 }
641
642 let auth_response: AuthResponse = response.json().await?;
643
644 if let Some(ref session) = auth_response.session {
645 self.set_session(session.clone()).await?;
646 }
647
648 info!("User updated successfully");
649 Ok(auth_response)
650 }
651
652 pub async fn refresh_session(&self) -> Result<AuthResponse> {
654 debug!("Refreshing session token");
655
656 let current_session = self.get_session()?;
657
658 let payload = RefreshTokenRequest {
659 refresh_token: current_session.refresh_token.clone(),
660 };
661
662 let response = self
663 .http_client
664 .post(format!(
665 "{}/auth/v1/token?grant_type=refresh_token",
666 self.config.url
667 ))
668 .json(&payload)
669 .send()
670 .await?;
671
672 if !response.status().is_success() {
673 let status = response.status();
674 let error_msg = match response.text().await {
675 Ok(text) => text,
676 Err(_) => format!("Token refresh failed with status: {}", status),
677 };
678 return Err(Error::auth(error_msg));
679 }
680
681 let auth_response: AuthResponse = response.json().await?;
682
683 if let Some(ref session) = auth_response.session {
684 self.set_session(session.clone()).await?;
685 self.trigger_auth_event(AuthEvent::TokenRefreshed);
686 info!("Session refreshed successfully");
687 }
688
689 Ok(auth_response)
690 }
691
692 pub async fn current_user(&self) -> Result<Option<User>> {
694 let session_guard = self
695 .session
696 .read()
697 .map_err(|_| Error::auth("Failed to read session"))?;
698 Ok(session_guard.as_ref().map(|s| s.user.clone()))
699 }
700
701 pub fn get_session(&self) -> Result<Session> {
703 let session_guard = self
704 .session
705 .read()
706 .map_err(|_| Error::auth("Failed to read session"))?;
707 session_guard
708 .as_ref()
709 .cloned()
710 .ok_or_else(|| Error::auth("No active session"))
711 }
712
713 pub async fn set_session(&self, session: Session) -> Result<()> {
715 let mut session_guard = self
716 .session
717 .write()
718 .map_err(|_| Error::auth("Failed to write session"))?;
719 *session_guard = Some(session);
720 Ok(())
721 }
722
723 pub async fn set_session_token(&self, token: &str) -> Result<()> {
725 debug!("Setting session from token");
726
727 let user_response = self
728 .http_client
729 .get(format!("{}/auth/v1/user", self.config.url))
730 .header("Authorization", format!("Bearer {}", token))
731 .send()
732 .await?;
733
734 if !user_response.status().is_success() {
735 return Err(Error::auth("Invalid token"));
736 }
737
738 let user: User = user_response.json().await?;
739
740 let session = Session {
741 access_token: token.to_string(),
742 refresh_token: String::new(),
743 expires_in: 3600,
744 expires_at: Utc::now() + chrono::Duration::seconds(3600),
745 token_type: "bearer".to_string(),
746 user,
747 };
748
749 self.set_session(session).await?;
750 Ok(())
751 }
752
753 pub async fn clear_session(&self) -> Result<()> {
755 let mut session_guard = self
756 .session
757 .write()
758 .map_err(|_| Error::auth("Failed to write session"))?;
759 *session_guard = None;
760 Ok(())
761 }
762
763 pub fn is_authenticated(&self) -> bool {
765 let session_guard = self.session.read().unwrap_or_else(|_| {
766 warn!("Failed to read session lock");
767 self.session.read().unwrap()
768 });
769
770 match session_guard.as_ref() {
771 Some(session) => {
772 let now = Utc::now();
773 session.expires_at > now
774 }
775 None => false,
776 }
777 }
778
779 pub fn needs_refresh(&self) -> bool {
781 let session_guard = match self.session.read() {
782 Ok(guard) => guard,
783 Err(_) => {
784 warn!("Failed to read session lock");
785 return false;
786 }
787 };
788
789 match session_guard.as_ref() {
790 Some(session) => {
791 let now = Utc::now();
792 let buffer = chrono::Duration::minutes(5); session.expires_at < (now + buffer)
794 }
795 None => false,
796 }
797 }
798
799 pub async fn sign_in_with_oauth(
824 &self,
825 provider: OAuthProvider,
826 options: Option<OAuthOptions>,
827 ) -> Result<OAuthResponse> {
828 debug!("Initiating OAuth sign-in with provider: {:?}", provider);
829
830 let mut url = format!(
831 "{}/auth/v1/authorize?provider={}",
832 self.config.url,
833 provider.as_str()
834 );
835
836 if let Some(opts) = options {
837 if let Some(redirect_to) = opts.redirect_to {
838 url.push_str(&format!(
839 "&redirect_to={}",
840 urlencoding::encode(&redirect_to)
841 ));
842 }
843
844 if let Some(scopes) = opts.scopes {
845 let scope_str = scopes.join(" ");
846 url.push_str(&format!("&scope={}", urlencoding::encode(&scope_str)));
847 }
848
849 if let Some(query_params) = opts.query_params {
850 for (key, value) in query_params {
851 url.push_str(&format!(
852 "&{}={}",
853 urlencoding::encode(&key),
854 urlencoding::encode(&value)
855 ));
856 }
857 }
858 }
859
860 Ok(OAuthResponse { url })
861 }
862
863 pub async fn sign_up_with_phone(
882 &self,
883 phone: &str,
884 password: &str,
885 data: Option<serde_json::Value>,
886 ) -> Result<AuthResponse> {
887 debug!("Signing up user with phone: {}", phone);
888
889 let payload = PhoneSignUpRequest {
890 phone: phone.to_string(),
891 password: password.to_string(),
892 data,
893 };
894
895 let response = self
896 .http_client
897 .post(format!("{}/auth/v1/signup", self.config.url))
898 .json(&payload)
899 .send()
900 .await?;
901
902 if !response.status().is_success() {
903 let status = response.status();
904 let error_msg = match response.text().await {
905 Ok(text) => text,
906 Err(_) => format!("Phone sign up failed with status: {}", status),
907 };
908 return Err(Error::auth(error_msg));
909 }
910
911 let auth_response: AuthResponse = response.json().await?;
912
913 if let Some(ref session) = auth_response.session {
914 self.set_session(session.clone()).await?;
915 self.trigger_auth_event(AuthEvent::SignedIn);
916 info!("User signed up with phone successfully");
917 }
918
919 Ok(auth_response)
920 }
921
922 pub async fn sign_in_with_phone(&self, phone: &str, password: &str) -> Result<AuthResponse> {
941 debug!("Signing in user with phone: {}", phone);
942
943 let payload = PhoneSignInRequest {
944 phone: phone.to_string(),
945 password: password.to_string(),
946 };
947
948 let response = self
949 .http_client
950 .post(format!(
951 "{}/auth/v1/token?grant_type=password",
952 self.config.url
953 ))
954 .json(&payload)
955 .send()
956 .await?;
957
958 if !response.status().is_success() {
959 let status = response.status();
960 let error_msg = match response.text().await {
961 Ok(text) => text,
962 Err(_) => format!("Phone sign in failed with status: {}", status),
963 };
964 return Err(Error::auth(error_msg));
965 }
966
967 let auth_response: AuthResponse = response.json().await?;
968
969 if let Some(ref session) = auth_response.session {
970 self.set_session(session.clone()).await?;
971 self.trigger_auth_event(AuthEvent::SignedIn);
972 info!("User signed in with phone successfully");
973 }
974
975 Ok(auth_response)
976 }
977
978 pub async fn verify_otp(
997 &self,
998 phone: &str,
999 token: &str,
1000 verification_type: &str,
1001 ) -> Result<AuthResponse> {
1002 debug!("Verifying OTP for phone: {}", phone);
1003
1004 let payload = OTPVerificationRequest {
1005 phone: phone.to_string(),
1006 token: token.to_string(),
1007 verification_type: verification_type.to_string(),
1008 };
1009
1010 let response = self
1011 .http_client
1012 .post(format!("{}/auth/v1/verify", self.config.url))
1013 .json(&payload)
1014 .send()
1015 .await?;
1016
1017 if !response.status().is_success() {
1018 let status = response.status();
1019 let error_msg = match response.text().await {
1020 Ok(text) => text,
1021 Err(_) => format!("OTP verification failed with status: {}", status),
1022 };
1023 return Err(Error::auth(error_msg));
1024 }
1025
1026 let auth_response: AuthResponse = response.json().await?;
1027
1028 if let Some(ref session) = auth_response.session {
1029 self.set_session(session.clone()).await?;
1030 self.trigger_auth_event(AuthEvent::SignedIn);
1031 info!("OTP verified successfully");
1032 }
1033
1034 Ok(auth_response)
1035 }
1036
1037 pub async fn sign_in_with_magic_link(
1054 &self,
1055 email: &str,
1056 redirect_to: Option<String>,
1057 data: Option<serde_json::Value>,
1058 ) -> Result<()> {
1059 debug!("Sending magic link to email: {}", email);
1060
1061 let payload = MagicLinkRequest {
1062 email: email.to_string(),
1063 redirect_to,
1064 data,
1065 };
1066
1067 let response = self
1068 .http_client
1069 .post(format!("{}/auth/v1/magiclink", self.config.url))
1070 .json(&payload)
1071 .send()
1072 .await?;
1073
1074 if !response.status().is_success() {
1075 let status = response.status();
1076 let error_msg = match response.text().await {
1077 Ok(text) => text,
1078 Err(_) => format!("Magic link request failed with status: {}", status),
1079 };
1080 return Err(Error::auth(error_msg));
1081 }
1082
1083 info!("Magic link sent successfully");
1084 Ok(())
1085 }
1086
1087 pub async fn sign_in_anonymously(
1108 &self,
1109 data: Option<serde_json::Value>,
1110 ) -> Result<AuthResponse> {
1111 debug!("Creating anonymous user session");
1112
1113 let payload = AnonymousSignInRequest { data };
1114
1115 let response = self
1116 .http_client
1117 .post(format!("{}/auth/v1/signup", self.config.url))
1118 .header("Authorization", format!("Bearer {}", self.config.key))
1119 .json(&payload)
1120 .send()
1121 .await?;
1122
1123 if !response.status().is_success() {
1124 let status = response.status();
1125 let error_msg = match response.text().await {
1126 Ok(text) => text,
1127 Err(_) => format!("Anonymous sign in failed with status: {}", status),
1128 };
1129 return Err(Error::auth(error_msg));
1130 }
1131
1132 let auth_response: AuthResponse = response.json().await?;
1133
1134 if let Some(ref session) = auth_response.session {
1135 self.set_session(session.clone()).await?;
1136 self.trigger_auth_event(AuthEvent::SignedIn);
1137 info!("Anonymous user session created successfully");
1138 }
1139
1140 Ok(auth_response)
1141 }
1142
1143 pub async fn reset_password_for_email_enhanced(
1160 &self,
1161 email: &str,
1162 redirect_to: Option<String>,
1163 ) -> Result<()> {
1164 debug!("Initiating enhanced password recovery for email: {}", email);
1165
1166 let payload = PasswordResetRequest {
1167 email: email.to_string(),
1168 redirect_to,
1169 };
1170
1171 let response = self
1172 .http_client
1173 .post(format!("{}/auth/v1/recover", self.config.url))
1174 .json(&payload)
1175 .send()
1176 .await?;
1177
1178 if !response.status().is_success() {
1179 let status = response.status();
1180 let error_msg = match response.text().await {
1181 Ok(text) => text,
1182 Err(_) => format!("Enhanced password recovery failed with status: {}", status),
1183 };
1184 return Err(Error::auth(error_msg));
1185 }
1186
1187 self.trigger_auth_event(AuthEvent::PasswordReset);
1188 info!("Enhanced password recovery email sent successfully");
1189 Ok(())
1190 }
1191
1192 pub fn on_auth_state_change<F>(&self, callback: F) -> AuthEventHandle
1223 where
1224 F: Fn(AuthEvent, Option<Session>) + Send + Sync + 'static,
1225 {
1226 let id = Uuid::new_v4();
1227 let callback = Box::new(callback);
1228
1229 if let Ok(mut listeners) = self.event_listeners.write() {
1230 listeners.insert(id, callback);
1231 }
1232
1233 AuthEventHandle {
1234 id,
1235 auth: Arc::downgrade(&Arc::new(self.clone())),
1236 }
1237 }
1238
1239 pub fn remove_auth_listener(&self, id: Uuid) {
1241 if let Ok(mut listeners) = self.event_listeners.write() {
1242 listeners.remove(&id);
1243 }
1244 }
1245
1246 fn trigger_auth_event(&self, event: AuthEvent) {
1248 let session = match self.session.read() {
1249 Ok(guard) => guard.clone(),
1250 Err(_) => {
1251 warn!("Failed to read session for event trigger");
1252 return;
1253 }
1254 };
1255
1256 let listeners = match self.event_listeners.read() {
1257 Ok(guard) => guard,
1258 Err(_) => {
1259 warn!("Failed to read event listeners");
1260 return;
1261 }
1262 };
1263
1264 for callback in listeners.values() {
1265 callback(event.clone(), session.clone());
1266 }
1267 }
1268
1269 pub async fn list_mfa_factors(&self) -> Result<Vec<MfaFactor>> {
1287 debug!("Listing MFA factors for user");
1288
1289 let session = self.get_session()?;
1290 let response = self
1291 .http_client
1292 .get(format!("{}/auth/v1/factors", self.config.url))
1293 .header("Authorization", format!("Bearer {}", session.access_token))
1294 .send()
1295 .await?;
1296
1297 if !response.status().is_success() {
1298 return Err(Error::auth("Failed to list MFA factors"));
1299 }
1300
1301 let factors: Vec<MfaFactor> = response.json().await?;
1302 Ok(factors)
1303 }
1304
1305 pub async fn setup_totp(&self, friendly_name: &str) -> Result<TotpSetupResponse> {
1325 debug!("Setting up TOTP factor: {}", friendly_name);
1326
1327 let session = self.get_session()?;
1328
1329 let request_body = serde_json::json!({
1330 "friendly_name": friendly_name,
1331 "factor_type": "totp"
1332 });
1333
1334 let response = self
1335 .http_client
1336 .post(format!("{}/auth/v1/factors", self.config.url))
1337 .header("Authorization", format!("Bearer {}", session.access_token))
1338 .json(&request_body)
1339 .send()
1340 .await?;
1341
1342 if !response.status().is_success() {
1343 return Err(Error::auth("Failed to setup TOTP"));
1344 }
1345
1346 let setup_response: TotpSetupResponse = response.json().await?;
1347
1348 let qr = QrCode::new(&setup_response.uri)
1350 .map_err(|e| Error::auth(format!("Failed to generate QR code: {}", e)))?;
1351
1352 let qr_string = qr
1354 .render::<char>()
1355 .quiet_zone(false)
1356 .module_dimensions(2, 1)
1357 .build();
1358
1359 Ok(TotpSetupResponse {
1360 secret: setup_response.secret,
1361 qr_code: qr_string,
1362 uri: setup_response.uri,
1363 factor_id: setup_response.factor_id,
1364 })
1365 }
1366
1367 pub async fn setup_sms_mfa(
1386 &self,
1387 phone: &str,
1388 friendly_name: &str,
1389 default_region: Option<&str>,
1390 ) -> Result<MfaFactor> {
1391 debug!("Setting up SMS MFA factor: {} for {}", friendly_name, phone);
1392
1393 let enhanced_phone = EnhancedPhoneNumber::new(phone, default_region)?;
1395
1396 if !enhanced_phone.is_valid {
1397 return Err(Error::auth("Invalid phone number provided"));
1398 }
1399
1400 let session = self.get_session()?;
1401
1402 let request_body = serde_json::json!({
1403 "friendly_name": friendly_name,
1404 "factor_type": "sms",
1405 "phone": enhanced_phone.formatted
1406 });
1407
1408 let response = self
1409 .http_client
1410 .post(format!("{}/auth/v1/factors", self.config.url))
1411 .header("Authorization", format!("Bearer {}", session.access_token))
1412 .json(&request_body)
1413 .send()
1414 .await?;
1415
1416 if !response.status().is_success() {
1417 return Err(Error::auth("Failed to setup SMS MFA"));
1418 }
1419
1420 let factor: MfaFactor = response.json().await?;
1421 self.trigger_auth_event(AuthEvent::MfaEnabled);
1422
1423 Ok(factor)
1424 }
1425
1426 pub async fn create_mfa_challenge(&self, factor_id: Uuid) -> Result<MfaChallenge> {
1446 debug!("Creating MFA challenge for factor: {}", factor_id);
1447
1448 let session = self.get_session()?;
1449
1450 let request_body = serde_json::json!({
1451 "factor_id": factor_id
1452 });
1453
1454 let response = self
1455 .http_client
1456 .post(format!(
1457 "{}/auth/v1/factors/{}/challenge",
1458 self.config.url, factor_id
1459 ))
1460 .header("Authorization", format!("Bearer {}", session.access_token))
1461 .json(&request_body)
1462 .send()
1463 .await?;
1464
1465 if !response.status().is_success() {
1466 return Err(Error::auth("Failed to create MFA challenge"));
1467 }
1468
1469 let challenge: MfaChallenge = response.json().await?;
1470 self.trigger_auth_event(AuthEvent::MfaChallengeRequired);
1471
1472 Ok(challenge)
1473 }
1474
1475 pub async fn verify_mfa_challenge(
1498 &self,
1499 factor_id: Uuid,
1500 challenge_id: Uuid,
1501 code: &str,
1502 ) -> Result<AuthResponse> {
1503 debug!("Verifying MFA challenge: {}", challenge_id);
1504
1505 let session = self.get_session()?;
1506
1507 let request_body = serde_json::json!({
1508 "factor_id": factor_id,
1509 "challenge_id": challenge_id,
1510 "code": code
1511 });
1512
1513 let response = self
1514 .http_client
1515 .post(format!(
1516 "{}/auth/v1/factors/{}/verify",
1517 self.config.url, factor_id
1518 ))
1519 .header("Authorization", format!("Bearer {}", session.access_token))
1520 .json(&request_body)
1521 .send()
1522 .await?;
1523
1524 if !response.status().is_success() {
1525 return Err(Error::auth("Failed to verify MFA challenge"));
1526 }
1527
1528 let auth_response: AuthResponse = response.json().await?;
1529
1530 if let Some(session) = &auth_response.session {
1532 self.set_session(session.clone()).await?;
1533 }
1534
1535 self.trigger_auth_event(AuthEvent::MfaChallengeCompleted);
1536 info!("MFA verification successful");
1537
1538 Ok(auth_response)
1539 }
1540
1541 pub async fn delete_mfa_factor(&self, factor_id: Uuid) -> Result<()> {
1559 debug!("Deleting MFA factor: {}", factor_id);
1560
1561 let session = self.get_session()?;
1562
1563 let response = self
1564 .http_client
1565 .delete(format!("{}/auth/v1/factors/{}", self.config.url, factor_id))
1566 .header("Authorization", format!("Bearer {}", session.access_token))
1567 .send()
1568 .await?;
1569
1570 if !response.status().is_success() {
1571 return Err(Error::auth("Failed to delete MFA factor"));
1572 }
1573
1574 self.trigger_auth_event(AuthEvent::MfaDisabled);
1575 info!("MFA factor deleted successfully");
1576
1577 Ok(())
1578 }
1579
1580 pub fn generate_totp_code(&self, secret: &str) -> Result<String> {
1600 debug!("Generating TOTP code for testing");
1601
1602 let decoded_secret = base32::decode(base32::Alphabet::Rfc4648 { padding: true }, secret)
1604 .ok_or_else(|| Error::auth("Invalid base32 secret"))?;
1605
1606 let totp = TOTP::new(
1608 Algorithm::SHA1,
1609 6, 1, 30, decoded_secret,
1613 )
1614 .map_err(|e| Error::auth(format!("Failed to create TOTP: {}", e)))?;
1615
1616 let code = totp
1618 .generate_current()
1619 .map_err(|e| Error::auth(format!("Failed to generate TOTP code: {}", e)))?;
1620
1621 Ok(code)
1622 }
1623
1624 pub fn get_token_metadata(&self) -> Result<Option<TokenMetadata>> {
1645 debug!("Getting token metadata");
1646
1647 let session = self
1648 .session
1649 .read()
1650 .map_err(|_| Error::auth("Failed to read session"))?;
1651
1652 if let Some(session) = session.as_ref() {
1653 let metadata = TokenMetadata {
1655 issued_at: Utc::now() - chrono::Duration::seconds(session.expires_in),
1656 expires_at: session.expires_at,
1657 refresh_count: 0, last_refresh_at: None,
1659 scopes: vec![], device_id: None, };
1662
1663 Ok(Some(metadata))
1664 } else {
1665 Ok(None)
1666 }
1667 }
1668
1669 pub async fn refresh_token_advanced(&self) -> Result<Session> {
1698 debug!("Refreshing token with advanced handling");
1699
1700 let current_session = self
1701 .session
1702 .read()
1703 .map_err(|_| Error::auth("Failed to read session"))?
1704 .clone();
1705
1706 let session = match current_session {
1707 Some(session) => session,
1708 None => return Err(Error::auth("No active session to refresh")),
1709 };
1710
1711 let request_body = serde_json::json!({
1712 "refresh_token": session.refresh_token
1713 });
1714
1715 let response = self
1716 .http_client
1717 .post(format!(
1718 "{}/auth/v1/token?grant_type=refresh_token",
1719 self.config.url
1720 ))
1721 .header("apikey", &self.config.key)
1722 .header("Authorization", format!("Bearer {}", &self.config.key))
1723 .json(&request_body)
1724 .send()
1725 .await;
1726
1727 match response {
1728 Ok(response) => {
1729 if response.status().is_success() {
1730 let auth_response: AuthResponse = response.json().await?;
1731
1732 if let Some(new_session) = auth_response.session {
1733 self.set_session(new_session.clone()).await?;
1734 self.trigger_auth_event(AuthEvent::TokenRefreshed);
1735 info!("Token refreshed successfully");
1736 Ok(new_session)
1737 } else {
1738 Err(Error::auth("No session in refresh response"))
1739 }
1740 } else {
1741 let status = response.status();
1742 let error_text = response.text().await.unwrap_or_default();
1743
1744 let context = crate::error::ErrorContext {
1746 platform: Some(crate::error::detect_platform_context()),
1747 http: Some(crate::error::HttpErrorContext {
1748 status_code: Some(status.as_u16()),
1749 headers: None,
1750 response_body: Some(error_text.clone()),
1751 url: Some(format!("{}/auth/v1/token", self.config.url)),
1752 method: Some("POST".to_string()),
1753 }),
1754 retry: if status.is_server_error() {
1755 Some(crate::error::RetryInfo {
1756 retryable: true,
1757 retry_after: Some(60), attempts: 0,
1759 })
1760 } else {
1761 None
1762 },
1763 metadata: std::collections::HashMap::new(),
1764 timestamp: chrono::Utc::now(),
1765 };
1766
1767 Err(Error::auth_with_context(
1768 format!("Token refresh failed: {} - {}", status, error_text),
1769 context,
1770 ))
1771 }
1772 }
1773 Err(e) => {
1774 let context = crate::error::ErrorContext {
1775 platform: Some(crate::error::detect_platform_context()),
1776 http: Some(crate::error::HttpErrorContext {
1777 status_code: None,
1778 headers: None,
1779 response_body: None,
1780 url: Some(format!("{}/auth/v1/token", self.config.url)),
1781 method: Some("POST".to_string()),
1782 }),
1783 retry: Some(crate::error::RetryInfo {
1784 retryable: true,
1785 retry_after: Some(30),
1786 attempts: 0,
1787 }),
1788 metadata: std::collections::HashMap::new(),
1789 timestamp: chrono::Utc::now(),
1790 };
1791
1792 Err(Error::auth_with_context(
1793 format!("Network error during token refresh: {}", e),
1794 context,
1795 ))
1796 }
1797 }
1798 }
1799
1800 pub fn needs_refresh_with_buffer(&self, buffer_seconds: i64) -> Result<bool> {
1818 let session_guard = self
1819 .session
1820 .read()
1821 .map_err(|_| Error::auth("Failed to read session"))?;
1822
1823 match session_guard.as_ref() {
1824 Some(session) => {
1825 let now = Utc::now();
1826 let refresh_threshold =
1827 session.expires_at - chrono::Duration::seconds(buffer_seconds);
1828 Ok(now >= refresh_threshold)
1829 }
1830 None => Ok(false), }
1832 }
1833
1834 pub fn time_until_expiry(&self) -> Result<Option<i64>> {
1856 let session_guard = self
1857 .session
1858 .read()
1859 .map_err(|_| Error::auth("Failed to read session"))?;
1860
1861 match session_guard.as_ref() {
1862 Some(session) => {
1863 let now = Utc::now();
1864 let duration = session.expires_at.signed_duration_since(now);
1865 Ok(Some(duration.num_seconds()))
1866 }
1867 None => Ok(None),
1868 }
1869 }
1870
1871 pub fn validate_token_local(&self) -> Result<bool> {
1889 let session_guard = self
1890 .session
1891 .read()
1892 .map_err(|_| Error::auth("Failed to read session"))?;
1893
1894 match session_guard.as_ref() {
1895 Some(session) => {
1896 let now = Utc::now();
1897 Ok(session.expires_at > now && !session.access_token.is_empty())
1898 }
1899 None => Ok(false),
1900 }
1901 }
1902}
1903
1904#[cfg(test)]
1905mod tests {
1906 use super::*;
1907 use crate::types::SupabaseConfig;
1908 use std::sync::Arc;
1909
1910 fn mock_config() -> Arc<SupabaseConfig> {
1911 Arc::new(SupabaseConfig {
1912 url: "https://test.supabase.co".to_string(),
1913 key: "test-key".to_string(),
1914 service_role_key: None,
1915 http_config: crate::types::HttpConfig::default(),
1916 auth_config: crate::types::AuthConfig::default(),
1917 database_config: crate::types::DatabaseConfig::default(),
1918 storage_config: crate::types::StorageConfig::default(),
1919 })
1920 }
1921
1922 #[test]
1923 fn test_enhanced_phone_number_creation() {
1924 let phone = EnhancedPhoneNumber::new("+1-555-123-4567", Some("US"));
1925 assert!(phone.is_ok());
1926
1927 let phone = phone.unwrap();
1928 assert!(!phone.raw.is_empty());
1929 assert!(!phone.formatted.is_empty());
1930 assert!(!phone.country_code.is_empty());
1931 }
1932
1933 #[test]
1934 fn test_mfa_method_serialization() {
1935 let totp = MfaMethod::Totp;
1936 let sms = MfaMethod::Sms;
1937 let email = MfaMethod::Email;
1938
1939 let totp_json = serde_json::to_string(&totp).unwrap();
1940 let sms_json = serde_json::to_string(&sms).unwrap();
1941 let email_json = serde_json::to_string(&email).unwrap();
1942
1943 assert_eq!(totp_json, r#""totp""#);
1944 assert_eq!(sms_json, r#""sms""#);
1945 assert_eq!(email_json, r#""email""#);
1946 }
1947
1948 #[test]
1949 fn test_mfa_challenge_status() {
1950 let pending = MfaChallengeStatus::Pending;
1951 let completed = MfaChallengeStatus::Completed;
1952 let expired = MfaChallengeStatus::Expired;
1953 let cancelled = MfaChallengeStatus::Cancelled;
1954
1955 assert_eq!(pending, MfaChallengeStatus::Pending);
1956 assert_eq!(completed, MfaChallengeStatus::Completed);
1957 assert_eq!(expired, MfaChallengeStatus::Expired);
1958 assert_eq!(cancelled, MfaChallengeStatus::Cancelled);
1959 }
1960
1961 #[test]
1962 fn test_auth_event_variants() {
1963 let events = vec![
1964 AuthEvent::SignedIn,
1965 AuthEvent::SignedOut,
1966 AuthEvent::TokenRefreshed,
1967 AuthEvent::UserUpdated,
1968 AuthEvent::PasswordReset,
1969 AuthEvent::MfaChallengeRequired,
1970 AuthEvent::MfaChallengeCompleted,
1971 AuthEvent::MfaEnabled,
1972 AuthEvent::MfaDisabled,
1973 ];
1974
1975 assert_eq!(events.len(), 9);
1976
1977 let cloned_events: Vec<AuthEvent> = events.to_vec();
1979 assert_eq!(cloned_events, events);
1980 }
1981
1982 #[tokio::test]
1983 async fn test_auth_creation() {
1984 let config = mock_config();
1985 let http_client = Arc::new(reqwest::Client::new());
1986
1987 let auth = Auth::new(config, http_client);
1988 assert!(auth.is_ok());
1989
1990 let auth = auth.unwrap();
1991 assert!(!auth.is_authenticated());
1992 }
1993
1994 #[test]
1995 fn test_totp_code_generation() {
1996 let config = mock_config();
1997 let http_client = Arc::new(reqwest::Client::new());
1998 let auth = Auth::new(config, http_client).unwrap();
1999
2000 let secret = "JBSWY3DPEHPK3PXP"; let result = auth.generate_totp_code(secret);
2003
2004 match &result {
2005 Ok(code) => {
2006 println!("Generated TOTP code: {}", code);
2007 assert_eq!(code.len(), 6);
2008 assert!(code.chars().all(|c| c.is_ascii_digit()));
2009 }
2010 Err(e) => {
2011 println!("TOTP generation error: {}", e);
2012 assert!(e.to_string().contains("base32") || e.to_string().contains("TOTP"));
2014 }
2015 }
2016 }
2017
2018 #[test]
2019 fn test_totp_code_generation_invalid_secret() {
2020 let config = mock_config();
2021 let http_client = Arc::new(reqwest::Client::new());
2022 let auth = Auth::new(config, http_client).unwrap();
2023
2024 let result = auth.generate_totp_code("invalid-secret");
2026 assert!(result.is_err());
2027 }
2028
2029 #[tokio::test]
2030 async fn test_token_validation_no_session() {
2031 let config = mock_config();
2032 let http_client = Arc::new(reqwest::Client::new());
2033 let auth = Auth::new(config, http_client).unwrap();
2034
2035 let is_valid = auth.validate_token_local().unwrap();
2037 assert!(!is_valid);
2038 }
2039
2040 #[test]
2041 fn test_time_until_expiry_no_session() {
2042 let config = mock_config();
2043 let http_client = Arc::new(reqwest::Client::new());
2044 let auth = Auth::new(config, http_client).unwrap();
2045
2046 let time = auth.time_until_expiry().unwrap();
2048 assert!(time.is_none());
2049 }
2050
2051 #[test]
2052 fn test_needs_refresh_no_session() {
2053 let config = mock_config();
2054 let http_client = Arc::new(reqwest::Client::new());
2055 let auth = Auth::new(config, http_client).unwrap();
2056
2057 let needs_refresh = auth.needs_refresh_with_buffer(300).unwrap();
2059 assert!(!needs_refresh);
2060 }
2061
2062 #[test]
2063 fn test_get_token_metadata_no_session() {
2064 let config = mock_config();
2065 let http_client = Arc::new(reqwest::Client::new());
2066 let auth = Auth::new(config, http_client).unwrap();
2067
2068 let metadata = auth.get_token_metadata().unwrap();
2070 assert!(metadata.is_none());
2071 }
2072
2073 #[test]
2074 fn test_token_metadata_structure() {
2075 let metadata = TokenMetadata {
2076 issued_at: Utc::now(),
2077 expires_at: Utc::now() + chrono::Duration::hours(1),
2078 refresh_count: 5,
2079 last_refresh_at: Some(Utc::now()),
2080 scopes: vec!["read".to_string(), "write".to_string()],
2081 device_id: Some("device-123".to_string()),
2082 };
2083
2084 assert_eq!(metadata.refresh_count, 5);
2085 assert!(metadata.last_refresh_at.is_some());
2086 assert_eq!(metadata.scopes.len(), 2);
2087 assert!(metadata.device_id.is_some());
2088 }
2089
2090 #[test]
2091 fn test_mfa_factor_structure() {
2092 let factor = MfaFactor {
2093 id: uuid::Uuid::new_v4(),
2094 factor_type: MfaMethod::Totp,
2095 friendly_name: "My Authenticator".to_string(),
2096 status: "verified".to_string(),
2097 created_at: Utc::now(),
2098 updated_at: Utc::now(),
2099 phone: None,
2100 };
2101
2102 assert_eq!(factor.factor_type, MfaMethod::Totp);
2103 assert_eq!(factor.friendly_name, "My Authenticator");
2104 assert_eq!(factor.status, "verified");
2105 assert!(factor.phone.is_none());
2106 }
2107
2108 #[test]
2109 fn test_enhanced_session_structure() {
2110 let user = User {
2111 id: uuid::Uuid::new_v4(),
2112 email: Some("user@example.com".to_string()),
2113 phone: None,
2114 email_confirmed_at: Some(Utc::now()),
2115 phone_confirmed_at: None,
2116 created_at: Utc::now(),
2117 updated_at: Utc::now(),
2118 last_sign_in_at: Some(Utc::now()),
2119 app_metadata: serde_json::json!({}),
2120 user_metadata: serde_json::json!({}),
2121 aud: "authenticated".to_string(),
2122 role: Some("authenticated".to_string()),
2123 };
2124
2125 let enhanced_session = EnhancedSession {
2126 access_token: "access-token".to_string(),
2127 refresh_token: "refresh-token".to_string(),
2128 expires_in: 3600,
2129 expires_at: Utc::now() + chrono::Duration::hours(1),
2130 token_type: "bearer".to_string(),
2131 user,
2132 token_metadata: None,
2133 mfa_verified: true,
2134 active_factors: vec![],
2135 };
2136
2137 assert!(enhanced_session.mfa_verified);
2138 assert_eq!(enhanced_session.active_factors.len(), 0);
2139 assert_eq!(enhanced_session.token_type, "bearer");
2140 }
2141}