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
7pub trait Provider: Send + Sync + Sized {
8    /// Authorization endpoint of IdP
9    ///
10    /// See [OpenID Connect Core spec. 3.1.2](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint)
11    fn authorization_endpoint(&self) -> url::Url;
12
13    /// Token endpoint of IdP
14    ///
15    /// See [OpenID Connect Core spec. 3.1.3](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint)
16    fn token_endpoint(&self) -> url::Url;
17
18    /// validate `iss` issure claim in ID token.
19    ///
20    /// If `iss` claim is valid, return true.
21    fn validate_iss(&self, iss: &str) -> bool;
22
23    /// Create `tiny_oidc_rp::Client`
24    fn client(self) -> crate::client::ClientBuilder<Self> {
25        crate::client::ClientBuilder::from_provider(self)
26    }
27}
28
29/// OpenID connect provider from discovery
30///
31/// See []()
32#[derive(Clone, Deserialize)]
33pub struct DiscoveredProvider {
34    authorization_endpoint: String,
35    issuer: String,
36    token_endpoint: String,
37}
38
39impl DiscoveredProvider {
40    /// Create Provider from OpenID connect discovery endpoint.
41    ///
42    /// https://some_provider/.well-known/openid-configuration
43    pub async fn from_discovery(
44        discovery_url: &str,
45        http_client: &reqwest::Client,
46    ) -> Result<Self, reqwest::Error> {
47        // Send HTTP request to OpenID connect discovery endpoint
48        let resp = http_client.get(discovery_url).send().await?;
49
50        // Parse body as OpenID connect discovery JSON format
51        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/// Google OpenID connect ID provider
72///
73/// <https://accounts.google.com/.well-known/openid-configuration>
74#[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/// Microsoft OpenID connect ID provider
96///
97/// <https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration>
98#[derive(Clone)]
99pub struct MicrosoftTenantProvider {
100    tenant_uuid: Option<String>,
101}
102impl MicrosoftTenantProvider {
103    /// Any tenant issuer
104    pub fn any_tenant() -> Self {
105        Self { tenant_uuid: None }
106    }
107
108    /// Specific tenant issure (Restrict specific Azure AD organization)
109    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            // any tenant
147            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        // Compare predefined Google IdP and OpenID connect discovery
169        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}