1use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
2use reqwest::Response;
3use serde::{Deserialize, Serialize};
4
5use crate::Supabase;
6
7#[derive(Serialize)]
8struct Credentials<'a> {
9 email: &'a str,
10 password: &'a str,
11}
12
13#[derive(Serialize)]
14struct RefreshTokenRequest<'a> {
15 refresh_token: &'a str,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Claims {
21 pub sub: String,
22 pub email: String,
23 pub exp: usize,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AuthResponse {
29 pub access_token: String,
31 pub token_type: String,
33 pub expires_in: u64,
35 #[serde(default)]
37 pub expires_at: Option<u64>,
38 pub refresh_token: String,
40 #[serde(default)]
42 pub user: Option<serde_json::Value>,
43}
44
45#[derive(Debug, Clone)]
47pub struct EmptyResponse {
48 pub status: u16,
50}
51
52#[derive(Serialize)]
53struct RecoverRequest<'a> {
54 email: &'a str,
55}
56
57#[derive(Serialize)]
58struct PhoneCredentials<'a> {
59 phone: &'a str,
60 password: &'a str,
61}
62
63#[derive(Serialize)]
64struct OtpRequest<'a> {
65 phone: &'a str,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 channel: Option<&'a str>,
68}
69
70#[derive(Serialize)]
71struct VerifyOtpRequest<'a> {
72 phone: &'a str,
73 token: &'a str,
74 #[serde(rename = "type")]
75 verification_type: &'a str,
76}
77
78#[derive(Serialize)]
79struct ResendOtpRequest<'a> {
80 phone: &'a str,
81 #[serde(rename = "type")]
82 verification_type: &'a str,
83}
84
85impl Supabase {
86 async fn auth_post(
88 &self,
89 path: &str,
90 body: &impl Serialize,
91 ) -> Result<Response, crate::Error> {
92 let url = format!("{}/auth/v1/{path}", self.url);
93
94 let resp = self
95 .client
96 .post(&url)
97 .header("apikey", &self.api_key)
98 .header("Content-Type", "application/json")
99 .json(body)
100 .send()
101 .await?;
102
103 Ok(resp)
104 }
105
106 async fn parse_auth_response(response: Response) -> Result<AuthResponse, crate::Error> {
108 let status = response.status().as_u16();
109 if !(200..300).contains(&status) {
110 let message = response.text().await.unwrap_or_default();
111 return Err(crate::Error::Api { status, message });
112 }
113 let auth: AuthResponse = response.json().await?;
114 Ok(auth)
115 }
116
117 async fn parse_empty_response(response: Response) -> Result<EmptyResponse, crate::Error> {
119 let status = response.status().as_u16();
120 if !(200..300).contains(&status) {
121 let message = response.text().await.unwrap_or_default();
122 return Err(crate::Error::Api { status, message });
123 }
124 Ok(EmptyResponse { status })
125 }
126
127 pub fn jwt_valid(&self, jwt: &str) -> Result<Claims, crate::Error> {
131 let decoding_key = DecodingKey::from_secret(self.jwt.as_bytes());
132 let validation = Validation::new(Algorithm::HS256);
133 let token_data = decode::<Claims>(jwt, &decoding_key, &validation)?;
134 Ok(token_data.claims)
135 }
136
137 pub async fn sign_in_password(
141 &self,
142 email: &str,
143 password: &str,
144 ) -> Result<AuthResponse, crate::Error> {
145 let resp = self
146 .auth_post(
147 "token?grant_type=password",
148 &Credentials { email, password },
149 )
150 .await?;
151 Self::parse_auth_response(resp).await
152 }
153
154 pub async fn refresh_token(
158 &self,
159 refresh_token: &str,
160 ) -> Result<AuthResponse, crate::Error> {
161 let resp = self
162 .auth_post(
163 "token?grant_type=refresh_token",
164 &RefreshTokenRequest { refresh_token },
165 )
166 .await?;
167 Self::parse_auth_response(resp).await
168 }
169
170 pub async fn logout(&self) -> Result<EmptyResponse, crate::Error> {
174 let token = self.bearer_token.as_ref().ok_or_else(|| {
175 crate::Error::AuthRequired("bearer token required for logout".into())
176 })?;
177 let url = format!("{}/auth/v1/logout", self.url);
178
179 let resp = self
180 .client
181 .post(&url)
182 .header("apikey", &self.api_key)
183 .header("Content-Type", "application/json")
184 .bearer_auth(token)
185 .send()
186 .await?;
187
188 Self::parse_empty_response(resp).await
189 }
190
191 pub async fn recover_password(
193 &self,
194 email: &str,
195 ) -> Result<EmptyResponse, crate::Error> {
196 let resp = self.auth_post("recover", &RecoverRequest { email }).await?;
197 Self::parse_empty_response(resp).await
198 }
199
200 pub async fn signup_phone_password(
202 &self,
203 phone: &str,
204 password: &str,
205 ) -> Result<AuthResponse, crate::Error> {
206 let resp = self
207 .auth_post("signup", &PhoneCredentials { phone, password })
208 .await?;
209 Self::parse_auth_response(resp).await
210 }
211
212 pub async fn sign_in_otp(
216 &self,
217 phone: &str,
218 channel: Option<&str>,
219 ) -> Result<EmptyResponse, crate::Error> {
220 let resp = self
221 .auth_post("otp", &OtpRequest { phone, channel })
222 .await?;
223 Self::parse_empty_response(resp).await
224 }
225
226 pub async fn verify_otp(
230 &self,
231 phone: &str,
232 token: &str,
233 verification_type: &str,
234 ) -> Result<AuthResponse, crate::Error> {
235 let resp = self
236 .auth_post(
237 "verify",
238 &VerifyOtpRequest {
239 phone,
240 token,
241 verification_type,
242 },
243 )
244 .await?;
245 Self::parse_auth_response(resp).await
246 }
247
248 pub async fn resend_otp(
250 &self,
251 phone: &str,
252 verification_type: &str,
253 ) -> Result<EmptyResponse, crate::Error> {
254 let resp = self
255 .auth_post(
256 "resend",
257 &ResendOtpRequest {
258 phone,
259 verification_type,
260 },
261 )
262 .await?;
263 Self::parse_empty_response(resp).await
264 }
265
266 pub async fn signup_email_password(
268 &self,
269 email: &str,
270 password: &str,
271 ) -> Result<AuthResponse, crate::Error> {
272 let resp = self
273 .auth_post("signup", &Credentials { email, password })
274 .await?;
275 Self::parse_auth_response(resp).await
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 fn client() -> Supabase {
284 Supabase::new(None, None, None).unwrap_or_else(|_| {
285 Supabase::new(
286 Some("https://example.supabase.co"),
287 Some("test-key"),
288 None,
289 )
290 .unwrap()
291 })
292 }
293
294 async fn sign_in_password() -> Result<AuthResponse, crate::Error> {
295 let client = client();
296 let test_email = std::env::var("SUPABASE_TEST_EMAIL").unwrap_or_default();
297 let test_pass = std::env::var("SUPABASE_TEST_PASS").unwrap_or_default();
298 client.sign_in_password(&test_email, &test_pass).await
299 }
300
301 #[tokio::test]
302 async fn test_token_with_password() {
303 let auth = match sign_in_password().await {
304 Ok(auth) => auth,
305 Err(e) => {
306 println!("Test skipped due to error: {e}");
307 return;
308 }
309 };
310
311 assert!(!auth.access_token.is_empty());
312 assert!(!auth.refresh_token.is_empty());
313 }
314
315 #[tokio::test]
316 async fn test_refresh() {
317 let auth = match sign_in_password().await {
318 Ok(auth) => auth,
319 Err(e) => {
320 println!("Test skipped due to error: {e}");
321 return;
322 }
323 };
324
325 let refreshed = match client().refresh_token(&auth.refresh_token).await {
326 Ok(auth) => auth,
327 Err(crate::Error::Api { status: 400, .. }) => {
328 println!("Skipping: automatic reuse detection is enabled");
329 return;
330 }
331 Err(e) => {
332 println!("Test skipped due to error: {e}");
333 return;
334 }
335 };
336
337 assert!(!refreshed.access_token.is_empty());
338 }
339
340 #[tokio::test]
341 async fn test_logout() {
342 let auth = match sign_in_password().await {
343 Ok(auth) => auth,
344 Err(e) => {
345 println!("Test skipped due to error: {e}");
346 return;
347 }
348 };
349
350 let mut client = client();
351 client.set_bearer_token(&auth.access_token);
352
353 let resp = match client.logout().await {
354 Ok(resp) => resp,
355 Err(e) => {
356 println!("Test skipped: {e}");
357 return;
358 }
359 };
360
361 assert_eq!(resp.status, 204);
362 }
363
364 #[tokio::test]
365 async fn test_signup_email_password() {
366 use rand::distr::Alphanumeric;
367 use rand::{rng, Rng};
368
369 let client = client();
370
371 let rand_string: String = rng()
372 .sample_iter(&Alphanumeric)
373 .take(20)
374 .map(char::from)
375 .collect();
376
377 let email = format!("{rand_string}@a-rust-domain-that-does-not-exist.com");
378
379 match client.signup_email_password(&email, &rand_string).await {
380 Ok(auth) => {
381 assert!(!auth.access_token.is_empty());
382 }
383 Err(e) => {
384 println!("Test skipped due to error: {e}");
385 }
386 }
387 }
388
389 #[tokio::test]
390 async fn test_authenticate_token() {
391 let client = client();
392
393 let auth = match sign_in_password().await {
394 Ok(auth) => auth,
395 Err(e) => {
396 println!("Test skipped due to error: {e}");
397 return;
398 }
399 };
400
401 assert!(client.jwt_valid(&auth.access_token).is_ok());
402 }
403
404 #[test]
405 fn test_logout_requires_bearer_token() {
406 let err = crate::Error::AuthRequired("bearer token required for logout".into());
407 assert!(format!("{err}").contains("bearer token required for logout"));
408 }
409
410 #[tokio::test]
411 async fn test_recover_password() {
412 let client = client();
413
414 match client
415 .recover_password("test@a-rust-domain-that-does-not-exist.com")
416 .await
417 {
418 Ok(resp) => {
419 assert!(resp.status >= 200);
420 }
421 Err(e) => {
422 println!("Test skipped due to error: {e}");
423 }
424 }
425 }
426
427 #[tokio::test]
428 async fn test_signup_phone_password() {
429 let client = client();
430
431 match client
432 .signup_phone_password("+10000000000", "test-password-123")
433 .await
434 {
435 Ok(_auth) => {}
436 Err(crate::Error::Api { status, .. }) => {
437 assert!(
438 status == 422 || status == 401 || status == 403,
439 "unexpected API error status: {status}"
440 );
441 }
442 Err(e) => {
443 println!("Test skipped due to error: {e}");
444 }
445 }
446 }
447
448 #[tokio::test]
449 async fn test_sign_in_otp() {
450 let client = client();
451
452 match client.sign_in_otp("+10000000000", Some("sms")).await {
453 Ok(_resp) => {}
454 Err(crate::Error::Api { .. }) => {}
455 Err(e) => {
456 println!("Test skipped due to error: {e}");
457 }
458 }
459 }
460
461 #[tokio::test]
462 async fn test_verify_otp() {
463 let client = client();
464
465 match client.verify_otp("+10000000000", "000000", "sms").await {
466 Ok(_auth) => {}
467 Err(crate::Error::Api { .. }) => {}
468 Err(e) => {
469 println!("Test skipped due to error: {e}");
470 }
471 }
472 }
473
474 #[tokio::test]
475 async fn test_resend_otp() {
476 let client = client();
477
478 match client.resend_otp("+10000000000", "sms").await {
479 Ok(_resp) => {}
480 Err(crate::Error::Api { .. }) => {}
481 Err(e) => {
482 println!("Test skipped due to error: {e}");
483 }
484 }
485 }
486}