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 supabase_client_core::platform;
8use tokio::sync::{broadcast, Mutex, RwLock};
9use url::Url;
10
11use crate::admin::AdminClient;
12use crate::error::{AuthError, GoTrueErrorResponse};
13use crate::params::{
14 MfaChallengeParams, MfaEnrollParams, MfaVerifyParams, OAuthAuthorizeUrlParams,
15 OAuthTokenExchangeParams, ResendParams, SignInWithIdTokenParams, SsoSignInParams,
16 UpdateUserParams, VerifyOtpParams,
17};
18use crate::types::*;
19
20const EVENT_CHANNEL_CAPACITY: usize = 64;
22
23struct AuthClientInner {
24 http: reqwest::Client,
25 base_url: Url,
26 api_key: String,
27 session: RwLock<Option<Session>>,
29 event_tx: broadcast::Sender<AuthStateChange>,
30 auto_refresh_handle: Mutex<Option<platform::SpawnHandle>>,
31}
32
33impl std::fmt::Debug for AuthClientInner {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.debug_struct("AuthClientInner")
36 .field("base_url", &self.base_url)
37 .field("api_key", &"***")
38 .finish()
39 }
40}
41
42#[derive(Debug, Clone)]
62pub struct AuthClient {
63 inner: Arc<AuthClientInner>,
64}
65
66impl AuthClient {
67 pub fn new(supabase_url: &str, api_key: &str) -> Result<Self, AuthError> {
72 let base = supabase_url.trim_end_matches('/');
73 let base_url = Url::parse(&format!("{}/auth/v1", base))?;
74
75 let mut default_headers = HeaderMap::new();
76 default_headers.insert(
77 "apikey",
78 HeaderValue::from_str(api_key)
79 .map_err(|e| AuthError::InvalidConfig(format!("Invalid API key header: {}", e)))?,
80 );
81 default_headers.insert(
82 CONTENT_TYPE,
83 HeaderValue::from_static("application/json"),
84 );
85
86 let http = reqwest::Client::builder()
87 .default_headers(default_headers)
88 .build()
89 .map_err(AuthError::Http)?;
90
91 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
92
93 Ok(Self {
94 inner: Arc::new(AuthClientInner {
95 http,
96 base_url,
97 api_key: api_key.to_string(),
98 session: RwLock::new(None),
99 event_tx,
100 auto_refresh_handle: Mutex::new(None),
101 }),
102 })
103 }
104
105 pub fn base_url(&self) -> &Url {
107 &self.inner.base_url
108 }
109
110 pub async fn get_session(&self) -> Option<Session> {
116 self.inner.session.read().await.clone()
117 }
118
119 pub async fn set_session(&self, session: Session) {
123 self.store_session(&session, AuthChangeEvent::SignedIn).await;
124 }
125
126 pub async fn clear_session(&self) {
131 self.emit_signed_out().await;
132 }
133
134 pub fn on_auth_state_change(&self) -> AuthSubscription {
141 AuthSubscription {
142 rx: self.inner.event_tx.subscribe(),
143 }
144 }
145
146 pub fn start_auto_refresh(&self) {
153 self.start_auto_refresh_with(AutoRefreshConfig::default());
154 }
155
156 pub fn start_auto_refresh_with(&self, config: AutoRefreshConfig) {
158 self.stop_auto_refresh_inner();
160
161 let inner = Arc::clone(&self.inner);
162 let handle = platform::spawn(async move {
163 auto_refresh_loop(inner, config).await;
164 });
165
166 if let Ok(mut guard) = self.inner.auto_refresh_handle.try_lock() {
168 *guard = Some(handle);
169 }
170 }
171
172 pub fn stop_auto_refresh(&self) {
174 self.stop_auto_refresh_inner();
175 }
176
177 #[allow(unused_mut)]
178 fn stop_auto_refresh_inner(&self) {
179 if let Ok(mut guard) = self.inner.auto_refresh_handle.try_lock() {
180 if let Some(mut handle) = guard.take() {
181 handle.abort();
182 }
183 }
184 }
185
186 pub async fn get_session_user(&self) -> Result<User, AuthError> {
192 let session = self.inner.session.read().await.clone();
193 match session {
194 Some(s) => self.get_user(&s.access_token).await,
195 None => Err(AuthError::NoSession),
196 }
197 }
198
199 pub async fn refresh_current_session(&self) -> Result<Session, AuthError> {
203 let session = self.inner.session.read().await.clone();
204 match session {
205 Some(s) => self.refresh_session(&s.refresh_token).await,
206 None => Err(AuthError::NoSession),
207 }
208 }
209
210 pub async fn sign_out_current(&self) -> Result<(), AuthError> {
214 self.sign_out_current_with_scope(SignOutScope::Global).await
215 }
216
217 pub async fn sign_out_current_with_scope(
221 &self,
222 scope: SignOutScope,
223 ) -> Result<(), AuthError> {
224 let session = self.inner.session.read().await.clone();
225 match session {
226 Some(s) => self.sign_out_with_scope(&s.access_token, scope).await,
227 None => Err(AuthError::NoSession),
228 }
229 }
230
231 pub async fn sign_up_with_email(
238 &self,
239 email: &str,
240 password: &str,
241 ) -> Result<AuthResponse, AuthError> {
242 self.sign_up_with_email_and_data(email, password, None).await
243 }
244
245 pub async fn sign_up_with_email_and_data(
250 &self,
251 email: &str,
252 password: &str,
253 data: Option<JsonValue>,
254 ) -> Result<AuthResponse, AuthError> {
255 let mut body = json!({
256 "email": email,
257 "password": password,
258 });
259 if let Some(data) = data {
260 body["data"] = data;
261 }
262
263 let url = self.url("/signup");
264 let resp = self.inner.http.post(url).json(&body).send().await?;
265 let auth_resp = self.handle_auth_response(resp).await?;
266 if let Some(session) = &auth_resp.session {
267 self.store_session(session, AuthChangeEvent::SignedIn).await;
268 }
269 Ok(auth_resp)
270 }
271
272 pub async fn sign_up_with_phone(
277 &self,
278 phone: &str,
279 password: &str,
280 ) -> Result<AuthResponse, AuthError> {
281 let body = json!({
282 "phone": phone,
283 "password": password,
284 });
285
286 let url = self.url("/signup");
287 let resp = self.inner.http.post(url).json(&body).send().await?;
288 let auth_resp = self.handle_auth_response(resp).await?;
289 if let Some(session) = &auth_resp.session {
290 self.store_session(session, AuthChangeEvent::SignedIn).await;
291 }
292 Ok(auth_resp)
293 }
294
295 pub async fn sign_in_with_password_email(
302 &self,
303 email: &str,
304 password: &str,
305 ) -> Result<Session, AuthError> {
306 let body = json!({
307 "email": email,
308 "password": password,
309 });
310
311 let url = self.url("/token?grant_type=password");
312 let resp = self.inner.http.post(url).json(&body).send().await?;
313 let session = self.handle_session_response(resp).await?;
314 self.store_session(&session, AuthChangeEvent::SignedIn).await;
315 Ok(session)
316 }
317
318 pub async fn sign_in_with_password_phone(
323 &self,
324 phone: &str,
325 password: &str,
326 ) -> Result<Session, AuthError> {
327 let body = json!({
328 "phone": phone,
329 "password": password,
330 });
331
332 let url = self.url("/token?grant_type=password");
333 let resp = self.inner.http.post(url).json(&body).send().await?;
334 let session = self.handle_session_response(resp).await?;
335 self.store_session(&session, AuthChangeEvent::SignedIn).await;
336 Ok(session)
337 }
338
339 pub async fn sign_in_with_otp_email(&self, email: &str) -> Result<(), AuthError> {
343 let body = json!({
344 "email": email,
345 });
346
347 let url = self.url("/otp");
348 let resp = self.inner.http.post(url).json(&body).send().await?;
349 self.handle_empty_response(resp).await
350 }
351
352 pub async fn sign_in_with_otp_phone(
356 &self,
357 phone: &str,
358 channel: OtpChannel,
359 ) -> Result<(), AuthError> {
360 let body = json!({
361 "phone": phone,
362 "channel": channel,
363 });
364
365 let url = self.url("/otp");
366 let resp = self.inner.http.post(url).json(&body).send().await?;
367 self.handle_empty_response(resp).await
368 }
369
370 pub async fn verify_otp(&self, params: VerifyOtpParams) -> Result<Session, AuthError> {
375 let url = self.url("/verify");
376 let resp = self.inner.http.post(url).json(¶ms).send().await?;
377 let session = self.handle_session_response(resp).await?;
378 self.store_session(&session, AuthChangeEvent::SignedIn).await;
379 Ok(session)
380 }
381
382 pub async fn sign_in_anonymous(&self) -> Result<Session, AuthError> {
387 let url = self.url("/signup");
388 let body = json!({});
389 let resp = self.inner.http.post(url).json(&body).send().await?;
390 let session = self.handle_session_response(resp).await?;
391 self.store_session(&session, AuthChangeEvent::SignedIn).await;
392 Ok(session)
393 }
394
395 pub fn get_oauth_sign_in_url(
403 &self,
404 provider: OAuthProvider,
405 redirect_to: Option<&str>,
406 scopes: Option<&str>,
407 ) -> Result<String, AuthError> {
408 let mut url = self.url("/authorize");
409 url.query_pairs_mut()
410 .append_pair("provider", &provider.to_string());
411
412 if let Some(redirect) = redirect_to {
413 url.query_pairs_mut()
414 .append_pair("redirect_to", redirect);
415 }
416 if let Some(scopes) = scopes {
417 url.query_pairs_mut().append_pair("scopes", scopes);
418 }
419
420 Ok(url.to_string())
421 }
422
423 pub async fn exchange_code_for_session(
428 &self,
429 code: &str,
430 code_verifier: Option<&str>,
431 ) -> Result<Session, AuthError> {
432 let mut body = json!({
433 "auth_code": code,
434 });
435 if let Some(verifier) = code_verifier {
436 body["code_verifier"] = json!(verifier);
437 }
438
439 let url = self.url("/token?grant_type=pkce");
440 let resp = self.inner.http.post(url).json(&body).send().await?;
441 let session = self.handle_session_response(resp).await?;
442 self.store_session(&session, AuthChangeEvent::SignedIn).await;
443 Ok(session)
444 }
445
446 pub async fn refresh_session(&self, refresh_token: &str) -> Result<Session, AuthError> {
453 let body = json!({
454 "refresh_token": refresh_token,
455 });
456
457 let url = self.url("/token?grant_type=refresh_token");
458 let resp = self.inner.http.post(url).json(&body).send().await?;
459 let session = self.handle_session_response(resp).await?;
460 self.store_session(&session, AuthChangeEvent::TokenRefreshed).await;
461 Ok(session)
462 }
463
464 pub async fn get_user(&self, access_token: &str) -> Result<User, AuthError> {
472 let url = self.url("/user");
473 let resp = self
474 .inner
475 .http
476 .get(url)
477 .bearer_auth(access_token)
478 .send()
479 .await?;
480 self.handle_user_response(resp).await
481 }
482
483 pub async fn update_user(
488 &self,
489 access_token: &str,
490 params: UpdateUserParams,
491 ) -> Result<User, AuthError> {
492 let url = self.url("/user");
493 let resp = self
494 .inner
495 .http
496 .put(url)
497 .bearer_auth(access_token)
498 .json(¶ms)
499 .send()
500 .await?;
501 let user = self.handle_user_response(resp).await?;
502
503 let mut guard = self.inner.session.write().await;
505 let session_clone = if let Some(session) = guard.as_mut() {
506 session.user = user.clone();
507 Some(session.clone())
508 } else {
509 None
510 };
511 drop(guard);
512
513 let _ = self.inner.event_tx.send(AuthStateChange {
514 event: AuthChangeEvent::UserUpdated,
515 session: session_clone,
516 });
517
518 Ok(user)
519 }
520
521 pub async fn sign_out(&self, access_token: &str) -> Result<(), AuthError> {
528 self.sign_out_with_scope(access_token, SignOutScope::Global)
529 .await
530 }
531
532 pub async fn sign_out_with_scope(
537 &self,
538 access_token: &str,
539 scope: SignOutScope,
540 ) -> Result<(), AuthError> {
541 let url = self.url(&format!("/logout?scope={}", scope));
542 let resp = self
543 .inner
544 .http
545 .post(url)
546 .bearer_auth(access_token)
547 .send()
548 .await?;
549 self.handle_empty_response(resp).await?;
550 self.emit_signed_out().await;
551 Ok(())
552 }
553
554 pub async fn reset_password_for_email(
560 &self,
561 email: &str,
562 redirect_to: Option<&str>,
563 ) -> Result<(), AuthError> {
564 let mut body = json!({ "email": email });
565 if let Some(redirect) = redirect_to {
566 body["redirect_to"] = json!(redirect);
567 }
568
569 let url = self.url("/recover");
570 let resp = self.inner.http.post(url).json(&body).send().await?;
571 self.handle_empty_response(resp).await
572 }
573
574 pub fn admin(&self) -> AdminClient<'_> {
582 AdminClient::new(self)
583 }
584
585 pub fn admin_with_key<'a>(&'a self, service_role_key: &'a str) -> AdminClient<'a> {
587 AdminClient::with_key(self, service_role_key)
588 }
589
590 pub async fn mfa_enroll(
596 &self,
597 access_token: &str,
598 params: MfaEnrollParams,
599 ) -> Result<MfaEnrollResponse, AuthError> {
600 let url = self.url("/factors");
601 let resp = self
602 .inner
603 .http
604 .post(url)
605 .bearer_auth(access_token)
606 .json(¶ms)
607 .send()
608 .await?;
609 let status = resp.status().as_u16();
610 if status >= 400 {
611 return Err(self.parse_error(status, resp).await);
612 }
613 let body: MfaEnrollResponse = resp.json().await?;
614 Ok(body)
615 }
616
617 pub async fn mfa_challenge(
621 &self,
622 access_token: &str,
623 factor_id: &str,
624 ) -> Result<MfaChallengeResponse, AuthError> {
625 self.mfa_challenge_with_params(access_token, factor_id, MfaChallengeParams::default())
626 .await
627 }
628
629 pub async fn mfa_challenge_with_params(
633 &self,
634 access_token: &str,
635 factor_id: &str,
636 params: MfaChallengeParams,
637 ) -> Result<MfaChallengeResponse, AuthError> {
638 let url = self.url(&format!("/factors/{}/challenge", factor_id));
639 let resp = self
640 .inner
641 .http
642 .post(url)
643 .bearer_auth(access_token)
644 .json(¶ms)
645 .send()
646 .await?;
647 let status = resp.status().as_u16();
648 if status >= 400 {
649 return Err(self.parse_error(status, resp).await);
650 }
651 let body: MfaChallengeResponse = resp.json().await?;
652 Ok(body)
653 }
654
655 pub async fn mfa_verify(
660 &self,
661 access_token: &str,
662 factor_id: &str,
663 params: MfaVerifyParams,
664 ) -> Result<Session, AuthError> {
665 let url = self.url(&format!("/factors/{}/verify", factor_id));
666 let resp = self
667 .inner
668 .http
669 .post(url)
670 .bearer_auth(access_token)
671 .json(¶ms)
672 .send()
673 .await?;
674 let session = self.handle_session_response(resp).await?;
675 self.store_session(&session, AuthChangeEvent::SignedIn).await;
676 Ok(session)
677 }
678
679 pub async fn mfa_challenge_and_verify(
684 &self,
685 access_token: &str,
686 factor_id: &str,
687 code: &str,
688 ) -> Result<Session, AuthError> {
689 let challenge = self.mfa_challenge(access_token, factor_id).await?;
690 self.mfa_verify(
691 access_token,
692 factor_id,
693 MfaVerifyParams::new(&challenge.id, code),
694 )
695 .await
696 }
697
698 pub async fn mfa_unenroll(
702 &self,
703 access_token: &str,
704 factor_id: &str,
705 ) -> Result<MfaUnenrollResponse, AuthError> {
706 let url = self.url(&format!("/factors/{}", factor_id));
707 let resp = self
708 .inner
709 .http
710 .delete(url)
711 .bearer_auth(access_token)
712 .send()
713 .await?;
714 let status = resp.status().as_u16();
715 if status >= 400 {
716 return Err(self.parse_error(status, resp).await);
717 }
718 let body: MfaUnenrollResponse = resp.json().await?;
719 Ok(body)
720 }
721
722 pub async fn mfa_list_factors(
728 &self,
729 access_token: &str,
730 ) -> Result<MfaListFactorsResponse, AuthError> {
731 let user = self.get_user(access_token).await?;
732 let all = user.factors.unwrap_or_default();
733 let totp = all
734 .iter()
735 .filter(|f| f.factor_type == "totp")
736 .cloned()
737 .collect();
738 let phone = all
739 .iter()
740 .filter(|f| f.factor_type == "phone")
741 .cloned()
742 .collect();
743 Ok(MfaListFactorsResponse { totp, phone, all })
744 }
745
746 pub async fn mfa_get_authenticator_assurance_level(
752 &self,
753 access_token: &str,
754 ) -> Result<AuthenticatorAssuranceLevelInfo, AuthError> {
755 let user = self.get_user(access_token).await?;
756 let factors = user.factors.unwrap_or_default();
757
758 let amr = parse_amr_from_jwt(access_token);
760
761 let has_mfa_amr = amr.iter().any(|e| e.method == "totp" || e.method == "phone");
763 let current_level = if !amr.is_empty() {
764 if has_mfa_amr {
765 Some(AuthenticatorAssuranceLevel::Aal2)
766 } else {
767 Some(AuthenticatorAssuranceLevel::Aal1)
768 }
769 } else {
770 Some(AuthenticatorAssuranceLevel::Aal1)
771 };
772
773 let has_verified_factor = factors.iter().any(|f| f.status == "verified");
775 let next_level = if has_verified_factor {
776 Some(AuthenticatorAssuranceLevel::Aal2)
777 } else {
778 Some(AuthenticatorAssuranceLevel::Aal1)
779 };
780
781 Ok(AuthenticatorAssuranceLevelInfo {
782 current_level,
783 next_level,
784 current_authentication_methods: amr,
785 })
786 }
787
788 pub async fn sign_in_with_sso(
794 &self,
795 params: SsoSignInParams,
796 ) -> Result<SsoSignInResponse, AuthError> {
797 let url = self.url("/sso");
798 let resp = self.inner.http.post(url).json(¶ms).send().await?;
799 let status = resp.status().as_u16();
800 if status >= 400 {
801 return Err(self.parse_error(status, resp).await);
802 }
803 let body: SsoSignInResponse = resp.json().await?;
804 Ok(body)
805 }
806
807 pub async fn sign_in_with_id_token(
814 &self,
815 params: SignInWithIdTokenParams,
816 ) -> Result<Session, AuthError> {
817 let url = self.url("/token?grant_type=id_token");
818 let resp = self.inner.http.post(url).json(¶ms).send().await?;
819 let session = self.handle_session_response(resp).await?;
820 self.store_session(&session, AuthChangeEvent::SignedIn).await;
821 Ok(session)
822 }
823
824 pub async fn link_identity(
832 &self,
833 access_token: &str,
834 provider: OAuthProvider,
835 ) -> Result<LinkIdentityResponse, AuthError> {
836 let mut url = self.url("/user/identities/authorize");
837 url.query_pairs_mut()
838 .append_pair("provider", &provider.to_string())
839 .append_pair("skip_http_redirect", "true");
840 let resp = self
841 .inner
842 .http
843 .get(url)
844 .bearer_auth(access_token)
845 .send()
846 .await?;
847 let status = resp.status().as_u16();
848 if status >= 400 {
849 return Err(self.parse_error(status, resp).await);
850 }
851 let body: JsonValue = resp.json().await?;
853 let redirect_url = body
854 .get("url")
855 .and_then(|v| v.as_str())
856 .unwrap_or_default()
857 .to_string();
858 Ok(LinkIdentityResponse { url: redirect_url })
859 }
860
861 pub async fn unlink_identity(
865 &self,
866 access_token: &str,
867 identity_id: &str,
868 ) -> Result<(), AuthError> {
869 let url = self.url(&format!("/user/identities/{}", identity_id));
870 let resp = self
871 .inner
872 .http
873 .delete(url)
874 .bearer_auth(access_token)
875 .send()
876 .await?;
877 self.handle_empty_response(resp).await
878 }
879
880 pub async fn resend(&self, params: ResendParams) -> Result<(), AuthError> {
886 let url = self.url("/resend");
887 let resp = self.inner.http.post(url).json(¶ms).send().await?;
888 self.handle_empty_response(resp).await
889 }
890
891 pub async fn reauthenticate(&self, access_token: &str) -> Result<(), AuthError> {
897 let url = self.url("/reauthenticate");
898 let resp = self
899 .inner
900 .http
901 .get(url)
902 .bearer_auth(access_token)
903 .send()
904 .await?;
905 self.handle_empty_response(resp).await
906 }
907
908 pub async fn get_user_identities(
916 &self,
917 access_token: &str,
918 ) -> Result<Vec<Identity>, AuthError> {
919 let user = self.get_user(access_token).await?;
920 Ok(user.identities.unwrap_or_default())
921 }
922
923 pub async fn oauth_get_authorization_details(
931 &self,
932 access_token: &str,
933 authorization_id: &str,
934 ) -> Result<OAuthAuthorizationDetailsResponse, AuthError> {
935 let url = self.url(&format!("/oauth/authorizations/{}", authorization_id));
936 let resp = self
937 .inner
938 .http
939 .get(url)
940 .bearer_auth(access_token)
941 .send()
942 .await?;
943 let status = resp.status().as_u16();
944 if status >= 400 {
945 return Err(self.parse_error(status, resp).await);
946 }
947 let body: OAuthAuthorizationDetailsResponse = resp.json().await?;
948 Ok(body)
949 }
950
951 pub async fn oauth_approve_authorization(
955 &self,
956 access_token: &str,
957 authorization_id: &str,
958 ) -> Result<OAuthRedirect, AuthError> {
959 self.oauth_consent_action(access_token, authorization_id, "approve")
960 .await
961 }
962
963 pub async fn oauth_deny_authorization(
967 &self,
968 access_token: &str,
969 authorization_id: &str,
970 ) -> Result<OAuthRedirect, AuthError> {
971 self.oauth_consent_action(access_token, authorization_id, "deny")
972 .await
973 }
974
975 pub async fn oauth_list_grants(
979 &self,
980 access_token: &str,
981 ) -> Result<Vec<OAuthGrant>, AuthError> {
982 let url = self.url("/user/oauth/grants");
983 let resp = self
984 .inner
985 .http
986 .get(url)
987 .bearer_auth(access_token)
988 .send()
989 .await?;
990 let status = resp.status().as_u16();
991 if status >= 400 {
992 return Err(self.parse_error(status, resp).await);
993 }
994 let grants: Vec<OAuthGrant> = resp.json().await?;
995 Ok(grants)
996 }
997
998 pub async fn oauth_revoke_grant(
1002 &self,
1003 access_token: &str,
1004 client_id: &str,
1005 ) -> Result<(), AuthError> {
1006 let mut url = self.url("/user/oauth/grants");
1007 url.query_pairs_mut()
1008 .append_pair("client_id", client_id);
1009 let resp = self
1010 .inner
1011 .http
1012 .delete(url)
1013 .bearer_auth(access_token)
1014 .send()
1015 .await?;
1016 self.handle_empty_response(resp).await
1017 }
1018
1019 async fn oauth_consent_action(
1020 &self,
1021 access_token: &str,
1022 authorization_id: &str,
1023 action: &str,
1024 ) -> Result<OAuthRedirect, AuthError> {
1025 let url = self.url(&format!(
1026 "/oauth/authorizations/{}/consent",
1027 authorization_id
1028 ));
1029 let body = serde_json::json!({ "action": action });
1030 let resp = self
1031 .inner
1032 .http
1033 .post(url)
1034 .bearer_auth(access_token)
1035 .json(&body)
1036 .send()
1037 .await?;
1038 let status = resp.status().as_u16();
1039 if status >= 400 {
1040 return Err(self.parse_error(status, resp).await);
1041 }
1042 let redirect: OAuthRedirect = resp.json().await?;
1043 Ok(redirect)
1044 }
1045
1046 pub fn generate_pkce_pair() -> PkcePair {
1053 use rand::Rng;
1054
1055 let mut rng = rand::rng();
1057 let random_bytes: [u8; 32] = rng.random();
1058 let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(random_bytes);
1059
1060 let hash = Sha256::digest(verifier.as_bytes());
1062 let challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash);
1063
1064 PkcePair {
1065 verifier: PkceCodeVerifier(verifier),
1066 challenge: PkceCodeChallenge(challenge),
1067 }
1068 }
1069
1070 pub fn build_oauth_authorize_url(&self, params: &OAuthAuthorizeUrlParams) -> String {
1074 let mut url = self.url("/oauth/authorize");
1075 {
1076 let mut pairs = url.query_pairs_mut();
1077 pairs.append_pair("client_id", ¶ms.client_id);
1078 pairs.append_pair("redirect_uri", ¶ms.redirect_uri);
1079 pairs.append_pair("response_type", "code");
1080
1081 if let Some(scope) = ¶ms.scope {
1082 pairs.append_pair("scope", scope);
1083 }
1084 if let Some(state) = ¶ms.state {
1085 pairs.append_pair("state", state);
1086 }
1087 if let Some(challenge) = ¶ms.code_challenge {
1088 pairs.append_pair("code_challenge", challenge);
1089 if let Some(method) = ¶ms.code_challenge_method {
1090 pairs.append_pair("code_challenge_method", method);
1091 }
1092 }
1093 }
1094 url.to_string()
1095 }
1096
1097 pub async fn oauth_token_exchange(
1102 &self,
1103 params: OAuthTokenExchangeParams,
1104 ) -> Result<OAuthTokenResponse, AuthError> {
1105 let url = self.url("/oauth/token");
1106 let mut form: Vec<(&str, String)> = vec![
1107 ("grant_type", "authorization_code".to_string()),
1108 ("code", params.code.clone()),
1109 ("redirect_uri", params.redirect_uri.clone()),
1110 ("client_id", params.client_id.clone()),
1111 ];
1112 if let Some(secret) = ¶ms.client_secret {
1113 form.push(("client_secret", secret.clone()));
1114 }
1115 if let Some(verifier) = ¶ms.code_verifier {
1116 form.push(("code_verifier", verifier.clone()));
1117 }
1118
1119 let resp = self.inner.http.post(url).form(&form).send().await?;
1120 let status = resp.status().as_u16();
1121 if status >= 400 {
1122 return Err(self.parse_error(status, resp).await);
1123 }
1124 let token_resp: OAuthTokenResponse = resp.json().await?;
1125 Ok(token_resp)
1126 }
1127
1128 pub async fn oauth_token_refresh(
1132 &self,
1133 client_id: &str,
1134 refresh_token: &str,
1135 client_secret: Option<&str>,
1136 ) -> Result<OAuthTokenResponse, AuthError> {
1137 let url = self.url("/oauth/token");
1138 let mut form: Vec<(&str, String)> = vec![
1139 ("grant_type", "refresh_token".to_string()),
1140 ("refresh_token", refresh_token.to_string()),
1141 ("client_id", client_id.to_string()),
1142 ];
1143 if let Some(secret) = client_secret {
1144 form.push(("client_secret", secret.to_string()));
1145 }
1146
1147 let resp = self.inner.http.post(url).form(&form).send().await?;
1148 let status = resp.status().as_u16();
1149 if status >= 400 {
1150 return Err(self.parse_error(status, resp).await);
1151 }
1152 let token_resp: OAuthTokenResponse = resp.json().await?;
1153 Ok(token_resp)
1154 }
1155
1156 pub async fn oauth_revoke_token(
1160 &self,
1161 token: &str,
1162 token_type_hint: Option<&str>,
1163 ) -> Result<(), AuthError> {
1164 let url = self.url("/oauth/revoke");
1165 let mut form: Vec<(&str, String)> = vec![("token", token.to_string())];
1166 if let Some(hint) = token_type_hint {
1167 form.push(("token_type_hint", hint.to_string()));
1168 }
1169
1170 let resp = self.inner.http.post(url).form(&form).send().await?;
1171 self.handle_empty_response(resp).await
1172 }
1173
1174 pub async fn oauth_get_openid_configuration(
1178 &self,
1179 ) -> Result<OpenIdConfiguration, AuthError> {
1180 let url = self.url("/.well-known/openid-configuration");
1181 let resp = self.inner.http.get(url).send().await?;
1182 let status = resp.status().as_u16();
1183 if status >= 400 {
1184 return Err(self.parse_error(status, resp).await);
1185 }
1186 let config: OpenIdConfiguration = resp.json().await?;
1187 Ok(config)
1188 }
1189
1190 pub async fn oauth_get_jwks(&self) -> Result<JwksResponse, AuthError> {
1194 let url = self.url("/.well-known/jwks.json");
1195 let resp = self.inner.http.get(url).send().await?;
1196 let status = resp.status().as_u16();
1197 if status >= 400 {
1198 return Err(self.parse_error(status, resp).await);
1199 }
1200 let jwks: JwksResponse = resp.json().await?;
1201 Ok(jwks)
1202 }
1203
1204 pub async fn oauth_get_userinfo(
1208 &self,
1209 access_token: &str,
1210 ) -> Result<JsonValue, AuthError> {
1211 let url = self.url("/oauth/userinfo");
1212 let resp = self
1213 .inner
1214 .http
1215 .get(url)
1216 .bearer_auth(access_token)
1217 .send()
1218 .await?;
1219 let status = resp.status().as_u16();
1220 if status >= 400 {
1221 return Err(self.parse_error(status, resp).await);
1222 }
1223 let userinfo: JsonValue = resp.json().await?;
1224 Ok(userinfo)
1225 }
1226
1227 pub fn get_claims(token: &str) -> Result<JsonValue, AuthError> {
1236 let parts: Vec<&str> = token.split('.').collect();
1237 if parts.len() != 3 {
1238 return Err(AuthError::InvalidToken(
1239 "JWT must have 3 parts separated by '.'".to_string(),
1240 ));
1241 }
1242
1243 let payload_b64 = parts[1];
1244 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
1245 .decode(payload_b64)
1246 .map_err(|e| AuthError::InvalidToken(format!("Invalid base64 in JWT payload: {}", e)))?;
1247
1248 serde_json::from_slice(&decoded)
1249 .map_err(|e| AuthError::InvalidToken(format!("Invalid JSON in JWT payload: {}", e)))
1250 }
1251
1252 pub async fn sign_up_with_email_full(
1260 &self,
1261 email: &str,
1262 password: &str,
1263 data: Option<JsonValue>,
1264 captcha_token: Option<&str>,
1265 ) -> Result<AuthResponse, AuthError> {
1266 let mut body = json!({
1267 "email": email,
1268 "password": password,
1269 });
1270 if let Some(data) = data {
1271 body["data"] = data;
1272 }
1273 if let Some(token) = captcha_token {
1274 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1275 }
1276
1277 let url = self.url("/signup");
1278 let resp = self.inner.http.post(url).json(&body).send().await?;
1279 let auth_resp = self.handle_auth_response(resp).await?;
1280 if let Some(session) = &auth_resp.session {
1281 self.store_session(session, AuthChangeEvent::SignedIn).await;
1282 }
1283 Ok(auth_resp)
1284 }
1285
1286 pub async fn sign_in_with_password_email_captcha(
1288 &self,
1289 email: &str,
1290 password: &str,
1291 captcha_token: Option<&str>,
1292 ) -> Result<Session, AuthError> {
1293 let mut body = json!({
1294 "email": email,
1295 "password": password,
1296 });
1297 if let Some(token) = captcha_token {
1298 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1299 }
1300
1301 let url = self.url("/token?grant_type=password");
1302 let resp = self.inner.http.post(url).json(&body).send().await?;
1303 let session = self.handle_session_response(resp).await?;
1304 self.store_session(&session, AuthChangeEvent::SignedIn).await;
1305 Ok(session)
1306 }
1307
1308 pub async fn sign_in_with_otp_email_captcha(
1310 &self,
1311 email: &str,
1312 captcha_token: Option<&str>,
1313 ) -> Result<(), AuthError> {
1314 let mut body = json!({
1315 "email": email,
1316 });
1317 if let Some(token) = captcha_token {
1318 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1319 }
1320
1321 let url = self.url("/otp");
1322 let resp = self.inner.http.post(url).json(&body).send().await?;
1323 self.handle_empty_response(resp).await
1324 }
1325
1326 pub async fn sign_in_with_otp_phone_captcha(
1328 &self,
1329 phone: &str,
1330 channel: OtpChannel,
1331 captcha_token: Option<&str>,
1332 ) -> Result<(), AuthError> {
1333 let mut body = json!({
1334 "phone": phone,
1335 "channel": channel,
1336 });
1337 if let Some(token) = captcha_token {
1338 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1339 }
1340
1341 let url = self.url("/otp");
1342 let resp = self.inner.http.post(url).json(&body).send().await?;
1343 self.handle_empty_response(resp).await
1344 }
1345
1346 pub async fn sign_in_with_web3(
1353 &self,
1354 params: Web3SignInParams,
1355 ) -> Result<Session, AuthError> {
1356 let body = json!({
1357 "chain": params.chain,
1358 "address": params.address,
1359 "message": params.message,
1360 "signature": params.signature,
1361 "nonce": params.nonce,
1362 });
1363
1364 let url = self.url("/token?grant_type=web3");
1365 let resp = self.inner.http.post(url).json(&body).send().await?;
1366 let session = self.handle_session_response(resp).await?;
1367 self.store_session(&session, AuthChangeEvent::SignedIn).await;
1368 Ok(session)
1369 }
1370
1371 async fn store_session(&self, session: &Session, event: AuthChangeEvent) {
1375 *self.inner.session.write().await = Some(session.clone());
1376 let _ = self.inner.event_tx.send(AuthStateChange {
1377 event,
1378 session: Some(session.clone()),
1379 });
1380 }
1381
1382 async fn emit_signed_out(&self) {
1384 *self.inner.session.write().await = None;
1385 let _ = self.inner.event_tx.send(AuthStateChange {
1386 event: AuthChangeEvent::SignedOut,
1387 session: None,
1388 });
1389 }
1390
1391 pub(crate) fn url(&self, path: &str) -> Url {
1392 let mut url = self.inner.base_url.clone();
1393 let current = url.path().to_string();
1394 if let Some(query_start) = path.find('?') {
1396 url.set_path(&format!("{}{}", current, &path[..query_start]));
1397 url.set_query(Some(&path[query_start + 1..]));
1398 } else {
1399 url.set_path(&format!("{}{}", current, path));
1400 }
1401 url
1402 }
1403
1404 pub(crate) fn http(&self) -> &reqwest::Client {
1405 &self.inner.http
1406 }
1407
1408 pub(crate) fn api_key(&self) -> &str {
1409 &self.inner.api_key
1410 }
1411
1412 async fn handle_auth_response(
1413 &self,
1414 resp: reqwest::Response,
1415 ) -> Result<AuthResponse, AuthError> {
1416 let status = resp.status().as_u16();
1417 if status >= 400 {
1418 return Err(self.parse_error(status, resp).await);
1419 }
1420
1421 let body: AuthResponse = resp.json().await?;
1422 Ok(body)
1423 }
1424
1425 async fn handle_session_response(
1426 &self,
1427 resp: reqwest::Response,
1428 ) -> Result<Session, AuthError> {
1429 let status = resp.status().as_u16();
1430 if status >= 400 {
1431 return Err(self.parse_error(status, resp).await);
1432 }
1433
1434 let session: Session = resp.json().await?;
1435 Ok(session)
1436 }
1437
1438 pub(crate) async fn handle_user_response(
1439 &self,
1440 resp: reqwest::Response,
1441 ) -> Result<User, AuthError> {
1442 let status = resp.status().as_u16();
1443 if status >= 400 {
1444 return Err(self.parse_error(status, resp).await);
1445 }
1446
1447 let user: User = resp.json().await?;
1448 Ok(user)
1449 }
1450
1451 pub(crate) async fn handle_empty_response(
1452 &self,
1453 resp: reqwest::Response,
1454 ) -> Result<(), AuthError> {
1455 let status = resp.status().as_u16();
1456 if status >= 400 {
1457 return Err(self.parse_error(status, resp).await);
1458 }
1459 Ok(())
1460 }
1461
1462 async fn parse_error(&self, status: u16, resp: reqwest::Response) -> AuthError {
1463 match resp.json::<GoTrueErrorResponse>().await {
1464 Ok(err_resp) => {
1465 let error_code = err_resp
1466 .error_code
1467 .as_deref()
1468 .map(|s| s.into());
1469 AuthError::Api {
1470 status,
1471 message: err_resp.error_message(),
1472 error_code,
1473 }
1474 }
1475 Err(_) => AuthError::Api {
1476 status,
1477 message: format!("HTTP {}", status),
1478 error_code: None,
1479 },
1480 }
1481 }
1482}
1483
1484async fn auto_refresh_loop(inner: Arc<AuthClientInner>, config: AutoRefreshConfig) {
1486 let mut retries = 0u32;
1487 loop {
1488 platform::sleep(config.check_interval).await;
1489
1490 let session = inner.session.read().await.clone();
1491 if let Some(session) = session {
1492 if should_refresh(&session, &config.refresh_margin) {
1493 match refresh_session_internal(&inner, &session.refresh_token).await {
1494 Ok(new_session) => {
1495 *inner.session.write().await = Some(new_session.clone());
1496 let _ = inner.event_tx.send(AuthStateChange {
1497 event: AuthChangeEvent::TokenRefreshed,
1498 session: Some(new_session),
1499 });
1500 retries = 0;
1501 }
1502 Err(_) => {
1503 retries += 1;
1504 if retries >= config.max_retries {
1505 *inner.session.write().await = None;
1506 let _ = inner.event_tx.send(AuthStateChange {
1507 event: AuthChangeEvent::SignedOut,
1508 session: None,
1509 });
1510 break;
1511 }
1512 }
1513 }
1514 }
1515 }
1516 }
1517}
1518
1519fn should_refresh(session: &Session, margin: &std::time::Duration) -> bool {
1521 session.expires_at.map(|exp| {
1522 let now = std::time::SystemTime::now()
1523 .duration_since(std::time::UNIX_EPOCH)
1524 .unwrap_or_default()
1525 .as_secs() as i64;
1526 exp - now < margin.as_secs() as i64
1527 }).unwrap_or(false)
1528}
1529
1530async fn refresh_session_internal(
1532 inner: &AuthClientInner,
1533 refresh_token: &str,
1534) -> Result<Session, AuthError> {
1535 let body = json!({
1536 "refresh_token": refresh_token,
1537 });
1538
1539 let mut url = inner.base_url.clone();
1540 let current = url.path().to_string();
1541 url.set_path(&format!("{}/token", current));
1542 url.set_query(Some("grant_type=refresh_token"));
1543
1544 let resp = inner.http.post(url).json(&body).send().await?;
1545 let status = resp.status().as_u16();
1546 if status >= 400 {
1547 return Err(AuthError::Api {
1548 status,
1549 message: format!("Token refresh failed (HTTP {})", status),
1550 error_code: None,
1551 });
1552 }
1553
1554 let session: Session = resp.json().await?;
1555 Ok(session)
1556}
1557
1558fn parse_amr_from_jwt(token: &str) -> Vec<AmrEntry> {
1563 let parts: Vec<&str> = token.split('.').collect();
1564 if parts.len() != 3 {
1565 return Vec::new();
1566 }
1567
1568 let payload_b64 = parts[1];
1569 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
1570 .decode(payload_b64)
1571 .ok();
1572 let decoded = match decoded {
1573 Some(d) => d,
1574 None => return Vec::new(),
1575 };
1576
1577 let payload: JsonValue = match serde_json::from_slice(&decoded) {
1578 Ok(v) => v,
1579 Err(_) => return Vec::new(),
1580 };
1581
1582 match payload.get("amr") {
1583 Some(amr_val) => serde_json::from_value::<Vec<AmrEntry>>(amr_val.clone()).unwrap_or_default(),
1584 None => Vec::new(),
1585 }
1586}
1587
1588#[cfg(test)]
1589mod tests {
1590 use super::*;
1591
1592 #[test]
1593 fn test_oauth_url_google() {
1594 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1595 let url = client
1596 .get_oauth_sign_in_url(OAuthProvider::Google, None, None)
1597 .unwrap();
1598 assert!(url.contains("/auth/v1/authorize"));
1599 assert!(url.contains("provider=google"));
1600 }
1601
1602 #[test]
1603 fn test_oauth_url_with_redirect_and_scopes() {
1604 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1605 let url = client
1606 .get_oauth_sign_in_url(
1607 OAuthProvider::GitHub,
1608 Some("https://myapp.com/callback"),
1609 Some("read:user"),
1610 )
1611 .unwrap();
1612 assert!(url.contains("provider=github"));
1613 assert!(url.contains("redirect_to="));
1614 assert!(url.contains("scopes="));
1615 }
1616
1617 #[test]
1618 fn test_oauth_url_custom_provider() {
1619 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1620 let url = client
1621 .get_oauth_sign_in_url(OAuthProvider::Custom("myidp".into()), None, None)
1622 .unwrap();
1623 assert!(url.contains("provider=myidp"));
1624 }
1625
1626 #[test]
1627 fn test_url_building() {
1628 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1629 let url = client.url("/signup");
1630 assert_eq!(url.path(), "/auth/v1/signup");
1631 assert!(url.query().is_none());
1632
1633 let url = client.url("/token?grant_type=password");
1634 assert_eq!(url.path(), "/auth/v1/token");
1635 assert_eq!(url.query(), Some("grant_type=password"));
1636 }
1637
1638 #[test]
1639 fn test_url_building_trailing_slash() {
1640 let client = AuthClient::new("https://example.supabase.co/", "test-key").unwrap();
1641 let url = client.url("/signup");
1642 assert_eq!(url.path(), "/auth/v1/signup");
1643 }
1644
1645 #[test]
1646 fn test_base_url() {
1647 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1648 assert_eq!(client.base_url().path(), "/auth/v1");
1649 }
1650
1651 #[test]
1654 fn test_generate_pkce_pair() {
1655 let pair = AuthClient::generate_pkce_pair();
1656 assert_eq!(pair.verifier.as_str().len(), 43);
1658 assert_eq!(pair.challenge.as_str().len(), 43);
1660 assert_ne!(pair.verifier.as_str(), pair.challenge.as_str());
1662 }
1663
1664 #[test]
1665 fn test_pkce_pair_is_deterministic_for_same_verifier() {
1666 use sha2::{Digest, Sha256};
1668 let pair = AuthClient::generate_pkce_pair();
1669 let hash = Sha256::digest(pair.verifier.as_str().as_bytes());
1670 let expected_challenge =
1671 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash);
1672 assert_eq!(pair.challenge.as_str(), expected_challenge);
1673 }
1674
1675 #[test]
1676 fn test_pkce_pairs_are_unique() {
1677 let pair1 = AuthClient::generate_pkce_pair();
1678 let pair2 = AuthClient::generate_pkce_pair();
1679 assert_ne!(pair1.verifier.as_str(), pair2.verifier.as_str());
1680 assert_ne!(pair1.challenge.as_str(), pair2.challenge.as_str());
1681 }
1682
1683 #[test]
1686 fn test_build_oauth_authorize_url_basic() {
1687 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1688 let params = OAuthAuthorizeUrlParams::new("client-abc", "https://app.com/callback");
1689 let url = client.build_oauth_authorize_url(¶ms);
1690 assert!(url.contains("/auth/v1/oauth/authorize"));
1691 assert!(url.contains("client_id=client-abc"));
1692 assert!(url.contains("redirect_uri="));
1693 assert!(url.contains("response_type=code"));
1694 }
1695
1696 #[test]
1697 fn test_build_oauth_authorize_url_with_scope_and_state() {
1698 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1699 let params = OAuthAuthorizeUrlParams::new("client-abc", "https://app.com/callback")
1700 .scope("openid profile")
1701 .state("csrf-token");
1702 let url = client.build_oauth_authorize_url(¶ms);
1703 assert!(url.contains("scope=openid+profile"));
1704 assert!(url.contains("state=csrf-token"));
1705 }
1706
1707 #[test]
1708 fn test_build_oauth_authorize_url_with_pkce() {
1709 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1710 let pkce = AuthClient::generate_pkce_pair();
1711 let params = OAuthAuthorizeUrlParams::new("client-abc", "https://app.com/callback")
1712 .pkce(&pkce.challenge);
1713 let url = client.build_oauth_authorize_url(¶ms);
1714 assert!(url.contains(&format!("code_challenge={}", pkce.challenge.as_str())));
1715 assert!(url.contains("code_challenge_method=S256"));
1716 }
1717
1718 #[tokio::test]
1721 async fn test_new_client_has_no_session() {
1722 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1723 assert!(client.get_session().await.is_none());
1724 }
1725
1726 #[tokio::test]
1727 async fn test_set_session_stores() {
1728 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1729 let session = make_test_session();
1730 client.set_session(session.clone()).await;
1731 let stored = client.get_session().await.unwrap();
1732 assert_eq!(stored.access_token, session.access_token);
1733 }
1734
1735 #[tokio::test]
1736 async fn test_clear_session() {
1737 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1738 client.set_session(make_test_session()).await;
1739 assert!(client.get_session().await.is_some());
1740 client.clear_session().await;
1741 assert!(client.get_session().await.is_none());
1742 }
1743
1744 #[tokio::test]
1745 async fn test_event_emitted_on_set_session() {
1746 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1747 let mut sub = client.on_auth_state_change();
1748 client.set_session(make_test_session()).await;
1749 let event = tokio::time::timeout(
1750 std::time::Duration::from_millis(100),
1751 sub.next(),
1752 ).await.unwrap().unwrap();
1753 assert_eq!(event.event, AuthChangeEvent::SignedIn);
1754 assert!(event.session.is_some());
1755 }
1756
1757 #[tokio::test]
1758 async fn test_event_emitted_on_clear_session() {
1759 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1760 let mut sub = client.on_auth_state_change();
1761 client.clear_session().await;
1762 let event = tokio::time::timeout(
1763 std::time::Duration::from_millis(100),
1764 sub.next(),
1765 ).await.unwrap().unwrap();
1766 assert_eq!(event.event, AuthChangeEvent::SignedOut);
1767 assert!(event.session.is_none());
1768 }
1769
1770 #[tokio::test]
1771 async fn test_multiple_subscribers() {
1772 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1773 let mut sub1 = client.on_auth_state_change();
1774 let mut sub2 = client.on_auth_state_change();
1775 client.set_session(make_test_session()).await;
1776
1777 let timeout = std::time::Duration::from_millis(100);
1778 let e1 = tokio::time::timeout(timeout, sub1.next()).await.unwrap().unwrap();
1779 let e2 = tokio::time::timeout(timeout, sub2.next()).await.unwrap().unwrap();
1780 assert_eq!(e1.event, AuthChangeEvent::SignedIn);
1781 assert_eq!(e2.event, AuthChangeEvent::SignedIn);
1782 }
1783
1784 #[tokio::test]
1785 async fn test_no_session_error() {
1786 let client = AuthClient::new("https://example.supabase.co", "test-key").unwrap();
1787 let err = client.get_session_user().await.unwrap_err();
1788 assert!(matches!(err, AuthError::NoSession));
1789 }
1790
1791 #[tokio::test]
1792 async fn test_should_refresh_logic() {
1793 let margin = std::time::Duration::from_secs(60);
1794
1795 let mut session = make_test_session();
1797 session.expires_at = Some(0);
1798 assert!(should_refresh(&session, &margin));
1799
1800 session.expires_at = None;
1802 assert!(!should_refresh(&session, &margin));
1803
1804 let future = std::time::SystemTime::now()
1806 .duration_since(std::time::UNIX_EPOCH)
1807 .unwrap()
1808 .as_secs() as i64 + 3600;
1809 session.expires_at = Some(future);
1810 assert!(!should_refresh(&session, &margin));
1811
1812 let soon = std::time::SystemTime::now()
1814 .duration_since(std::time::UNIX_EPOCH)
1815 .unwrap()
1816 .as_secs() as i64 + 30;
1817 session.expires_at = Some(soon);
1818 assert!(should_refresh(&session, &margin));
1819 }
1820
1821 fn make_test_session() -> Session {
1823 Session {
1824 access_token: "test-access-token".to_string(),
1825 refresh_token: "test-refresh-token".to_string(),
1826 expires_in: 3600,
1827 expires_at: Some(
1828 std::time::SystemTime::now()
1829 .duration_since(std::time::UNIX_EPOCH)
1830 .unwrap()
1831 .as_secs() as i64
1832 + 3600,
1833 ),
1834 token_type: "bearer".to_string(),
1835 user: User {
1836 id: "test-user-id".to_string(),
1837 aud: Some("authenticated".to_string()),
1838 role: Some("authenticated".to_string()),
1839 email: Some("test@example.com".to_string()),
1840 phone: None,
1841 email_confirmed_at: None,
1842 phone_confirmed_at: None,
1843 confirmation_sent_at: None,
1844 recovery_sent_at: None,
1845 last_sign_in_at: None,
1846 created_at: None,
1847 updated_at: None,
1848 user_metadata: None,
1849 app_metadata: None,
1850 identities: None,
1851 factors: None,
1852 is_anonymous: None,
1853 },
1854 }
1855 }
1856
1857 #[test]
1860 fn test_get_claims_valid_jwt() {
1861 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
1864 r#"{"sub":"user-123","email":"test@example.com","role":"authenticated"}"#
1865 );
1866 let token = format!("eyJhbGciOiJIUzI1NiJ9.{}.fake-signature", payload);
1867 let claims = AuthClient::get_claims(&token).unwrap();
1868 assert_eq!(claims["sub"], "user-123");
1869 assert_eq!(claims["email"], "test@example.com");
1870 assert_eq!(claims["role"], "authenticated");
1871 }
1872
1873 #[test]
1874 fn test_get_claims_invalid_format() {
1875 let err = AuthClient::get_claims("not-a-jwt").unwrap_err();
1876 assert!(matches!(err, AuthError::InvalidToken(_)));
1877 }
1878
1879 #[test]
1880 fn test_get_claims_invalid_base64() {
1881 let err = AuthClient::get_claims("a.!!!invalid!!!.c").unwrap_err();
1882 assert!(matches!(err, AuthError::InvalidToken(_)));
1883 }
1884
1885 #[test]
1886 fn test_get_claims_invalid_json() {
1887 let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode("not json");
1888 let token = format!("a.{}.c", payload);
1889 let err = AuthClient::get_claims(&token).unwrap_err();
1890 assert!(matches!(err, AuthError::InvalidToken(_)));
1891 }
1892
1893 #[test]
1896 fn test_web3_chain_serialization() {
1897 assert_eq!(serde_json::to_string(&Web3Chain::Ethereum).unwrap(), "\"ethereum\"");
1898 assert_eq!(serde_json::to_string(&Web3Chain::Solana).unwrap(), "\"solana\"");
1899 }
1900
1901 #[test]
1902 fn test_web3_chain_display() {
1903 assert_eq!(Web3Chain::Ethereum.to_string(), "ethereum");
1904 assert_eq!(Web3Chain::Solana.to_string(), "solana");
1905 }
1906
1907 #[test]
1908 fn test_web3_sign_in_params() {
1909 let params = Web3SignInParams::new(
1910 Web3Chain::Ethereum,
1911 "0x1234567890abcdef",
1912 "Sign this message",
1913 "0xsignature",
1914 "random-nonce",
1915 );
1916 assert_eq!(params.chain, Web3Chain::Ethereum);
1917 assert_eq!(params.address, "0x1234567890abcdef");
1918 let json = serde_json::to_value(¶ms).unwrap();
1919 assert_eq!(json["chain"], "ethereum");
1920 assert_eq!(json["address"], "0x1234567890abcdef");
1921 }
1922
1923 #[test]
1926 fn test_captcha_body_structure() {
1927 let mut body = json!({
1928 "email": "test@example.com",
1929 "password": "pass",
1930 });
1931 let token = "captcha-abc-123";
1932 body["gotrue_meta_security"] = json!({ "captcha_token": token });
1933
1934 assert_eq!(body["gotrue_meta_security"]["captcha_token"], "captcha-abc-123");
1935 assert_eq!(body["email"], "test@example.com");
1936 }
1937
1938 use wiremock::matchers::{method, path, query_param};
1941 use wiremock::{Mock, MockServer, ResponseTemplate};
1942
1943 fn mock_auth(server: &MockServer) -> AuthClient {
1945 AuthClient::new(&server.uri(), "test-anon-key").unwrap()
1946 }
1947
1948 fn session_json() -> serde_json::Value {
1950 json!({
1951 "access_token": "mock-access-token",
1952 "refresh_token": "mock-refresh-token",
1953 "expires_in": 3600,
1954 "expires_at": 9999999999i64,
1955 "token_type": "bearer",
1956 "user": {
1957 "id": "user-123",
1958 "aud": "authenticated",
1959 "role": "authenticated",
1960 "email": "test@example.com"
1961 }
1962 })
1963 }
1964
1965 #[tokio::test]
1968 async fn wiremock_sign_up_422_error() {
1969 let server = MockServer::start().await;
1970 Mock::given(method("POST"))
1971 .and(path("/auth/v1/signup"))
1972 .respond_with(
1973 ResponseTemplate::new(422)
1974 .set_body_json(json!({
1975 "msg": "Password should be at least 6 characters",
1976 "error_code": "weak_password"
1977 })),
1978 )
1979 .mount(&server)
1980 .await;
1981
1982 let auth = mock_auth(&server);
1983 let err = auth.sign_up_with_email("user@test.com", "short").await.unwrap_err();
1984 match err {
1985 AuthError::Api { status, message, error_code } => {
1986 assert_eq!(status, 422);
1987 assert_eq!(message, "Password should be at least 6 characters");
1988 assert_eq!(error_code, Some(crate::error::AuthErrorCode::WeakPassword));
1989 }
1990 other => panic!("Expected Api error, got: {:?}", other),
1991 }
1992 }
1993
1994 #[tokio::test]
1997 async fn wiremock_sign_in_invalid_credentials() {
1998 let server = MockServer::start().await;
1999 Mock::given(method("POST"))
2000 .and(path("/auth/v1/token"))
2001 .and(query_param("grant_type", "password"))
2002 .respond_with(
2003 ResponseTemplate::new(400)
2004 .set_body_json(json!({
2005 "msg": "Invalid login credentials",
2006 "error_code": "invalid_credentials"
2007 })),
2008 )
2009 .mount(&server)
2010 .await;
2011
2012 let auth = mock_auth(&server);
2013 let err = auth
2014 .sign_in_with_password_email("wrong@test.com", "badpass")
2015 .await
2016 .unwrap_err();
2017 match err {
2018 AuthError::Api { status, message, error_code } => {
2019 assert_eq!(status, 400);
2020 assert_eq!(message, "Invalid login credentials");
2021 assert_eq!(
2022 error_code,
2023 Some(crate::error::AuthErrorCode::InvalidCredentials)
2024 );
2025 }
2026 other => panic!("Expected Api error, got: {:?}", other),
2027 }
2028 }
2029
2030 #[tokio::test]
2033 async fn wiremock_token_refresh_error() {
2034 let server = MockServer::start().await;
2035 Mock::given(method("POST"))
2036 .and(path("/auth/v1/token"))
2037 .and(query_param("grant_type", "refresh_token"))
2038 .respond_with(
2039 ResponseTemplate::new(400)
2040 .set_body_json(json!({
2041 "msg": "Invalid Refresh Token: Already Used",
2042 "error_code": "refresh_token_not_found"
2043 })),
2044 )
2045 .mount(&server)
2046 .await;
2047
2048 let auth = mock_auth(&server);
2049 let err = auth.refresh_session("expired-token").await.unwrap_err();
2050 match err {
2051 AuthError::Api { status, message, error_code } => {
2052 assert_eq!(status, 400);
2053 assert!(message.contains("Already Used"));
2054 assert_eq!(
2055 error_code,
2056 Some(crate::error::AuthErrorCode::RefreshTokenNotFound)
2057 );
2058 }
2059 other => panic!("Expected Api error, got: {:?}", other),
2060 }
2061 }
2062
2063 #[tokio::test]
2066 async fn wiremock_password_reset_error() {
2067 let server = MockServer::start().await;
2068 Mock::given(method("POST"))
2069 .and(path("/auth/v1/recover"))
2070 .respond_with(
2071 ResponseTemplate::new(429)
2072 .set_body_json(json!({
2073 "msg": "Rate limit exceeded",
2074 "error_code": "over_request_rate_limit"
2075 })),
2076 )
2077 .mount(&server)
2078 .await;
2079
2080 let auth = mock_auth(&server);
2081 let err = auth
2082 .reset_password_for_email("user@test.com", None)
2083 .await
2084 .unwrap_err();
2085 match err {
2086 AuthError::Api { status, message, error_code } => {
2087 assert_eq!(status, 429);
2088 assert_eq!(message, "Rate limit exceeded");
2089 assert_eq!(
2090 error_code,
2091 Some(crate::error::AuthErrorCode::OverRequestRateLimit)
2092 );
2093 }
2094 other => panic!("Expected Api error, got: {:?}", other),
2095 }
2096 }
2097
2098 #[tokio::test]
2101 async fn wiremock_verify_otp_error() {
2102 let server = MockServer::start().await;
2103 Mock::given(method("POST"))
2104 .and(path("/auth/v1/verify"))
2105 .respond_with(
2106 ResponseTemplate::new(403)
2107 .set_body_json(json!({
2108 "msg": "Token has expired or is invalid",
2109 "error_code": "otp_expired"
2110 })),
2111 )
2112 .mount(&server)
2113 .await;
2114
2115 let auth = mock_auth(&server);
2116 let params = crate::params::VerifyOtpParams {
2117 token: "999999".to_string(),
2118 otp_type: OtpType::Email,
2119 email: Some("user@test.com".to_string()),
2120 phone: None,
2121 token_hash: None,
2122 };
2123 let err = auth.verify_otp(params).await.unwrap_err();
2124 match err {
2125 AuthError::Api { status, message, error_code } => {
2126 assert_eq!(status, 403);
2127 assert!(message.contains("expired"));
2128 assert_eq!(
2129 error_code,
2130 Some(crate::error::AuthErrorCode::OtpExpired)
2131 );
2132 }
2133 other => panic!("Expected Api error, got: {:?}", other),
2134 }
2135 }
2136
2137 #[tokio::test]
2140 async fn wiremock_admin_list_users_error() {
2141 let server = MockServer::start().await;
2142 Mock::given(method("GET"))
2143 .and(path("/auth/v1/admin/users"))
2144 .respond_with(
2145 ResponseTemplate::new(401)
2146 .set_body_json(json!({
2147 "msg": "Invalid API key"
2148 })),
2149 )
2150 .mount(&server)
2151 .await;
2152
2153 let auth = mock_auth(&server);
2154 let admin = auth.admin();
2155 let err = admin.list_users(None, None).await.unwrap_err();
2156 match err {
2157 AuthError::Api { status, message, .. } => {
2158 assert_eq!(status, 401);
2159 assert_eq!(message, "Invalid API key");
2160 }
2161 other => panic!("Expected Api error, got: {:?}", other),
2162 }
2163 }
2164
2165 #[tokio::test]
2166 async fn wiremock_admin_delete_user_error() {
2167 let server = MockServer::start().await;
2168 Mock::given(method("DELETE"))
2169 .and(path("/auth/v1/admin/users/nonexistent-id"))
2170 .respond_with(
2171 ResponseTemplate::new(404)
2172 .set_body_json(json!({
2173 "msg": "User not found",
2174 "error_code": "user_not_found"
2175 })),
2176 )
2177 .mount(&server)
2178 .await;
2179
2180 let auth = mock_auth(&server);
2181 let admin = auth.admin();
2182 let err = admin.delete_user("nonexistent-id").await.unwrap_err();
2183 match err {
2184 AuthError::Api { status, message, .. } => {
2185 assert_eq!(status, 404);
2186 assert_eq!(message, "User not found");
2187 }
2188 other => panic!("Expected Api error, got: {:?}", other),
2189 }
2190 }
2191
2192 #[tokio::test]
2195 async fn wiremock_sso_sign_in_error() {
2196 let server = MockServer::start().await;
2197 Mock::given(method("POST"))
2198 .and(path("/auth/v1/sso"))
2199 .respond_with(
2200 ResponseTemplate::new(404)
2201 .set_body_json(json!({
2202 "msg": "No SSO provider found for domain",
2203 "error_code": "sso_provider_not_found"
2204 })),
2205 )
2206 .mount(&server)
2207 .await;
2208
2209 let auth = mock_auth(&server);
2210 let params = crate::params::SsoSignInParams::domain("unknown.com");
2211 let err = auth.sign_in_with_sso(params).await.unwrap_err();
2212 match err {
2213 AuthError::Api { status, message, error_code } => {
2214 assert_eq!(status, 404);
2215 assert!(message.contains("SSO provider"));
2216 assert_eq!(
2217 error_code,
2218 Some(crate::error::AuthErrorCode::SsoProviderNotFound)
2219 );
2220 }
2221 other => panic!("Expected Api error, got: {:?}", other),
2222 }
2223 }
2224
2225 #[tokio::test]
2228 async fn wiremock_link_identity_error() {
2229 let server = MockServer::start().await;
2230 Mock::given(method("GET"))
2231 .and(path("/auth/v1/user/identities/authorize"))
2232 .respond_with(
2233 ResponseTemplate::new(422)
2234 .set_body_json(json!({
2235 "msg": "Identity already exists",
2236 "error_code": "identity_already_exists"
2237 })),
2238 )
2239 .mount(&server)
2240 .await;
2241
2242 let auth = mock_auth(&server);
2243 let err = auth
2244 .link_identity("access-token", OAuthProvider::Google)
2245 .await
2246 .unwrap_err();
2247 match err {
2248 AuthError::Api { status, message, error_code } => {
2249 assert_eq!(status, 422);
2250 assert!(message.contains("Identity already exists"));
2251 assert_eq!(
2252 error_code,
2253 Some(crate::error::AuthErrorCode::IdentityAlreadyExists)
2254 );
2255 }
2256 other => panic!("Expected Api error, got: {:?}", other),
2257 }
2258 }
2259
2260 #[tokio::test]
2263 async fn wiremock_unlink_identity_error() {
2264 let server = MockServer::start().await;
2265 Mock::given(method("DELETE"))
2266 .and(path("/auth/v1/user/identities/identity-123"))
2267 .respond_with(
2268 ResponseTemplate::new(422)
2269 .set_body_json(json!({
2270 "msg": "Unlinking this identity is not allowed",
2271 "error_code": "single_identity_not_deletable"
2272 })),
2273 )
2274 .mount(&server)
2275 .await;
2276
2277 let auth = mock_auth(&server);
2278 let err = auth
2279 .unlink_identity("access-token", "identity-123")
2280 .await
2281 .unwrap_err();
2282 match err {
2283 AuthError::Api { status, message, error_code } => {
2284 assert_eq!(status, 422);
2285 assert!(message.contains("not allowed"));
2286 assert_eq!(
2287 error_code,
2288 Some(crate::error::AuthErrorCode::SingleIdentityNotDeletable)
2289 );
2290 }
2291 other => panic!("Expected Api error, got: {:?}", other),
2292 }
2293 }
2294
2295 #[tokio::test]
2298 async fn wiremock_sign_in_success_stores_session() {
2299 let server = MockServer::start().await;
2300 Mock::given(method("POST"))
2301 .and(path("/auth/v1/token"))
2302 .and(query_param("grant_type", "password"))
2303 .respond_with(
2304 ResponseTemplate::new(200).set_body_json(session_json()),
2305 )
2306 .mount(&server)
2307 .await;
2308
2309 let auth = mock_auth(&server);
2310 let session = auth
2311 .sign_in_with_password_email("test@example.com", "password123")
2312 .await
2313 .unwrap();
2314 assert_eq!(session.access_token, "mock-access-token");
2315 assert_eq!(session.user.id, "user-123");
2316
2317 let stored = auth.get_session().await.unwrap();
2319 assert_eq!(stored.access_token, "mock-access-token");
2320 }
2321
2322 #[tokio::test]
2325 async fn wiremock_sign_out_error() {
2326 let server = MockServer::start().await;
2327 Mock::given(method("POST"))
2328 .and(path("/auth/v1/logout"))
2329 .respond_with(
2330 ResponseTemplate::new(401)
2331 .set_body_json(json!({
2332 "msg": "Invalid token"
2333 })),
2334 )
2335 .mount(&server)
2336 .await;
2337
2338 let auth = mock_auth(&server);
2339 let err = auth.sign_out("bad-token").await.unwrap_err();
2340 match err {
2341 AuthError::Api { status, .. } => {
2342 assert_eq!(status, 401);
2343 }
2344 other => panic!("Expected Api error, got: {:?}", other),
2345 }
2346 }
2347
2348 #[tokio::test]
2351 async fn wiremock_sign_in_non_json_error_body() {
2352 let server = MockServer::start().await;
2353 Mock::given(method("POST"))
2354 .and(path("/auth/v1/token"))
2355 .and(query_param("grant_type", "password"))
2356 .respond_with(
2357 ResponseTemplate::new(500).set_body_string("Internal Server Error"),
2358 )
2359 .mount(&server)
2360 .await;
2361
2362 let auth = mock_auth(&server);
2363 let err = auth
2364 .sign_in_with_password_email("user@test.com", "pass")
2365 .await
2366 .unwrap_err();
2367 match err {
2368 AuthError::Api { status, message, error_code } => {
2369 assert_eq!(status, 500);
2370 assert_eq!(message, "HTTP 500");
2371 assert!(error_code.is_none());
2372 }
2373 other => panic!("Expected Api error, got: {:?}", other),
2374 }
2375 }
2376}