1use chrono::{DateTime, Utc};
18use serde::{Deserialize, Serialize};
19
20use crate::auth_client::AuthFuture;
21use crate::types::SocialProviderId;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::Type)]
35#[sqlx(type_name = "TEXT", rename_all = "lowercase")]
36pub enum ProviderType {
37 Google,
38 Github, Apple,
40 Microsoft,
41 #[sqlx(rename = "custom_oidc")]
42 #[serde(rename = "custom_oidc")]
43 CustomOidc,
44}
45
46#[derive(Debug, Clone)]
52pub struct SocialUserInfo {
53 pub provider_user_id: String,
54 pub email: String,
55 pub email_verified: bool,
56 pub name: Option<String>,
57 pub avatar_url: Option<String>,
58}
59
60#[derive(Debug, Clone, sqlx::FromRow)]
70pub struct SocialProviderRow {
71 pub id: SocialProviderId,
72 pub provider_type: ProviderType,
73 pub display_name: String,
74 pub client_id: String,
75 pub client_secret_enc: Vec<u8>,
76 pub client_secret_nonce: Vec<u8>,
77 pub scopes: String, pub enabled: bool,
79 pub priority: i64,
80 pub config: Option<String>, pub created_at: DateTime<Utc>,
82 pub updated_at: DateTime<Utc>,
83}
84
85#[derive(Debug, Clone)]
111pub struct SocialProviderConfig {
112 pub id: SocialProviderId,
113 pub provider_type: ProviderType,
114 pub display_name: String,
115 pub client_id: String,
116 pub client_secret: String, pub scopes: Vec<String>, pub enabled: bool,
119 pub priority: i64,
120 pub config: Option<serde_json::Value>,
121}
122
123pub trait SocialProvider: Send + Sync {
133 fn provider_type(&self) -> ProviderType;
135
136 fn authorize_url(&self, redirect_uri: &str, state: &str, pkce_challenge: &str) -> String;
138
139 fn exchange_code<'a>(
144 &'a self,
145 code: &'a str,
146 redirect_uri: &'a str,
147 pkce_verifier: &'a str,
148 ) -> AuthFuture<'a, String>;
149
150 fn fetch_user_info<'a>(&'a self, access_token: &'a str) -> AuthFuture<'a, SocialUserInfo>;
152}
153
154use crate::error::AuthError;
157
158pub async fn build_social_provider(
172 config: SocialProviderConfig,
173) -> Result<Box<dyn SocialProvider>, AuthError> {
174 match config.provider_type {
175 ProviderType::Google => Ok(Box::new(crate::social_google::GoogleSocialProvider::new(
176 config,
177 )?)),
178 ProviderType::Github => Ok(Box::new(crate::social_github::GitHubSocialProvider::new(
179 config,
180 )?)),
181 ProviderType::CustomOidc => Ok(Box::new(
182 crate::social_oidc::CustomOidcSocialProvider::new(config).await?,
183 )),
184 ProviderType::Apple | ProviderType::Microsoft => Err(AuthError::Validation(format!(
185 "provider type {:?} not yet supported",
186 config.provider_type
187 ))),
188 }
189}
190
191use crate::db::Db;
194use crate::social_provider_encrypt::{decrypt_split, encrypt_split};
195
196fn map_unique_violation(err: sqlx::Error) -> AuthError {
199 if let sqlx::Error::Database(ref db_err) = err {
200 let msg = db_err.message();
201 if msg.contains("UNIQUE constraint failed") {
202 return AuthError::Conflict(
203 "a provider with that type and display name already exists".into(),
204 );
205 }
206 }
207 AuthError::Database(err)
208}
209
210impl Db {
211 #[allow(clippy::too_many_arguments)]
216 pub async fn create_social_provider(
217 &self,
218 provider_type: ProviderType,
219 display_name: &str,
220 client_id: &str,
221 client_secret: &str,
222 scopes: &[String],
223 config: Option<&serde_json::Value>,
224 priority: i64,
225 mfa_key: &[u8; 32],
226 ) -> Result<SocialProviderId, AuthError> {
227 let id = SocialProviderId::new();
228 let enc = encrypt_split(client_secret.as_bytes(), mfa_key)?;
229 let scopes_json = serde_json::to_string(scopes).expect("Vec<String> serializes to JSON");
230 let config_json =
231 config.map(|v| serde_json::to_string(v).expect("serde_json::Value serializes to JSON"));
232 sqlx::query(
233 "INSERT INTO allowthem_social_providers \
234 (id, provider_type, display_name, client_id, \
235 client_secret_enc, client_secret_nonce, scopes, priority, config) \
236 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
237 )
238 .bind(id)
239 .bind(provider_type)
240 .bind(display_name)
241 .bind(client_id)
242 .bind(enc.ciphertext)
243 .bind(enc.nonce.to_vec())
244 .bind(scopes_json)
245 .bind(priority)
246 .bind(config_json)
247 .execute(self.pool())
248 .await
249 .map_err(map_unique_violation)?;
250 Ok(id)
251 }
252
253 pub async fn list_social_providers(&self) -> Result<Vec<SocialProviderRow>, AuthError> {
255 sqlx::query_as::<_, SocialProviderRow>(
256 "SELECT id, provider_type, display_name, client_id, \
257 client_secret_enc, client_secret_nonce, scopes, enabled, \
258 priority, config, created_at, updated_at \
259 FROM allowthem_social_providers \
260 ORDER BY priority ASC, created_at ASC",
261 )
262 .fetch_all(self.pool())
263 .await
264 .map_err(AuthError::Database)
265 }
266
267 pub async fn list_enabled_social_providers(&self) -> Result<Vec<SocialProviderRow>, AuthError> {
269 sqlx::query_as::<_, SocialProviderRow>(
270 "SELECT id, provider_type, display_name, client_id, \
271 client_secret_enc, client_secret_nonce, scopes, enabled, \
272 priority, config, created_at, updated_at \
273 FROM allowthem_social_providers \
274 WHERE enabled = 1 \
275 ORDER BY priority ASC, created_at ASC",
276 )
277 .fetch_all(self.pool())
278 .await
279 .map_err(AuthError::Database)
280 }
281
282 pub async fn get_social_provider(
284 &self,
285 id: SocialProviderId,
286 ) -> Result<SocialProviderRow, AuthError> {
287 sqlx::query_as::<_, SocialProviderRow>(
288 "SELECT id, provider_type, display_name, client_id, \
289 client_secret_enc, client_secret_nonce, scopes, enabled, \
290 priority, config, created_at, updated_at \
291 FROM allowthem_social_providers WHERE id = ?",
292 )
293 .bind(id)
294 .fetch_optional(self.pool())
295 .await
296 .map_err(AuthError::Database)?
297 .ok_or(AuthError::NotFound)
298 }
299
300 #[allow(clippy::too_many_arguments)]
311 pub async fn update_social_provider(
312 &self,
313 id: SocialProviderId,
314 display_name: Option<&str>,
315 client_id: Option<&str>,
316 client_secret: Option<&str>,
317 scopes: Option<&[String]>,
318 enabled: Option<bool>,
319 priority: Option<i64>,
320 config: Option<Option<&serde_json::Value>>,
321 mfa_key: &[u8; 32],
322 ) -> Result<(), AuthError> {
323 let enc = match client_secret {
325 Some(s) => Some(encrypt_split(s.as_bytes(), mfa_key)?),
326 None => None,
327 };
328 let scopes_json =
329 scopes.map(|s| serde_json::to_string(s).expect("Vec<String> serializes to JSON"));
330 let config_json: Option<Option<String>> =
335 config.map(|c| c.map(|v| serde_json::to_string(v).expect("Value serializes to JSON")));
336
337 let mut set_clauses: Vec<&'static str> = Vec::new();
338 if display_name.is_some() {
339 set_clauses.push("display_name = ?");
340 }
341 if client_id.is_some() {
342 set_clauses.push("client_id = ?");
343 }
344 if enc.is_some() {
345 set_clauses.push("client_secret_enc = ?");
346 set_clauses.push("client_secret_nonce = ?");
347 }
348 if scopes_json.is_some() {
349 set_clauses.push("scopes = ?");
350 }
351 if enabled.is_some() {
352 set_clauses.push("enabled = ?");
353 }
354 if priority.is_some() {
355 set_clauses.push("priority = ?");
356 }
357 if config_json.is_some() {
358 set_clauses.push("config = ?");
359 }
360
361 if set_clauses.is_empty() {
362 return Ok(());
363 }
364
365 set_clauses.push("updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')");
366
367 let sql: &'static str = Box::leak(
368 format!(
369 "UPDATE allowthem_social_providers SET {} WHERE id = ?",
370 set_clauses.join(", ")
371 )
372 .into_boxed_str(),
373 );
374
375 let mut q = sqlx::query(sql);
377 if let Some(v) = display_name {
378 q = q.bind(v);
379 }
380 if let Some(v) = client_id {
381 q = q.bind(v);
382 }
383 if let Some(ref e) = enc {
384 q = q.bind(e.ciphertext.clone());
385 q = q.bind(e.nonce.to_vec());
386 }
387 if let Some(v) = scopes_json {
388 q = q.bind(v);
389 }
390 if let Some(v) = enabled {
391 q = q.bind(v);
392 }
393 if let Some(v) = priority {
394 q = q.bind(v);
395 }
396 if let Some(v) = config_json {
397 q = q.bind(v);
398 }
399 q = q.bind(id);
400
401 q.execute(self.pool()).await.map_err(map_unique_violation)?;
402 Ok(())
403 }
404
405 pub async fn delete_social_provider(&self, id: SocialProviderId) -> Result<bool, AuthError> {
407 let result = sqlx::query("DELETE FROM allowthem_social_providers WHERE id = ?")
408 .bind(id)
409 .execute(self.pool())
410 .await
411 .map_err(AuthError::Database)?;
412 Ok(result.rows_affected() > 0)
413 }
414
415 pub fn social_provider_to_config(
421 &self,
422 row: SocialProviderRow,
423 mfa_key: &[u8; 32],
424 ) -> Result<SocialProviderConfig, AuthError> {
425 let secret_bytes =
426 decrypt_split(&row.client_secret_nonce, &row.client_secret_enc, mfa_key)?;
427 let client_secret =
428 String::from_utf8(secret_bytes).map_err(|e| AuthError::MfaEncryption(e.to_string()))?;
429 let scopes: Vec<String> =
430 serde_json::from_str(&row.scopes).map_err(|e| AuthError::Validation(e.to_string()))?;
431 let config: Option<serde_json::Value> = row
432 .config
433 .map(|s| serde_json::from_str(&s))
434 .transpose()
435 .map_err(|e| AuthError::Validation(e.to_string()))?;
436 Ok(SocialProviderConfig {
437 id: row.id,
438 provider_type: row.provider_type,
439 display_name: row.display_name,
440 client_id: row.client_id,
441 client_secret,
442 scopes,
443 enabled: row.enabled,
444 priority: row.priority,
445 config,
446 })
447 }
448}
449
450use crate::handle::AllowThem;
453
454impl AllowThem {
455 #[allow(clippy::too_many_arguments)]
457 pub async fn create_social_provider(
458 &self,
459 provider_type: ProviderType,
460 display_name: &str,
461 client_id: &str,
462 client_secret: &str,
463 scopes: &[String],
464 config: Option<&serde_json::Value>,
465 priority: i64,
466 ) -> Result<SocialProviderId, AuthError> {
467 self.db()
468 .create_social_provider(
469 provider_type,
470 display_name,
471 client_id,
472 client_secret,
473 scopes,
474 config,
475 priority,
476 self.mfa_key()?,
477 )
478 .await
479 }
480
481 pub async fn list_social_providers(&self) -> Result<Vec<SocialProviderRow>, AuthError> {
483 self.db().list_social_providers().await
484 }
485
486 pub async fn list_enabled_social_providers(&self) -> Result<Vec<SocialProviderRow>, AuthError> {
488 self.db().list_enabled_social_providers().await
489 }
490
491 pub async fn get_social_provider(
493 &self,
494 id: SocialProviderId,
495 ) -> Result<SocialProviderRow, AuthError> {
496 self.db().get_social_provider(id).await
497 }
498
499 pub async fn get_social_provider_decrypted(
504 &self,
505 id: SocialProviderId,
506 ) -> Result<SocialProviderConfig, AuthError> {
507 let row = self.db().get_social_provider(id).await?;
508 self.db().social_provider_to_config(row, self.mfa_key()?)
509 }
510
511 #[allow(clippy::too_many_arguments)]
513 pub async fn update_social_provider(
514 &self,
515 id: SocialProviderId,
516 display_name: Option<&str>,
517 client_id: Option<&str>,
518 client_secret: Option<&str>,
519 scopes: Option<&[String]>,
520 enabled: Option<bool>,
521 priority: Option<i64>,
522 config: Option<Option<&serde_json::Value>>,
523 ) -> Result<(), AuthError> {
524 self.db()
525 .update_social_provider(
526 id,
527 display_name,
528 client_id,
529 client_secret,
530 scopes,
531 enabled,
532 priority,
533 config,
534 self.mfa_key()?,
535 )
536 .await
537 }
538
539 pub async fn delete_social_provider(&self, id: SocialProviderId) -> Result<bool, AuthError> {
541 self.db().delete_social_provider(id).await
542 }
543}
544
545#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::handle::{AllowThem, AllowThemBuilder};
551
552 const TEST_KEY: [u8; 32] = [0x42u8; 32];
553 const OTHER_KEY: [u8; 32] = [0x99u8; 32];
554
555 async fn setup() -> AllowThem {
556 AllowThemBuilder::new("sqlite::memory:")
557 .cookie_secure(false)
558 .mfa_key(TEST_KEY)
559 .build()
560 .await
561 .unwrap()
562 }
563
564 async fn setup_no_key() -> AllowThem {
565 AllowThemBuilder::new("sqlite::memory:")
566 .cookie_secure(false)
567 .build()
568 .await
569 .unwrap()
570 }
571
572 fn _assert_object_safe(_: &dyn SocialProvider) {}
576
577 struct StubProvider {
580 ptype: ProviderType,
581 }
582
583 impl SocialProvider for StubProvider {
584 fn provider_type(&self) -> ProviderType {
585 self.ptype
586 }
587 fn authorize_url(&self, _r: &str, _s: &str, _p: &str) -> String {
588 "https://example.com/authorize".into()
589 }
590 fn exchange_code<'a>(
591 &'a self,
592 _code: &'a str,
593 _redirect_uri: &'a str,
594 _pkce_verifier: &'a str,
595 ) -> AuthFuture<'a, String> {
596 Box::pin(async move { Ok("stub-token".into()) })
597 }
598 fn fetch_user_info<'a>(&'a self, _access_token: &'a str) -> AuthFuture<'a, SocialUserInfo> {
599 Box::pin(async move {
600 Ok(SocialUserInfo {
601 provider_user_id: "p1".into(),
602 email: "stub@example.com".into(),
603 email_verified: true,
604 name: None,
605 avatar_url: None,
606 })
607 })
608 }
609 }
610
611 #[tokio::test]
612 async fn stub_provider_dyn_dispatch() {
613 let p: Box<dyn SocialProvider> = Box::new(StubProvider {
614 ptype: ProviderType::Google,
615 });
616 assert_eq!(p.provider_type(), ProviderType::Google);
617 let token = p.exchange_code("c", "r", "v").await.unwrap();
618 assert_eq!(token, "stub-token");
619 let info = p.fetch_user_info(&token).await.unwrap();
620 assert_eq!(info.email, "stub@example.com");
621 }
622
623 fn default_scopes() -> Vec<String> {
626 vec!["openid".into(), "email".into()]
627 }
628
629 async fn create_google(ath: &AllowThem, display_name: &str) -> SocialProviderId {
630 ath.create_social_provider(
631 ProviderType::Google,
632 display_name,
633 "client-id",
634 "client-secret",
635 &default_scopes(),
636 None,
637 0,
638 )
639 .await
640 .unwrap()
641 }
642
643 #[tokio::test]
646 async fn create_then_get_round_trip() {
647 let ath = setup().await;
648 let id = ath
649 .create_social_provider(
650 ProviderType::Google,
651 "Google Login",
652 "my-client-id",
653 "supersecret",
654 &default_scopes(),
655 None,
656 10,
657 )
658 .await
659 .unwrap();
660
661 let config = ath.get_social_provider_decrypted(id).await.unwrap();
662 assert_eq!(config.client_id, "my-client-id");
663 assert_eq!(config.client_secret, "supersecret");
664 assert_eq!(config.scopes, default_scopes());
665 assert_eq!(config.priority, 10);
666 assert!(config.enabled);
667 assert_eq!(config.provider_type, ProviderType::Google);
668 assert_eq!(config.display_name, "Google Login");
669 assert!(config.config.is_none());
670 }
671
672 #[tokio::test]
673 async fn list_orders_by_priority() {
674 let ath = setup().await;
675 let db = ath.db();
676 for (name, pri) in [("P5", 5i64), ("P1", 1), ("P3", 3)] {
677 db.create_social_provider(
678 ProviderType::Google,
679 name,
680 "cid",
681 "sec",
682 &default_scopes(),
683 None,
684 pri,
685 &TEST_KEY,
686 )
687 .await
688 .unwrap();
689 }
690 let rows = db.list_social_providers().await.unwrap();
691 let priorities: Vec<i64> = rows.iter().map(|r| r.priority).collect();
692 assert_eq!(priorities, vec![1, 3, 5]);
693 }
694
695 #[tokio::test]
696 async fn list_enabled_filters_disabled() {
697 let ath = setup().await;
698 let db = ath.db();
699 let enabled_id = db
700 .create_social_provider(
701 ProviderType::Google,
702 "Enabled",
703 "cid",
704 "sec",
705 &default_scopes(),
706 None,
707 0,
708 &TEST_KEY,
709 )
710 .await
711 .unwrap();
712 let disabled_id = db
713 .create_social_provider(
714 ProviderType::Github,
715 "Disabled",
716 "cid2",
717 "sec2",
718 &default_scopes(),
719 None,
720 0,
721 &TEST_KEY,
722 )
723 .await
724 .unwrap();
725 db.update_social_provider(
727 disabled_id,
728 None,
729 None,
730 None,
731 None,
732 Some(false),
733 None,
734 None,
735 &TEST_KEY,
736 )
737 .await
738 .unwrap();
739
740 let enabled = db.list_enabled_social_providers().await.unwrap();
741 assert_eq!(enabled.len(), 1);
742 assert_eq!(enabled[0].id, enabled_id);
743 }
744
745 #[tokio::test]
746 async fn update_partial_display_name() {
747 let ath = setup().await;
748 let id = create_google(&ath, "Old Name").await;
749 ath.update_social_provider(id, Some("New Name"), None, None, None, None, None, None)
750 .await
751 .unwrap();
752 let row = ath.db().get_social_provider(id).await.unwrap();
753 assert_eq!(row.display_name, "New Name");
754 assert_eq!(row.client_id, "client-id");
755 }
756
757 #[tokio::test]
758 async fn update_secret_re_encrypts_with_fresh_nonce() {
759 let ath = setup().await;
760 let id = create_google(&ath, "G").await;
761 let before = ath.db().get_social_provider(id).await.unwrap();
762
763 ath.update_social_provider(id, None, None, Some("new-secret"), None, None, None, None)
764 .await
765 .unwrap();
766
767 let after = ath.db().get_social_provider(id).await.unwrap();
768 assert_ne!(
769 before.client_secret_nonce, after.client_secret_nonce,
770 "nonce must change on secret rotation"
771 );
772 let config = ath.get_social_provider_decrypted(id).await.unwrap();
773 assert_eq!(config.client_secret, "new-secret");
774 }
775
776 #[tokio::test]
777 async fn delete_returns_true_on_hit_false_on_miss() {
778 let ath = setup().await;
779 let id = create_google(&ath, "G").await;
780 assert!(ath.delete_social_provider(id).await.unwrap());
781 assert!(!ath.delete_social_provider(id).await.unwrap());
782 }
783
784 #[tokio::test]
785 async fn unique_index_blocks_duplicate_type_displayname() {
786 let ath = setup().await;
787 create_google(&ath, "Google").await;
788 let err = ath
789 .create_social_provider(
790 ProviderType::Google,
791 "Google",
792 "other-cid",
793 "sec",
794 &default_scopes(),
795 None,
796 0,
797 )
798 .await
799 .unwrap_err();
800 assert!(matches!(err, AuthError::Conflict(_)));
801 }
802
803 #[tokio::test]
804 async fn decrypt_with_wrong_mfa_key_fails() {
805 let ath = setup().await;
806 let id = create_google(&ath, "G").await;
807 let row = ath.db().get_social_provider(id).await.unwrap();
808 let err = ath
809 .db()
810 .social_provider_to_config(row, &OTHER_KEY)
811 .unwrap_err();
812 assert!(matches!(err, AuthError::MfaEncryption(_)));
813 }
814
815 #[tokio::test]
816 async fn create_without_mfa_key_returns_mfa_not_configured() {
817 let ath = setup_no_key().await;
818 let err = ath
819 .create_social_provider(
820 ProviderType::Google,
821 "G",
822 "cid",
823 "sec",
824 &default_scopes(),
825 None,
826 0,
827 )
828 .await
829 .unwrap_err();
830 assert!(matches!(err, AuthError::MfaNotConfigured));
831 }
832
833 #[tokio::test]
834 async fn provider_type_all_variants_round_trip_through_sqlite() {
835 let ath = setup().await;
836 let db = ath.db();
837 let variants = [
838 (ProviderType::Google, "google-rt"),
839 (ProviderType::Github, "github-rt"),
840 (ProviderType::Apple, "apple-rt"),
841 (ProviderType::Microsoft, "microsoft-rt"),
842 (ProviderType::CustomOidc, "customoidc-rt"),
843 ];
844 for (ptype, name) in variants {
845 let id = db
846 .create_social_provider(
847 ptype,
848 name,
849 "cid",
850 "sec",
851 &default_scopes(),
852 None,
853 0,
854 &TEST_KEY,
855 )
856 .await
857 .unwrap();
858 let row = db.get_social_provider(id).await.unwrap();
859 assert_eq!(row.provider_type, ptype, "round-trip for {ptype:?}");
860 }
861 }
862
863 #[tokio::test]
864 async fn scopes_and_config_are_valid_json_as_stored() {
865 let ath = setup().await;
866 let db = ath.db();
867 let cfg_val =
868 serde_json::json!({"discovery_url": "https://issuer/.well-known/openid-configuration"});
869 let id = db
870 .create_social_provider(
871 ProviderType::CustomOidc,
872 "OIDC",
873 "cid",
874 "sec",
875 &default_scopes(),
876 Some(&cfg_val),
877 0,
878 &TEST_KEY,
879 )
880 .await
881 .unwrap();
882 let row = db.get_social_provider(id).await.unwrap();
883 let parsed_scopes: Vec<String> =
885 serde_json::from_str(&row.scopes).expect("scopes column must be valid JSON array");
886 assert_eq!(parsed_scopes, default_scopes());
887 let raw_config = row.config.expect("config must be Some");
889 let _: serde_json::Value =
890 serde_json::from_str(&raw_config).expect("config column must be valid JSON");
891 }
892
893 #[tokio::test]
894 async fn check_constraint_blocks_invalid_provider_type() {
895 let ath = setup().await;
896 let db = ath.db();
897 let err = sqlx::query(
898 "INSERT INTO allowthem_social_providers \
899 (id, provider_type, display_name, client_id, \
900 client_secret_enc, client_secret_nonce, scopes) \
901 VALUES (?, 'badprovider', 'x', 'y', X'00', X'00', '[]')",
902 )
903 .bind(SocialProviderId::new())
904 .execute(db.pool())
905 .await
906 .unwrap_err();
907 assert!(
908 matches!(err, sqlx::Error::Database(_)),
909 "CHECK constraint must fire for invalid provider_type"
910 );
911 }
912
913 #[tokio::test]
914 async fn check_constraint_blocks_invalid_enabled_value() {
915 let ath = setup().await;
916 let db = ath.db();
917 let err = sqlx::query(
918 "INSERT INTO allowthem_social_providers \
919 (id, provider_type, display_name, client_id, \
920 client_secret_enc, client_secret_nonce, scopes, enabled) \
921 VALUES (?, 'google', 'x', 'y', X'00', X'00', '[]', 2)",
922 )
923 .bind(SocialProviderId::new())
924 .execute(db.pool())
925 .await
926 .unwrap_err();
927 assert!(
928 matches!(err, sqlx::Error::Database(_)),
929 "CHECK constraint must fire for enabled = 2"
930 );
931 }
932
933 #[tokio::test]
934 async fn encryption_round_trip_via_db_boundary() {
935 use crate::social_provider_encrypt::decrypt_split;
938 let ath = setup().await;
939 let id = create_google(&ath, "Wire").await;
940 let row = ath.db().get_social_provider(id).await.unwrap();
941 let bytes =
942 decrypt_split(&row.client_secret_nonce, &row.client_secret_enc, &TEST_KEY).unwrap();
943 assert_eq!(bytes, b"client-secret");
944 }
945
946 fn make_config(provider_type: ProviderType, scopes: Vec<String>) -> SocialProviderConfig {
949 use crate::types::SocialProviderId;
950 SocialProviderConfig {
951 id: SocialProviderId::new(),
952 provider_type,
953 display_name: "Test".into(),
954 client_id: "cid".into(),
955 client_secret: "sec".into(),
956 scopes,
957 enabled: true,
958 priority: 0,
959 config: None,
960 }
961 }
962
963 #[tokio::test]
964 async fn build_social_provider_dispatches_to_google() {
965 let cfg = make_config(ProviderType::Google, vec!["openid".into()]);
966 let provider = build_social_provider(cfg).await.unwrap();
967 assert_eq!(provider.provider_type(), ProviderType::Google);
968 }
969
970 #[tokio::test]
971 async fn build_social_provider_dispatches_to_github() {
972 let cfg = make_config(ProviderType::Github, vec!["user:email".into()]);
973 let provider = build_social_provider(cfg).await.unwrap();
974 assert_eq!(provider.provider_type(), ProviderType::Github);
975 }
976
977 #[tokio::test]
978 async fn build_social_provider_rejects_custom_oidc_with_missing_config() {
979 let cfg = make_config(ProviderType::CustomOidc, vec!["openid".into()]);
981 let err = match build_social_provider(cfg).await {
982 Err(e) => e,
983 Ok(_) => panic!("expected Err, got Ok"),
984 };
985 assert!(
986 matches!(err, AuthError::Validation(ref m) if m.contains("config")),
987 "got: {err:?}"
988 );
989 }
990
991 #[tokio::test]
992 async fn build_social_provider_dispatches_to_custom_oidc() {
993 use wiremock::matchers::{method, path};
994 use wiremock::{Mock, MockServer, ResponseTemplate};
995
996 let server = MockServer::start().await;
997 let base = server.uri();
998 Mock::given(method("GET"))
999 .and(path("/.well-known/openid-configuration"))
1000 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1001 "authorization_endpoint": format!("{base}/authorize"),
1002 "token_endpoint": format!("{base}/token"),
1003 "userinfo_endpoint": format!("{base}/userinfo"),
1004 })))
1005 .mount(&server)
1006 .await;
1007
1008 use crate::types::SocialProviderId;
1009 let cfg = SocialProviderConfig {
1010 id: SocialProviderId::new(),
1011 provider_type: ProviderType::CustomOidc,
1012 display_name: "Test OIDC".into(),
1013 client_id: "cid".into(),
1014 client_secret: "sec".into(),
1015 scopes: vec!["openid".into(), "email".into()],
1016 enabled: true,
1017 priority: 0,
1018 config: Some(serde_json::json!({
1019 "discovery_url": format!("{base}/.well-known/openid-configuration")
1020 })),
1021 };
1022 let provider = build_social_provider(cfg).await.unwrap();
1023 assert_eq!(provider.provider_type(), ProviderType::CustomOidc);
1024 }
1025
1026 #[tokio::test]
1027 async fn build_social_provider_rejects_apple_with_validation_error() {
1028 let cfg = make_config(ProviderType::Apple, vec!["openid".into()]);
1029 let err = match build_social_provider(cfg).await {
1030 Err(e) => e,
1031 Ok(_) => panic!("expected Err, got Ok"),
1032 };
1033 assert!(matches!(err, AuthError::Validation(_)));
1034 }
1035
1036 #[tokio::test]
1037 async fn build_social_provider_rejects_microsoft_with_validation_error() {
1038 let cfg = make_config(ProviderType::Microsoft, vec!["openid".into()]);
1039 let err = match build_social_provider(cfg).await {
1040 Err(e) => e,
1041 Ok(_) => panic!("expected Err, got Ok"),
1042 };
1043 assert!(matches!(err, AuthError::Validation(_)));
1044 }
1045
1046 #[tokio::test]
1047 async fn build_social_provider_propagates_constructor_error_from_inner() {
1048 let cfg = make_config(ProviderType::Google, vec![]);
1050 let err = match build_social_provider(cfg).await {
1051 Err(e) => e,
1052 Ok(_) => panic!("expected Err, got Ok"),
1053 };
1054 match err {
1055 AuthError::Validation(msg) => {
1056 assert!(msg.contains("scopes must not be empty"), "got: {msg}");
1057 }
1058 other => panic!("expected Validation, got {other:?}"),
1059 }
1060 }
1061}