tiny_oidc_rp/
provider.rs

1// SPDX-License-Identifier: MIT
2
3//! OpenID connect ID Provider
4use serde::Deserialize;
5
6/// OpenID Connect ID provider issuer
7pub 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/// OpenID connect provider from discovery
18#[derive(Clone, Deserialize)]
19pub struct DiscoveredProvider {
20    authorization_endpoint: String,
21    issuer: String,
22    token_endpoint: String,
23}
24
25impl DiscoveredProvider {
26    /// Create Provider from OpenID connect discovery endpoint
27    /// https://<provider>/.well-known/openid-configuration
28    pub async fn from_discovery(
29        discovery_url: &str,
30        http_client: &reqwest::Client,
31    ) -> Result<Self, reqwest::Error> {
32        // Send HTTP request to OpenID connect discovery endpoint
33        let resp = http_client.get(discovery_url).send().await?;
34
35        // Parse body as OpenID connect discovery JSON format
36        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/// Google OpenID connect ID provider
57/// https://accounts.google.com/.well-known/openid-configuration
58#[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/// Microsoft OpenID connect ID provider
80/// https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
81#[derive(Clone)]
82pub struct MicrosoftTenantProvider {
83    tenant_uuid: Option<String>,
84}
85impl MicrosoftTenantProvider {
86    /// Any tenant issuer
87    pub fn any_tenant() -> Self {
88        Self { tenant_uuid: None }
89    }
90
91    /// Specific tenant issure (Restrict specific Azure AD organization)
92    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            // any tenant
130            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        // Compare predefined Google IdP and OpenID connect discovery
152        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}