1use std::sync::Arc;
2
3use base64::Engine;
4use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
5use serde_json::{json, Value as JsonValue};
6use sha2::{Digest, Sha256};
7use tokio::sync::{broadcast, Mutex, RwLock};
8use url::Url;
9
10use crate::admin::AdminClient;
11use crate::error::{AuthError, GoTrueErrorResponse};
12use crate::params::{
13 MfaChallengeParams, MfaEnrollParams, MfaVerifyParams, OAuthAuthorizeUrlParams,
14 OAuthTokenExchangeParams, ResendParams, SignInWithIdTokenParams, SsoSignInParams,
15 UpdateUserParams, VerifyOtpParams,
16};
17use crate::types::*;
18
19const EVENT_CHANNEL_CAPACITY: usize = 64;
21
22struct AuthClientInner {
23 http: reqwest::Client,
24 base_url: Url,
25 api_key: String,
26 session: RwLock<Option<Session>>,
28 event_tx: broadcast::Sender<AuthStateChange>,
29 auto_refresh_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
30}
31
32impl std::fmt::Debug for AuthClientInner {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("AuthClientInner")
35 .field("base_url", &self.base_url)
36 .field("api_key", &"***")
37 .finish()
38 }
39}
40
41#[derive(Debug, Clone)]
61pub struct AuthClient {
62 inner: Arc<AuthClientInner>,
63}
64
65impl AuthClient {
66 pub fn new(supabase_url: &str, api_key: &str) -> Result<Self, AuthError> {
71 let base = supabase_url.trim_end_matches('/');
72 let base_url = Url::parse(&format!("{}/auth/v1", base))?;
73
74 let mut default_headers = HeaderMap::new();
75 default_headers.insert(
76 "apikey",
77 HeaderValue::from_str(api_key)
78 .map_err(|e| AuthError::InvalidConfig(format!("Invalid API key header: {}", e)))?,
79 );
80 default_headers.insert(
81 CONTENT_TYPE,
82 HeaderValue::from_static("application/json"),
83 );
84
85 let http = reqwest::Client::builder()
86 .default_headers(default_headers)
87 .build()
88 .map_err(AuthError::Http)?;
89
90 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
91
92 Ok(Self {
93 inner: Arc::new(AuthClientInner {
94 http,
95 base_url,
96 api_key: api_key.to_string(),
97 session: RwLock::new(None),
98 event_tx,
99 auto_refresh_handle: Mutex::new(None),
100 }),
101 })
102 }
103
104 pub fn base_url(&self) -> &Url {
106 &self.inner.base_url
107 }
108
109 pub async fn get_session(&self) -> Option<Session> {
115 self.inner.session.read().await.clone()
116 }
117
118 pub async fn set_session(&self, session: Session) {
122 self.store_session(&session, AuthChangeEvent::SignedIn).await;
123 }
124
125 pub async fn clear_session(&self) {
130 self.emit_signed_out().await;
131 }
132
133 pub fn on_auth_state_change(&self) -> AuthSubscription {
140 AuthSubscription {
141 rx: self.inner.event_tx.subscribe(),
142 }
143 }
144
145 pub fn start_auto_refresh(&self) {
152 self.start_auto_refresh_with(AutoRefreshConfig::default());
153 }
154
155 pub fn start_auto_refresh_with(&self, config: AutoRefreshConfig) {
157 self.stop_auto_refresh_inner();
159
160 let inner = Arc::clone(&self.inner);
161 let handle = tokio::spawn(async move {
162 auto_refresh_loop(inner, config).await;
163 });
164
165 if let Ok(mut guard) = self.inner.auto_refresh_handle.try_lock() {
167 *guard = Some(handle);
168 }
169 }
170
171 pub fn stop_auto_refresh(&self) {
173 self.stop_auto_refresh_inner();
174 }
175
176 fn stop_auto_refresh_inner(&self) {
177 if let Ok(mut guard) = self.inner.auto_refresh_handle.try_lock() {
178 if let Some(handle) = guard.take() {
179 handle.abort();
180 }
181 }
182 }
183
184 pub async fn get_session_user(&self) -> Result<User, AuthError> {
190 let session = self.inner.session.read().await.clone();
191 match session {
192 Some(s) => self.get_user(&s.access_token).await,
193 None => Err(AuthError::NoSession),
194 }
195 }
196
197 pub async fn refresh_current_session(&self) -> Result<Session, AuthError> {
201 let session = self.inner.session.read().await.clone();
202 match session {
203 Some(s) => self.refresh_session(&s.refresh_token).await,
204 None => Err(AuthError::NoSession),
205 }
206 }
207
208 pub async fn sign_out_current(&self) -> Result<(), AuthError> {
212 self.sign_out_current_with_scope(SignOutScope::Global).await
213 }
214
215 pub async fn sign_out_current_with_scope(
219 &self,
220 scope: SignOutScope,
221 ) -> Result<(), AuthError> {
222 let session = self.inner.session.read().await.clone();
223 match session {
224 Some(s) => self.sign_out_with_scope(&s.access_token, scope).await,
225 None => Err(AuthError::NoSession),
226 }
227 }
228
229 pub async fn sign_up_with_email(
236 &self,
237 email: &str,
238 password: &str,
239 ) -> Result<AuthResponse, AuthError> {
240 self.sign_up_with_email_and_data(email, password, None).await
241 }
242
243 pub async fn sign_up_with_email_and_data(
248 &self,
249 email: &str,
250 password: &str,
251 data: Option<JsonValue>,
252 ) -> Result<AuthResponse, AuthError> {
253 let mut body = json!({
254 "email": email,
255 "password": password,
256 });
257 if let Some(data) = data {
258 body["data"] = data;
259 }
260
261 let url = self.url("/signup");
262 let resp = self.inner.http.post(url).json(&body).send().await?;
263 let auth_resp = self.handle_auth_response(resp).await?;
264 if let Some(session) = &auth_resp.session {
265 self.store_session(session, AuthChangeEvent::SignedIn).await;
266 }
267 Ok(auth_resp)
268 }
269
270 pub async fn sign_up_with_phone(
275 &self,
276 phone: &str,
277 password: &str,
278 ) -> Result<AuthResponse, AuthError> {
279 let body = json!({
280 "phone": phone,
281 "password": password,
282 });
283
284 let url = self.url("/signup");
285 let resp = self.inner.http.post(url).json(&body).send().await?;
286 let auth_resp = self.handle_auth_response(resp).await?;
287 if let Some(session) = &auth_resp.session {
288 self.store_session(session, AuthChangeEvent::SignedIn).await;
289 }
290 Ok(auth_resp)
291 }
292
293 pub async fn sign_in_with_password_email(
300 &self,
301 email: &str,
302 password: &str,
303 ) -> Result<Session, AuthError> {
304 let body = json!({
305 "email": email,
306 "password": password,
307 });
308
309 let url = self.url("/token?grant_type=password");
310 let resp = self.inner.http.post(url).json(&body).send().await?;
311 let session = self.handle_session_response(resp).await?;
312 self.store_session(&session, AuthChangeEvent::SignedIn).await;
313 Ok(session)
314 }
315
316 pub async fn sign_in_with_password_phone(
321 &self,
322 phone: &str,
323 password: &str,
324 ) -> Result<Session, AuthError> {
325 let body = json!({
326 "phone": phone,
327 "password": password,
328 });
329
330 let url = self.url("/token?grant_type=password");
331 let resp = self.inner.http.post(url).json(&body).send().await?;
332 let session = self.handle_session_response(resp).await?;
333 self.store_session(&session, AuthChangeEvent::SignedIn).await;
334 Ok(session)
335 }
336
337 pub async fn sign_in_with_otp_email(&self, email: &str) -> Result<(), AuthError> {
341 let body = json!({
342 "email": email,
343 });
344
345 let url = self.url("/otp");
346 let resp = self.inner.http.post(url).json(&body).send().await?;
347 self.handle_empty_response(resp).await
348 }
349
350 pub async fn sign_in_with_otp_phone(
354 &self,
355 phone: &str,
356 channel: OtpChannel,
357 ) -> Result<(), AuthError> {
358 let body = json!({
359 "phone": phone,
360 "channel": channel,
361 });
362
363 let url = self.url("/otp");
364 let resp = self.inner.http.post(url).json(&body).send().await?;
365 self.handle_empty_response(resp).await
366 }
367
368 pub async fn verify_otp(&self, params: VerifyOtpParams) -> Result<Session, AuthError> {
373 let url = self.url("/verify");
374 let resp = self.inner.http.post(url).json(¶ms).send().await?;
375 let session = self.handle_session_response(resp).await?;
376 self.store_session(&session, AuthChangeEvent::SignedIn).await;
377 Ok(session)
378 }
379
380 pub async fn sign_in_anonymous(&self) -> Result<Session, AuthError> {
385 let url = self.url("/signup");
386 let body = json!({});
387 let resp = self.inner.http.post(url).json(&body).send().await?;
388 let session = self.handle_session_response(resp).await?;
389 self.store_session(&session, AuthChangeEvent::SignedIn).await;
390 Ok(session)
391 }
392
393 pub fn get_oauth_sign_in_url(
401 &self,
402 provider: OAuthProvider,
403 redirect_to: Option<&str>,
404 scopes: Option<&str>,
405 ) -> Result<String, AuthError> {
406 let mut url = self.url("/authorize");
407 url.query_pairs_mut()
408 .append_pair("provider", &provider.to_string());
409
410 if let Some(redirect) = redirect_to {
411 url.query_pairs_mut()
412 .append_pair("redirect_to", redirect);
413 }
414 if let Some(scopes) = scopes {
415 url.query_pairs_mut().append_pair("scopes", scopes);
416 }
417
418 Ok(url.to_string())
419 }
420
421 pub async fn exchange_code_for_session(
426 &self,
427 code: &str,
428 code_verifier: Option<&str>,
429 ) -> Result<Session, AuthError> {
430 let mut body = json!({
431 "auth_code": code,
432 });
433 if let Some(verifier) = code_verifier {
434 body["code_verifier"] = json!(verifier);
435 }
436
437 let url = self.url("/token?grant_type=pkce");
438 let resp = self.inner.http.post(url).json(&body).send().await?;
439 let session = self.handle_session_response(resp).await?;
440 self.store_session(&session, AuthChangeEvent::SignedIn).await;
441 Ok(session)
442 }
443
444 pub async fn refresh_session(&self, refresh_token: &str) -> Result<Session, AuthError> {
451 let body = json!({
452 "refresh_token": refresh_token,
453 });
454
455 let url = self.url("/token?grant_type=refresh_token");
456 let resp = self.inner.http.post(url).json(&body).send().await?;
457 let session = self.handle_session_response(resp).await?;
458 self.store_session(&session, AuthChangeEvent::TokenRefreshed).await;
459 Ok(session)
460 }
461
462 pub async fn get_user(&self, access_token: &str) -> Result<User, AuthError> {
470 let url = self.url("/user");
471 let resp = self
472 .inner
473 .http
474 .get(url)
475 .bearer_auth(access_token)
476 .send()
477 .await?;
478 self.handle_user_response(resp).await
479 }
480
481 pub async fn update_user(
486 &self,
487 access_token: &str,
488 params: UpdateUserParams,
489 ) -> Result<User, AuthError> {
490 let url = self.url("/user");
491 let resp = self
492 .inner
493 .http
494 .put(url)
495 .bearer_auth(access_token)
496 .json(¶ms)
497 .send()
498 .await?;
499 let user = self.handle_user_response(resp).await?;
500
501 let mut guard = self.inner.session.write().await;
503 let session_clone = if let Some(session) = guard.as_mut() {
504 session.user = user.clone();
505 Some(session.clone())
506 } else {
507 None
508 };
509 drop(guard);
510
511 let _ = self.inner.event_tx.send(AuthStateChange {
512 event: AuthChangeEvent::UserUpdated,
513 session: session_clone,
514 });
515
516 Ok(user)
517 }
518
519 pub async fn sign_out(&self, access_token: &str) -> Result<(), AuthError> {
526 self.sign_out_with_scope(access_token, SignOutScope::Global)
527 .await
528 }
529
530 pub async fn sign_out_with_scope(
535 &self,
536 access_token: &str,
537 scope: SignOutScope,
538 ) -> Result<(), AuthError> {
539 let url = self.url(&format!("/logout?scope={}", scope));
540 let resp = self
541 .inner
542 .http
543 .post(url)
544 .bearer_auth(access_token)
545 .send()
546 .await?;
547 self.handle_empty_response(resp).await?;
548 self.emit_signed_out().await;
549 Ok(())
550 }
551
552 pub async fn reset_password_for_email(
558 &self,
559 email: &str,
560 redirect_to: Option<&str>,
561 ) -> Result<(), AuthError> {
562 let mut body = json!({ "email": email });
563 if let Some(redirect) = redirect_to {
564 body["redirect_to"] = json!(redirect);
565 }
566
567 let url = self.url("/recover");
568 let resp = self.inner.http.post(url).json(&body).send().await?;
569 self.handle_empty_response(resp).await
570 }
571
572 pub fn admin(&self) -> AdminClient<'_> {
580 AdminClient::new(self)
581 }
582
583 pub fn admin_with_key<'a>(&'a self, service_role_key: &'a str) -> AdminClient<'a> {
585 AdminClient::with_key(self, service_role_key)
586 }
587
588 pub async fn mfa_enroll(
594 &self,
595 access_token: &str,
596 params: MfaEnrollParams,
597 ) -> Result<MfaEnrollResponse, AuthError> {
598 let url = self.url("/factors");
599 let resp = self
600 .inner
601 .http
602 .post(url)
603 .bearer_auth(access_token)
604 .json(¶ms)
605 .send()
606 .await?;
607 let status = resp.status().as_u16();
608 if status >= 400 {
609 return Err(self.parse_error(status, resp).await);
610 }
611 let body: MfaEnrollResponse = resp.json().await?;
612 Ok(body)
613 }
614
615 pub async fn mfa_challenge(
619 &self,
620 access_token: &str,
621 factor_id: &str,
622 ) -> Result<MfaChallengeResponse, AuthError> {
623 self.mfa_challenge_with_params(access_token, factor_id, MfaChallengeParams::default())
624 .await
625 }
626
627 pub async fn mfa_challenge_with_params(
631 &self,
632 access_token: &str,
633 factor_id: &str,
634 params: MfaChallengeParams,
635 ) -> Result<MfaChallengeResponse, AuthError> {
636 let url = self.url(&format!("/factors/{}/challenge", factor_id));
637 let resp = self
638 .inner
639 .http
640 .post(url)
641 .bearer_auth(access_token)
642 .json(¶ms)
643 .send()
644 .await?;
645 let status = resp.status().as_u16();
646 if status >= 400 {
647 return Err(self.parse_error(status, resp).await);
648 }
649 let body: MfaChallengeResponse = resp.json().await?;
650 Ok(body)
651 }
652
653 pub async fn mfa_verify(
658 &self,
659 access_token: &str,
660 factor_id: &str,
661 params: MfaVerifyParams,
662 ) -> Result<Session, AuthError> {
663 let url = self.url(&format!("/factors/{}/verify", factor_id));
664 let resp = self
665 .inner
666 .http
667 .post(url)
668 .bearer_auth(access_token)
669 .json(¶ms)
670 .send()
671 .await?;
672 let session = self.handle_session_response(resp).await?;
673 self.store_session(&session, AuthChangeEvent::SignedIn).await;
674 Ok(session)
675 }
676
677 pub async fn mfa_challenge_and_verify(
682 &self,
683 access_token: &str,
684 factor_id: &str,
685 code: &str,
686 ) -> Result<Session, AuthError> {
687 let challenge = self.mfa_challenge(access_token, factor_id).await?;
688 self.mfa_verify(
689 access_token,
690 factor_id,
691 MfaVerifyParams::new(&challenge.id, code),
692 )
693 .await
694 }
695
696 pub async fn mfa_unenroll(
700 &self,
701 access_token: &str,
702 factor_id: &str,
703 ) -> Result<MfaUnenrollResponse, AuthError> {
704 let url = self.url(&format!("/factors/{}", factor_id));
705 let resp = self
706 .inner
707 .http
708 .delete(url)
709 .bearer_auth(access_token)
710 .send()
711 .await?;
712 let status = resp.status().as_u16();
713 if status >= 400 {
714 return Err(self.parse_error(status, resp).await);
715 }
716 let body: MfaUnenrollResponse = resp.json().await?;
717 Ok(body)
718 }
719
720 pub async fn mfa_list_factors(
726 &self,
727 access_token: &str,
728 ) -> Result<MfaListFactorsResponse, AuthError> {
729 let user = self.get_user(access_token).await?;
730 let all = user.factors.unwrap_or_default();
731 let totp = all
732 .iter()
733 .filter(|f| f.factor_type == "totp")
734 .cloned()
735 .collect();
736 let phone = all
737 .iter()
738 .filter(|f| f.factor_type == "phone")
739 .cloned()
740 .collect();
741 Ok(MfaListFactorsResponse { totp, phone, all })
742 }
743
744 pub async fn mfa_get_authenticator_assurance_level(
750 &self,
751 access_token: &str,
752 ) -> Result<AuthenticatorAssuranceLevelInfo, AuthError> {
753 let user = self.get_user(access_token).await?;
754 let factors = user.factors.unwrap_or_default();
755
756 let amr = parse_amr_from_jwt(access_token);
758
759 let has_mfa_amr = amr.iter().any(|e| e.method == "totp" || e.method == "phone");
761 let current_level = if !amr.is_empty() {
762 if has_mfa_amr {
763 Some(AuthenticatorAssuranceLevel::Aal2)
764 } else {
765 Some(AuthenticatorAssuranceLevel::Aal1)
766 }
767 } else {
768 Some(AuthenticatorAssuranceLevel::Aal1)
769 };
770
771 let has_verified_factor = factors.iter().any(|f| f.status == "verified");
773 let next_level = if has_verified_factor {
774 Some(AuthenticatorAssuranceLevel::Aal2)
775 } else {
776 Some(AuthenticatorAssuranceLevel::Aal1)
777 };
778
779 Ok(AuthenticatorAssuranceLevelInfo {
780 current_level,
781 next_level,
782 current_authentication_methods: amr,
783 })
784 }
785
786 pub async fn sign_in_with_sso(
792 &self,
793 params: SsoSignInParams,
794 ) -> Result<SsoSignInResponse, AuthError> {
795 let url = self.url("/sso");
796 let resp = self.inner.http.post(url).json(¶ms).send().await?;
797 let status = resp.status().as_u16();
798 if status >= 400 {
799 return Err(self.parse_error(status, resp).await);
800 }
801 let body: SsoSignInResponse = resp.json().await?;
802 Ok(body)
803 }
804
805 pub async fn sign_in_with_id_token(
812 &self,
813 params: SignInWithIdTokenParams,
814 ) -> Result<Session, AuthError> {
815 let url = self.url("/token?grant_type=id_token");
816 let resp = self.inner.http.post(url).json(¶ms).send().await?;
817 let session = self.handle_session_response(resp).await?;
818 self.store_session(&session, AuthChangeEvent::SignedIn).await;
819 Ok(session)
820 }
821
822 pub async fn link_identity(
830 &self,
831 access_token: &str,
832 provider: OAuthProvider,
833 ) -> Result<LinkIdentityResponse, AuthError> {
834 let mut url = self.url("/user/identities/authorize");
835 url.query_pairs_mut()
836 .append_pair("provider", &provider.to_string())
837 .append_pair("skip_http_redirect", "true");
838 let resp = self
839 .inner
840 .http
841 .get(url)
842 .bearer_auth(access_token)
843 .send()
844 .await?;
845 let status = resp.status().as_u16();
846 if status >= 400 {
847 return Err(self.parse_error(status, resp).await);
848 }
849 let body: JsonValue = resp.json().await?;
851 let redirect_url = body
852 .get("url")
853 .and_then(|v| v.as_str())
854 .unwrap_or_default()
855 .to_string();
856 Ok(LinkIdentityResponse { url: redirect_url })
857 }
858
859 pub async fn unlink_identity(
863 &self,
864 access_token: &str,
865 identity_id: &str,
866 ) -> Result<(), AuthError> {
867 let url = self.url(&format!("/user/identities/{}", identity_id));
868 let resp = self
869 .inner
870 .http
871 .delete(url)
872 .bearer_auth(access_token)
873 .send()
874 .await?;
875 self.handle_empty_response(resp).await
876 }
877
878 pub async fn resend(&self, params: ResendParams) -> Result<(), AuthError> {
884 let url = self.url("/resend");
885 let resp = self.inner.http.post(url).json(¶ms).send().await?;
886 self.handle_empty_response(resp).await
887 }
888
889 pub async fn reauthenticate(&self, access_token: &str) -> Result<(), AuthError> {
895 let url = self.url("/reauthenticate");
896 let resp = self
897 .inner
898 .http
899 .get(url)
900 .bearer_auth(access_token)
901 .send()
902 .await?;
903 self.handle_empty_response(resp).await
904 }
905
906 pub async fn get_user_identities(
914 &self,
915 access_token: &str,
916 ) -> Result<Vec<Identity>, AuthError> {
917 let user = self.get_user(access_token).await?;
918 Ok(user.identities.unwrap_or_default())
919 }
920
921 pub async fn oauth_get_authorization_details(
929 &self,
930 access_token: &str,
931 authorization_id: &str,
932 ) -> Result<OAuthAuthorizationDetailsResponse, AuthError> {
933 let url = self.url(&format!("/oauth/authorizations/{}", authorization_id));
934 let resp = self
935 .inner
936 .http
937 .get(url)
938 .bearer_auth(access_token)
939 .send()
940 .await?;
941 let status = resp.status().as_u16();
942 if status >= 400 {
943 return Err(self.parse_error(status, resp).await);
944 }
945 let body: OAuthAuthorizationDetailsResponse = resp.json().await?;
946 Ok(body)
947 }
948
949 pub async fn oauth_approve_authorization(
953 &self,
954 access_token: &str,
955 authorization_id: &str,
956 ) -> Result<OAuthRedirect, AuthError> {
957 self.oauth_consent_action(access_token, authorization_id, "approve")
958 .await
959 }
960
961 pub async fn oauth_deny_authorization(
965 &self,
966 access_token: &str,
967 authorization_id: &str,
968 ) -> Result<OAuthRedirect, AuthError> {
969 self.oauth_consent_action(access_token, authorization_id, "deny")
970 .await
971 }
972
973 pub async fn oauth_list_grants(
977 &self,
978 access_token: &str,
979 ) -> Result<Vec<OAuthGrant>, AuthError> {
980 let url = self.url("/user/oauth/grants");
981 let resp = self
982 .inner
983 .http
984 .get(url)
985 .bearer_auth(access_token)
986 .send()
987 .await?;
988 let status = resp.status().as_u16();
989 if status >= 400 {
990 return Err(self.parse_error(status, resp).await);
991 }
992 let grants: Vec<OAuthGrant> = resp.json().await?;
993 Ok(grants)
994 }
995
996 pub async fn oauth_revoke_grant(
1000 &self,
1001 access_token: &str,
1002 client_id: &str,
1003 ) -> Result<(), AuthError> {
1004 let mut url = self.url("/user/oauth/grants");
1005 url.query_pairs_mut()
1006 .append_pair("client_id", client_id);
1007 let resp = self
1008 .inner
1009 .http
1010 .delete(url)
1011 .bearer_auth(access_token)
1012 .send()
1013 .await?;
1014 self.handle_empty_response(resp).await
1015 }
1016
1017 async fn oauth_consent_action(
1018 &self,
1019 access_token: &str,
1020 authorization_id: &str,
1021 action: &str,
1022 ) -> Result<OAuthRedirect, AuthError> {
1023 let url = self.url(&format!(
1024 "/oauth/authorizations/{}/consent",
1025 authorization_id
1026 ));
1027 let body = serde_json::json!({ "action": action });
1028 let resp = self
1029 .inner
1030 .http
1031 .post(url)
1032 .bearer_auth(access_token)
1033 .json(&body)
1034 .send()
1035 .await?;
1036 let status = resp.status().as_u16();
1037 if status >= 400 {
1038 return Err(self.parse_error(status, resp).await);
1039 }
1040 let redirect: OAuthRedirect = resp.json().await?;
1041 Ok(redirect)
1042 }
1043
1044 pub fn generate_pkce_pair() -> PkcePair {
1051 use rand::Rng;
1052
1053 let mut rng = rand::rng();
1055 let random_bytes: [u8; 32] = rng.random();
1056 let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(random_bytes);
1057
1058 let hash = Sha256::digest(verifier.as_bytes());
1060 let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash);
1061
1062 PkcePair {
1063 verifier: PkceCodeVerifier(verifier),
1064 challenge: PkceCodeChallenge(challenge),
1065 }
1066 }
1067
1068 pub fn build_oauth_authorize_url(&self, params: &OAuthAuthorizeUrlParams) -> String {
1072 let mut url = self.url("/oauth/authorize");
1073 {
1074 let mut pairs = url.query_pairs_mut();
1075 pairs.append_pair("client_id", ¶ms.client_id);
1076 pairs.append_pair("redirect_uri", ¶ms.redirect_uri);
1077 pairs.append_pair("response_type", "code");
1078
1079 if let Some(scope) = ¶ms.scope {
1080 pairs.append_pair("scope", scope);
1081 }
1082 if let Some(state) = ¶ms.state {
1083 pairs.append_pair("state", state);
1084 }
1085 if let Some(challenge) = ¶ms.code_challenge {
1086 pairs.append_pair("code_challenge", challenge);
1087 if let Some(method) = ¶ms.code_challenge_method {
1088 pairs.append_pair("code_challenge_method", method);
1089 }
1090 }
1091 }
1092 url.to_string()
1093 }
1094
1095 pub async fn oauth_token_exchange(
1100 &self,
1101 params: OAuthTokenExchangeParams,
1102 ) -> Result<OAuthTokenResponse, AuthError> {
1103 let url = self.url("/oauth/token");
1104 let mut form: Vec<(&str, String)> = vec![
1105 ("grant_type", "authorization_code".to_string()),
1106 ("code", params.code.clone()),
1107 ("redirect_uri", params.redirect_uri.clone()),
1108 ("client_id", params.client_id.clone()),
1109 ];
1110 if let Some(secret) = ¶ms.client_secret {
1111 form.push(("client_secret", secret.clone()));
1112 }
1113 if let Some(verifier) = ¶ms.code_verifier {
1114 form.push(("code_verifier", verifier.clone()));
1115 }
1116
1117 let resp = self.inner.http.post(url).form(&form).send().await?;
1118 let status = resp.status().as_u16();
1119 if status >= 400 {
1120 return Err(self.parse_error(status, resp).await);
1121 }
1122 let token_resp: OAuthTokenResponse = resp.json().await?;
1123 Ok(token_resp)
1124 }
1125
1126 pub async fn oauth_token_refresh(
1130 &self,
1131 client_id: &str,
1132 refresh_token: &str,
1133 client_secret: Option<&str>,
1134 ) -> Result<OAuthTokenResponse, AuthError> {
1135 let url = self.url("/oauth/token");
1136 let mut form: Vec<(&str, String)> = vec![
1137 ("grant_type", "refresh_token".to_string()),
1138 ("refresh_token", refresh_token.to_string()),
1139 ("client_id", client_id.to_string()),
1140 ];
1141 if let Some(secret) = client_secret {
1142 form.push(("client_secret", secret.to_string()));
1143 }
1144
1145 let resp = self.inner.http.post(url).form(&form).send().await?;
1146 let status = resp.status().as_u16();
1147 if status >= 400 {
1148 return Err(self.parse_error(status, resp).await);
1149 }
1150 let token_resp: OAuthTokenResponse = resp.json().await?;
1151 Ok(token_resp)
1152 }
1153
1154 pub async fn oauth_revoke_token(
1158 &self,
1159 token: &str,
1160 token_type_hint: Option<&str>,
1161 ) -> Result<(), AuthError> {
1162 let url = self.url("/oauth/revoke");
1163 let mut form: Vec<(&str, String)> = vec![("token", token.to_string())];
1164 if let Some(hint) = token_type_hint {
1165 form.push(("token_type_hint", hint.to_string()));
1166 }
1167
1168 let resp = self.inner.http.post(url).form(&form).send().await?;
1169 self.handle_empty_response(resp).await
1170 }
1171
1172 pub async fn oauth_get_openid_configuration(
1176 &self,
1177 ) -> Result<OpenIdConfiguration, AuthError> {
1178 let url = self.url("/.well-known/openid-configuration");
1179 let resp = self.inner.http.get(url).send().await?;
1180 let status = resp.status().as_u16();
1181 if status >= 400 {
1182 return Err(self.parse_error(status, resp).await);
1183 }
1184 let config: OpenIdConfiguration = resp.json().await?;
1185 Ok(config)
1186 }
1187
1188 pub async fn oauth_get_jwks(&self) -> Result<JwksResponse, AuthError> {
1192 let url = self.url("/.well-known/jwks.json");
1193 let resp = self.inner.http.get(url).send().await?;
1194 let status = resp.status().as_u16();
1195 if status >= 400 {
1196 return Err(self.parse_error(status, resp).await);
1197 }
1198 let jwks: JwksResponse = resp.json().await?;
1199 Ok(jwks)
1200 }
1201
1202 pub async fn oauth_get_userinfo(
1206 &self,
1207 access_token: &str,
1208 ) -> Result<JsonValue, AuthError> {
1209 let url = self.url("/oauth/userinfo");
1210 let resp = self
1211 .inner
1212 .http
1213 .get(url)
1214 .bearer_auth(access_token)
1215 .send()
1216 .await?;
1217 let status = resp.status().as_u16();
1218 if status >= 400 {
1219 return Err(self.parse_error(status, resp).await);
1220 }
1221 let userinfo: JsonValue = resp.json().await?;
1222 Ok(userinfo)
1223 }
1224
1225 pub fn get_claims(token: &str) -> Result<JsonValue, AuthError> {
1234 let parts: Vec<&str> = token.split('.').collect();
1235 if parts.len() != 3 {
1236 return Err(AuthError::InvalidToken(
1237 "JWT must have 3 parts separated by '.'".to_string(),
1238 ));
1239 }
1240
1241 let payload_b64 = parts[1];
1242 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
1243 .decode(payload_b64)
1244 .map_err(|e| AuthError::InvalidToken(format!("Invalid base64 in JWT payload: {}", e)))?;
1245
1246 serde_json::from_slice(&decoded)
1247 .map_err(|e| AuthError::InvalidToken(format!("Invalid JSON in JWT payload: {}", e)))
1248 }
1249
1250 pub async fn sign_up_with_email_full(
1258 &self,
1259 email: &str,
1260 password: &str,
1261 data: Option<JsonValue>,
1262 captcha_token: Option<&str>,
1263 ) -> Result<AuthResponse, AuthError> {
1264 let mut body = json!({
1265 "email": email,
1266 "password": password,
1267 });
1268 if let Some(data) = data {
1269 body["data"] = data;
1270 }
1271 if let Some(token) = captcha_token {
1272 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1273 }
1274
1275 let url = self.url("/signup");
1276 let resp = self.inner.http.post(url).json(&body).send().await?;
1277 let auth_resp = self.handle_auth_response(resp).await?;
1278 if let Some(session) = &auth_resp.session {
1279 self.store_session(session, AuthChangeEvent::SignedIn).await;
1280 }
1281 Ok(auth_resp)
1282 }
1283
1284 pub async fn sign_in_with_password_email_captcha(
1286 &self,
1287 email: &str,
1288 password: &str,
1289 captcha_token: Option<&str>,
1290 ) -> Result<Session, AuthError> {
1291 let mut body = json!({
1292 "email": email,
1293 "password": password,
1294 });
1295 if let Some(token) = captcha_token {
1296 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1297 }
1298
1299 let url = self.url("/token?grant_type=password");
1300 let resp = self.inner.http.post(url).json(&body).send().await?;
1301 let session = self.handle_session_response(resp).await?;
1302 self.store_session(&session, AuthChangeEvent::SignedIn).await;
1303 Ok(session)
1304 }
1305
1306 pub async fn sign_in_with_otp_email_captcha(
1308 &self,
1309 email: &str,
1310 captcha_token: Option<&str>,
1311 ) -> Result<(), AuthError> {
1312 let mut body = json!({
1313 "email": email,
1314 });
1315 if let Some(token) = captcha_token {
1316 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1317 }
1318
1319 let url = self.url("/otp");
1320 let resp = self.inner.http.post(url).json(&body).send().await?;
1321 self.handle_empty_response(resp).await
1322 }
1323
1324 pub async fn sign_in_with_otp_phone_captcha(
1326 &self,
1327 phone: &str,
1328 channel: OtpChannel,
1329 captcha_token: Option<&str>,
1330 ) -> Result<(), AuthError> {
1331 let mut body = json!({
1332 "phone": phone,
1333 "channel": channel,
1334 });
1335 if let Some(token) = captcha_token {
1336 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1337 }
1338
1339 let url = self.url("/otp");
1340 let resp = self.inner.http.post(url).json(&body).send().await?;
1341 self.handle_empty_response(resp).await
1342 }
1343
1344 pub async fn sign_in_with_web3(
1351 &self,
1352 params: Web3SignInParams,
1353 ) -> Result<Session, AuthError> {
1354 let body = json!({
1355 "chain": params.chain,
1356 "address": params.address,
1357 "message": params.message,
1358 "signature": params.signature,
1359 "nonce": params.nonce,
1360 });
1361
1362 let url = self.url("/token?grant_type=web3");
1363 let resp = self.inner.http.post(url).json(&body).send().await?;
1364 let session = self.handle_session_response(resp).await?;
1365 self.store_session(&session, AuthChangeEvent::SignedIn).await;
1366 Ok(session)
1367 }
1368
1369 async fn store_session(&self, session: &Session, event: AuthChangeEvent) {
1373 *self.inner.session.write().await = Some(session.clone());
1374 let _ = self.inner.event_tx.send(AuthStateChange {
1375 event,
1376 session: Some(session.clone()),
1377 });
1378 }
1379
1380 async fn emit_signed_out(&self) {
1382 *self.inner.session.write().await = None;
1383 let _ = self.inner.event_tx.send(AuthStateChange {
1384 event: AuthChangeEvent::SignedOut,
1385 session: None,
1386 });
1387 }
1388
1389 pub(crate) fn url(&self, path: &str) -> Url {
1390 let mut url = self.inner.base_url.clone();
1391 let current = url.path().to_string();
1392 if let Some(query_start) = path.find('?') {
1394 url.set_path(&format!("{}{}", current, &path[..query_start]));
1395 url.set_query(Some(&path[query_start + 1..]));
1396 } else {
1397 url.set_path(&format!("{}{}", current, path));
1398 }
1399 url
1400 }
1401
1402 pub(crate) fn http(&self) -> &reqwest::Client {
1403 &self.inner.http
1404 }
1405
1406 pub(crate) fn api_key(&self) -> &str {
1407 &self.inner.api_key
1408 }
1409
1410 async fn handle_auth_response(
1411 &self,
1412 resp: reqwest::Response,
1413 ) -> Result<AuthResponse, AuthError> {
1414 let status = resp.status().as_u16();
1415 if status >= 400 {
1416 return Err(self.parse_error(status, resp).await);
1417 }
1418
1419 let body: AuthResponse = resp.json().await?;
1420 Ok(body)
1421 }
1422
1423 async fn handle_session_response(
1424 &self,
1425 resp: reqwest::Response,
1426 ) -> Result<Session, AuthError> {
1427 let status = resp.status().as_u16();
1428 if status >= 400 {
1429 return Err(self.parse_error(status, resp).await);
1430 }
1431
1432 let session: Session = resp.json().await?;
1433 Ok(session)
1434 }
1435
1436 pub(crate) async fn handle_user_response(
1437 &self,
1438 resp: reqwest::Response,
1439 ) -> Result<User, AuthError> {
1440 let status = resp.status().as_u16();
1441 if status >= 400 {
1442 return Err(self.parse_error(status, resp).await);
1443 }
1444
1445 let user: User = resp.json().await?;
1446 Ok(user)
1447 }
1448
1449 pub(crate) async fn handle_empty_response(
1450 &self,
1451 resp: reqwest::Response,
1452 ) -> Result<(), AuthError> {
1453 let status = resp.status().as_u16();
1454 if status >= 400 {
1455 return Err(self.parse_error(status, resp).await);
1456 }
1457 Ok(())
1458 }
1459
1460 async fn parse_error(&self, status: u16, resp: reqwest::Response) -> AuthError {
1461 match resp.json::<GoTrueErrorResponse>().await {
1462 Ok(err_resp) => {
1463 let error_code = err_resp
1464 .error_code
1465 .as_deref()
1466 .map(|s| s.into());
1467 AuthError::Api {
1468 status,
1469 message: err_resp.error_message(),
1470 error_code,
1471 }
1472 }
1473 Err(_) => AuthError::Api {
1474 status,
1475 message: format!("HTTP {}", status),
1476 error_code: None,
1477 },
1478 }
1479 }
1480}
1481
1482async fn auto_refresh_loop(inner: Arc<AuthClientInner>, config: AutoRefreshConfig) {
1484 let mut retries = 0u32;
1485 loop {
1486 tokio::time::sleep(config.check_interval).await;
1487
1488 let session = inner.session.read().await.clone();
1489 if let Some(session) = session {
1490 if should_refresh(&session, &config.refresh_margin) {
1491 match refresh_session_internal(&inner, &session.refresh_token).await {
1492 Ok(new_session) => {
1493 *inner.session.write().await = Some(new_session.clone());
1494 let _ = inner.event_tx.send(AuthStateChange {
1495 event: AuthChangeEvent::TokenRefreshed,
1496 session: Some(new_session),
1497 });
1498 retries = 0;
1499 }
1500 Err(_) => {
1501 retries += 1;
1502 if retries >= config.max_retries {
1503 *inner.session.write().await = None;
1504 let _ = inner.event_tx.send(AuthStateChange {
1505 event: AuthChangeEvent::SignedOut,
1506 session: None,
1507 });
1508 break;
1509 }
1510 }
1511 }
1512 }
1513 }
1514 }
1515}
1516
1517fn should_refresh(session: &Session, margin: &std::time::Duration) -> bool {
1519 session.expires_at.map(|exp| {
1520 let now = std::time::SystemTime::now()
1521 .duration_since(std::time::UNIX_EPOCH)
1522 .unwrap_or_default()
1523 .as_secs() as i64;
1524 exp - now < margin.as_secs() as i64
1525 }).unwrap_or(false)
1526}
1527
1528async fn refresh_session_internal(
1530 inner: &AuthClientInner,
1531 refresh_token: &str,
1532) -> Result<Session, AuthError> {
1533 let body = json!({
1534 "refresh_token": refresh_token,
1535 });
1536
1537 let mut url = inner.base_url.clone();
1538 let current = url.path().to_string();
1539 url.set_path(&format!("{}/token", current));
1540 url.set_query(Some("grant_type=refresh_token"));
1541
1542 let resp = inner.http.post(url).json(&body).send().await?;
1543 let status = resp.status().as_u16();
1544 if status >= 400 {
1545 return Err(AuthError::Api {
1546 status,
1547 message: format!("Token refresh failed (HTTP {})", status),
1548 error_code: None,
1549 });
1550 }
1551
1552 let session: Session = resp.json().await?;
1553 Ok(session)
1554}
1555
1556fn parse_amr_from_jwt(token: &str) -> Vec<AmrEntry> {
1561 let parts: Vec<&str> = token.split('.').collect();
1562 if parts.len() != 3 {
1563 return Vec::new();
1564 }
1565
1566 let payload_b64 = parts[1];
1567 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
1568 .decode(payload_b64)
1569 .ok();
1570 let decoded = match decoded {
1571 Some(d) => d,
1572 None => return Vec::new(),
1573 };
1574
1575 let payload: JsonValue = match serde_json::from_slice(&decoded) {
1576 Ok(v) => v,
1577 Err(_) => return Vec::new(),
1578 };
1579
1580 match payload.get("amr") {
1581 Some(amr_val) => serde_json::from_value::<Vec<AmrEntry>>(amr_val.clone()).unwrap_or_default(),
1582 None => Vec::new(),
1583 }
1584}
1585
1586#[cfg(test)]
1587mod tests {
1588 use super::*;
1589
1590 #[test]
1591 fn test_oauth_url_google() {
1592 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1593 let url = client
1594 .get_oauth_sign_in_url(OAuthProvider::Google, None, None)
1595 .unwrap();
1596 assert!(url.contains("/auth/v1/authorize"));
1597 assert!(url.contains("provider=google"));
1598 }
1599
1600 #[test]
1601 fn test_oauth_url_with_redirect_and_scopes() {
1602 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1603 let url = client
1604 .get_oauth_sign_in_url(
1605 OAuthProvider::GitHub,
1606 Some("https://myapp.com/callback"),
1607 Some("read:user"),
1608 )
1609 .unwrap();
1610 assert!(url.contains("provider=github"));
1611 assert!(url.contains("redirect_to="));
1612 assert!(url.contains("scopes="));
1613 }
1614
1615 #[test]
1616 fn test_oauth_url_custom_provider() {
1617 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1618 let url = client
1619 .get_oauth_sign_in_url(OAuthProvider::Custom("myidp".into()), None, None)
1620 .unwrap();
1621 assert!(url.contains("provider=myidp"));
1622 }
1623
1624 #[test]
1625 fn test_url_building() {
1626 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1627 let url = client.url("/signup");
1628 assert_eq!(url.path(), "/auth/v1/signup");
1629 assert!(url.query().is_none());
1630
1631 let url = client.url("/token?grant_type=password");
1632 assert_eq!(url.path(), "/auth/v1/token");
1633 assert_eq!(url.query(), Some("grant_type=password"));
1634 }
1635
1636 #[test]
1637 fn test_url_building_trailing_slash() {
1638 let client = AuthClient::new("https://example.supabase.co/", "test-key").unwrap();
1639 let url = client.url("/signup");
1640 assert_eq!(url.path(), "/auth/v1/signup");
1641 }
1642
1643 #[test]
1644 fn test_base_url() {
1645 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1646 assert_eq!(client.base_url().path(), "/auth/v1");
1647 }
1648
1649 #[test]
1652 fn test_generate_pkce_pair() {
1653 let pair = AuthClient::generate_pkce_pair();
1654 assert_eq!(pair.verifier.as_str().len(), 43);
1656 assert_eq!(pair.challenge.as_str().len(), 43);
1658 assert_ne!(pair.verifier.as_str(), pair.challenge.as_str());
1660 }
1661
1662 #[test]
1663 fn test_pkce_pair_is_deterministic_for_same_verifier() {
1664 use sha2::{Digest, Sha256};
1666 let pair = AuthClient::generate_pkce_pair();
1667 let hash = Sha256::digest(pair.verifier.as_str().as_bytes());
1668 let expected_challenge =
1669 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash);
1670 assert_eq!(pair.challenge.as_str(), expected_challenge);
1671 }
1672
1673 #[test]
1674 fn test_pkce_pairs_are_unique() {
1675 let pair1 = AuthClient::generate_pkce_pair();
1676 let pair2 = AuthClient::generate_pkce_pair();
1677 assert_ne!(pair1.verifier.as_str(), pair2.verifier.as_str());
1678 assert_ne!(pair1.challenge.as_str(), pair2.challenge.as_str());
1679 }
1680
1681 #[test]
1684 fn test_build_oauth_authorize_url_basic() {
1685 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1686 let params = OAuthAuthorizeUrlParams::new("client-abc", "https://app.com/callback");
1687 let url = client.build_oauth_authorize_url(¶ms);
1688 assert!(url.contains("/auth/v1/oauth/authorize"));
1689 assert!(url.contains("client_id=client-abc"));
1690 assert!(url.contains("redirect_uri="));
1691 assert!(url.contains("response_type=code"));
1692 }
1693
1694 #[test]
1695 fn test_build_oauth_authorize_url_with_scope_and_state() {
1696 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1697 let params = OAuthAuthorizeUrlParams::new("client-abc", "https://app.com/callback")
1698 .scope("openid profile")
1699 .state("csrf-token");
1700 let url = client.build_oauth_authorize_url(¶ms);
1701 assert!(url.contains("scope=openid+profile"));
1702 assert!(url.contains("state=csrf-token"));
1703 }
1704
1705 #[test]
1706 fn test_build_oauth_authorize_url_with_pkce() {
1707 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1708 let pkce = AuthClient::generate_pkce_pair();
1709 let params = OAuthAuthorizeUrlParams::new("client-abc", "https://app.com/callback")
1710 .pkce(&pkce.challenge);
1711 let url = client.build_oauth_authorize_url(¶ms);
1712 assert!(url.contains(&format!("code_challenge={}", pkce.challenge.as_str())));
1713 assert!(url.contains("code_challenge_method=S256"));
1714 }
1715
1716 #[tokio::test]
1719 async fn test_new_client_has_no_session() {
1720 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1721 assert!(client.get_session().await.is_none());
1722 }
1723
1724 #[tokio::test]
1725 async fn test_set_session_stores() {
1726 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1727 let session = make_test_session();
1728 client.set_session(session.clone()).await;
1729 let stored = client.get_session().await.unwrap();
1730 assert_eq!(stored.access_token, session.access_token);
1731 }
1732
1733 #[tokio::test]
1734 async fn test_clear_session() {
1735 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1736 client.set_session(make_test_session()).await;
1737 assert!(client.get_session().await.is_some());
1738 client.clear_session().await;
1739 assert!(client.get_session().await.is_none());
1740 }
1741
1742 #[tokio::test]
1743 async fn test_event_emitted_on_set_session() {
1744 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1745 let mut sub = client.on_auth_state_change();
1746 client.set_session(make_test_session()).await;
1747 let event = tokio::time::timeout(
1748 std::time::Duration::from_millis(100),
1749 sub.next(),
1750 ).await.unwrap().unwrap();
1751 assert_eq!(event.event, AuthChangeEvent::SignedIn);
1752 assert!(event.session.is_some());
1753 }
1754
1755 #[tokio::test]
1756 async fn test_event_emitted_on_clear_session() {
1757 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1758 let mut sub = client.on_auth_state_change();
1759 client.clear_session().await;
1760 let event = tokio::time::timeout(
1761 std::time::Duration::from_millis(100),
1762 sub.next(),
1763 ).await.unwrap().unwrap();
1764 assert_eq!(event.event, AuthChangeEvent::SignedOut);
1765 assert!(event.session.is_none());
1766 }
1767
1768 #[tokio::test]
1769 async fn test_multiple_subscribers() {
1770 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1771 let mut sub1 = client.on_auth_state_change();
1772 let mut sub2 = client.on_auth_state_change();
1773 client.set_session(make_test_session()).await;
1774
1775 let timeout = std::time::Duration::from_millis(100);
1776 let e1 = tokio::time::timeout(timeout, sub1.next()).await.unwrap().unwrap();
1777 let e2 = tokio::time::timeout(timeout, sub2.next()).await.unwrap().unwrap();
1778 assert_eq!(e1.event, AuthChangeEvent::SignedIn);
1779 assert_eq!(e2.event, AuthChangeEvent::SignedIn);
1780 }
1781
1782 #[tokio::test]
1783 async fn test_no_session_error() {
1784 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1785 let err = client.get_session_user().await.unwrap_err();
1786 assert!(matches!(err, AuthError::NoSession));
1787 }
1788
1789 #[tokio::test]
1790 async fn test_should_refresh_logic() {
1791 let margin = std::time::Duration::from_secs(60);
1792
1793 let mut session = make_test_session();
1795 session.expires_at = Some(0);
1796 assert!(should_refresh(&session, &margin));
1797
1798 session.expires_at = None;
1800 assert!(!should_refresh(&session, &margin));
1801
1802 let future = std::time::SystemTime::now()
1804 .duration_since(std::time::UNIX_EPOCH)
1805 .unwrap()
1806 .as_secs() as i64 + 3600;
1807 session.expires_at = Some(future);
1808 assert!(!should_refresh(&session, &margin));
1809
1810 let soon = std::time::SystemTime::now()
1812 .duration_since(std::time::UNIX_EPOCH)
1813 .unwrap()
1814 .as_secs() as i64 + 30;
1815 session.expires_at = Some(soon);
1816 assert!(should_refresh(&session, &margin));
1817 }
1818
1819 fn make_test_session() -> Session {
1821 Session {
1822 access_token: "test-access-token".to_string(),
1823 refresh_token: "test-refresh-token".to_string(),
1824 expires_in: 3600,
1825 expires_at: Some(
1826 std::time::SystemTime::now()
1827 .duration_since(std::time::UNIX_EPOCH)
1828 .unwrap()
1829 .as_secs() as i64
1830 + 3600,
1831 ),
1832 token_type: "bearer".to_string(),
1833 user: User {
1834 id: "test-user-id".to_string(),
1835 aud: Some("authenticated".to_string()),
1836 role: Some("authenticated".to_string()),
1837 email: Some("test@example.com".to_string()),
1838 phone: None,
1839 email_confirmed_at: None,
1840 phone_confirmed_at: None,
1841 confirmation_sent_at: None,
1842 recovery_sent_at: None,
1843 last_sign_in_at: None,
1844 created_at: None,
1845 updated_at: None,
1846 user_metadata: None,
1847 app_metadata: None,
1848 identities: None,
1849 factors: None,
1850 is_anonymous: None,
1851 },
1852 }
1853 }
1854
1855 #[test]
1858 fn test_get_claims_valid_jwt() {
1859 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
1862 r#"{"sub":"user-123","email":"test@example.com","role":"authenticated"}"#
1863 );
1864 let token = format!("eyJhbGciOiJIUzI1NiJ9.{}.fake-signature", payload);
1865 let claims = AuthClient::get_claims(&token).unwrap();
1866 assert_eq!(claims["sub"], "user-123");
1867 assert_eq!(claims["email"], "test@example.com");
1868 assert_eq!(claims["role"], "authenticated");
1869 }
1870
1871 #[test]
1872 fn test_get_claims_invalid_format() {
1873 let err = AuthClient::get_claims("not-a-jwt").unwrap_err();
1874 assert!(matches!(err, AuthError::InvalidToken(_)));
1875 }
1876
1877 #[test]
1878 fn test_get_claims_invalid_base64() {
1879 let err = AuthClient::get_claims("a.!!!invalid!!!.c").unwrap_err();
1880 assert!(matches!(err, AuthError::InvalidToken(_)));
1881 }
1882
1883 #[test]
1884 fn test_get_claims_invalid_json() {
1885 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode("not json");
1886 let token = format!("a.{}.c", payload);
1887 let err = AuthClient::get_claims(&token).unwrap_err();
1888 assert!(matches!(err, AuthError::InvalidToken(_)));
1889 }
1890
1891 #[test]
1894 fn test_web3_chain_serialization() {
1895 assert_eq!(serde_json::to_string(&Web3Chain::Ethereum).unwrap(), "\"ethereum\"");
1896 assert_eq!(serde_json::to_string(&Web3Chain::Solana).unwrap(), "\"solana\"");
1897 }
1898
1899 #[test]
1900 fn test_web3_chain_display() {
1901 assert_eq!(Web3Chain::Ethereum.to_string(), "ethereum");
1902 assert_eq!(Web3Chain::Solana.to_string(), "solana");
1903 }
1904
1905 #[test]
1906 fn test_web3_sign_in_params() {
1907 let params = Web3SignInParams::new(
1908 Web3Chain::Ethereum,
1909 "0x1234567890abcdef",
1910 "Sign this message",
1911 "0xsignature",
1912 "random-nonce",
1913 );
1914 assert_eq!(params.chain, Web3Chain::Ethereum);
1915 assert_eq!(params.address, "0x1234567890abcdef");
1916 let json = serde_json::to_value(¶ms).unwrap();
1917 assert_eq!(json["chain"], "ethereum");
1918 assert_eq!(json["address"], "0x1234567890abcdef");
1919 }
1920
1921 #[test]
1924 fn test_captcha_body_structure() {
1925 let mut body = json!({
1926 "email": "test@example.com",
1927 "password": "pass",
1928 });
1929 let token = "captcha-abc-123";
1930 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1931
1932 assert_eq!(body["gotrue_meta_security"]["captcha_token"], "captcha-abc-123");
1933 assert_eq!(body["email"], "test@example.com");
1934 }
1935}