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