1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SPDX-License-Identifier: MIT

//! OpenID connect ID Provider
use serde::Deserialize;

/// OpenID Connect ID provider issuer
pub trait Provider: Send + Sync + Sized {
    fn authorization_endpoint(&self) -> url::Url;
    fn token_endpoint(&self) -> url::Url;
    fn validate_iss(&self, iss: &str) -> bool;

    fn client(self) -> crate::client::ClientBuilder<Self> {
        crate::client::ClientBuilder::from_provider(self)
    }
}

/// OpenID connect provider from discovery
#[derive(Clone, Deserialize)]
pub struct DiscoveredProvider {
    authorization_endpoint: String,
    issuer: String,
    token_endpoint: String,
}

impl DiscoveredProvider {
    /// Create Provider from OpenID connect discovery endpoint
    /// https://<provider>/.well-known/openid-configuration
    pub async fn from_discovery(
        discovery_url: &str,
        http_client: &reqwest::Client,
    ) -> Result<Self, reqwest::Error> {
        // Send HTTP request to OpenID connect discovery endpoint
        let resp = http_client.get(discovery_url).send().await?;

        // Parse body as OpenID connect discovery JSON format
        let provider: DiscoveredProvider = resp.json().await?;

        Ok(provider)
    }
}

impl Provider for DiscoveredProvider {
    fn authorization_endpoint(&self) -> url::Url {
        url::Url::parse(&self.authorization_endpoint).unwrap()
    }

    fn token_endpoint(&self) -> url::Url {
        url::Url::parse(&self.token_endpoint).unwrap()
    }

    fn validate_iss(&self, iss: &str) -> bool {
        &self.issuer == iss
    }
}

/// Google OpenID connect ID provider
/// https://accounts.google.com/.well-known/openid-configuration
#[derive(Clone)]
pub struct GoogleProvider {}
impl GoogleProvider {
    pub fn new() -> Self {
        Self {}
    }
}
impl Provider for GoogleProvider {
    fn authorization_endpoint(&self) -> url::Url {
        url::Url::parse("https://accounts.google.com/o/oauth2/v2/auth").unwrap()
    }

    fn token_endpoint(&self) -> url::Url {
        url::Url::parse("https://oauth2.googleapis.com/token").unwrap()
    }

    fn validate_iss(&self, iss: &str) -> bool {
        "https://accounts.google.com" == iss
    }
}

/// Microsoft OpenID connect ID provider
/// https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
#[derive(Clone)]
pub struct MicrosoftTenantProvider {
    tenant_uuid: Option<String>,
}
impl MicrosoftTenantProvider {
    /// Any tenant issuer
    pub fn any_tenant() -> Self {
        Self { tenant_uuid: None }
    }

    /// Specific tenant issure (Restrict specific Azure AD organization)
    pub fn tenant(tenant_uuid: &str) -> Self {
        Self {
            tenant_uuid: Some(tenant_uuid.to_string()),
        }
    }
}

impl Provider for MicrosoftTenantProvider {
    fn authorization_endpoint(&self) -> url::Url {
        url::Url::parse("https://login.microsoftonline.com/common/oauth2/v2.0/authorize").unwrap()
    }

    fn token_endpoint(&self) -> url::Url {
        url::Url::parse("https://login.microsoftonline.com/common/oauth2/v2.0/token").unwrap()
    }

    fn validate_iss(&self, iss: &str) -> bool {
        if let Some(tenant_uuid) = &self.tenant_uuid {
            format!("https://login.microsoftonline.com/{}/v2.0", tenant_uuid) == iss
        } else {
            // any tenant
            iss.starts_with("https://login.microsoftonline.com/") && iss.ends_with("/v2.0")
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;
    /*
        #[tokio::test]
        async fn discover_google() {
            let client = reqwest::Client::new();

            let provider = Provider::from_discovery(
                "https://accounts.google.com/.well-known/openid-configuration",
                &client,
            )
            .await
            .unwrap();

            assert_eq!(
                provider.authorization_endpoint,
                GOOGLE_PROVIDER.authorization_endpoint
            );
            assert_eq!(provider.issuer, GOOGLE_PROVIDER.issuer);
            assert_eq!(provider.jwks_uri, GOOGLE_PROVIDER.jwks_uri);
            assert_eq!(provider.token_endpoint, GOOGLE_PROVIDER.token_endpoint);
        }
    */
}