1use serde::Deserialize;
5
6pub trait Provider: Send + Sync + Sized {
8 fn authorization_endpoint(&self) -> url::Url;
9 fn token_endpoint(&self) -> url::Url;
10 fn validate_iss(&self, iss: &str) -> bool;
11
12 fn client(self) -> crate::client::ClientBuilder<Self> {
13 crate::client::ClientBuilder::from_provider(self)
14 }
15}
16
17#[derive(Clone, Deserialize)]
19pub struct DiscoveredProvider {
20 authorization_endpoint: String,
21 issuer: String,
22 token_endpoint: String,
23}
24
25impl DiscoveredProvider {
26 pub async fn from_discovery(
29 discovery_url: &str,
30 http_client: &reqwest::Client,
31 ) -> Result<Self, reqwest::Error> {
32 let resp = http_client.get(discovery_url).send().await?;
34
35 let provider: DiscoveredProvider = resp.json().await?;
37
38 Ok(provider)
39 }
40}
41
42impl Provider for DiscoveredProvider {
43 fn authorization_endpoint(&self) -> url::Url {
44 url::Url::parse(&self.authorization_endpoint).unwrap()
45 }
46
47 fn token_endpoint(&self) -> url::Url {
48 url::Url::parse(&self.token_endpoint).unwrap()
49 }
50
51 fn validate_iss(&self, iss: &str) -> bool {
52 &self.issuer == iss
53 }
54}
55
56#[derive(Clone)]
59pub struct GoogleProvider {}
60impl GoogleProvider {
61 pub fn new() -> Self {
62 Self {}
63 }
64}
65impl Provider for GoogleProvider {
66 fn authorization_endpoint(&self) -> url::Url {
67 url::Url::parse("https://accounts.google.com/o/oauth2/v2/auth").unwrap()
68 }
69
70 fn token_endpoint(&self) -> url::Url {
71 url::Url::parse("https://oauth2.googleapis.com/token").unwrap()
72 }
73
74 fn validate_iss(&self, iss: &str) -> bool {
75 "https://accounts.google.com" == iss
76 }
77}
78
79#[derive(Clone)]
82pub struct MicrosoftTenantProvider {
83 tenant_uuid: Option<String>,
84}
85impl MicrosoftTenantProvider {
86 pub fn any_tenant() -> Self {
88 Self { tenant_uuid: None }
89 }
90
91 pub fn tenant(tenant_uuid: &str) -> Self {
93 Self {
94 tenant_uuid: Some(tenant_uuid.to_string()),
95 }
96 }
97}
98
99impl Provider for MicrosoftTenantProvider {
100 fn authorization_endpoint(&self) -> url::Url {
101 if let Some(tenant_uuid) = &self.tenant_uuid {
102 url::Url::parse(&format!(
103 "https://login.microsoftonline.com/{}/oauth2/v2.0/authorize",
104 tenant_uuid
105 ))
106 .unwrap()
107 } else {
108 url::Url::parse("https://login.microsoftonline.com/common/oauth2/v2.0/authorize")
109 .unwrap()
110 }
111 }
112
113 fn token_endpoint(&self) -> url::Url {
114 if let Some(tenant_uuid) = &self.tenant_uuid {
115 url::Url::parse(&format!(
116 "https://login.microsoftonline.com/{}/oauth2/v2.0/token",
117 tenant_uuid
118 ))
119 .unwrap()
120 } else {
121 url::Url::parse("https://login.microsoftonline.com/common/oauth2/v2.0/token").unwrap()
122 }
123 }
124
125 fn validate_iss(&self, iss: &str) -> bool {
126 if let Some(tenant_uuid) = &self.tenant_uuid {
127 format!("https://login.microsoftonline.com/{}/v2.0", tenant_uuid) == iss
128 } else {
129 iss.starts_with("https://login.microsoftonline.com/") && iss.ends_with("/v2.0")
131 }
132 }
133}
134
135#[cfg(test)]
136mod test {
137 use super::*;
138
139 #[tokio::test]
140 async fn discover_google() {
141 let google_idp = GoogleProvider::new();
142 let client = reqwest::Client::new();
143
144 let provider = DiscoveredProvider::from_discovery(
145 "https://accounts.google.com/.well-known/openid-configuration",
146 &client,
147 )
148 .await
149 .unwrap();
150
151 assert_eq!(
153 provider.authorization_endpoint,
154 google_idp.authorization_endpoint().as_str()
155 );
156 assert_eq!(
157 provider.token_endpoint,
158 google_idp.token_endpoint().as_str()
159 );
160 assert!(google_idp.validate_iss(&provider.issuer));
161 }
162}