drogue_bazaar/auth/openid/
authenticator.rs

1use crate::{auth::openid::ExtendedClaims, core::config::CommaSeparatedVec};
2use anyhow::Context;
3use core::fmt::{Debug, Formatter};
4use futures_util::{stream, StreamExt, TryStreamExt};
5use openid::{
6    biscuit::jws::Compact, Claims, Client, CompactJson, Configurable, Discovered, Empty, Jws,
7};
8use serde::Deserialize;
9use std::collections::HashMap;
10use thiserror::Error;
11use tracing::instrument;
12use url::Url;
13
14#[derive(Clone, Debug, Deserialize)]
15pub struct AuthenticatorConfig {
16    #[serde(default)]
17    pub disabled: bool,
18
19    #[serde(flatten)]
20    pub global: AuthenticatorGlobalConfig,
21
22    #[serde(default)]
23    pub clients: HashMap<String, AuthenticatorClientConfig>,
24}
25
26#[derive(Clone, Debug, Deserialize)]
27pub struct AuthenticatorGlobalConfig {
28    pub issuer_url: Option<String>,
29
30    #[serde(default)]
31    pub redirect_url: Option<String>,
32
33    #[serde(default)]
34    pub tls_insecure: bool,
35
36    #[serde(default)]
37    pub tls_ca_certificates: CommaSeparatedVec,
38}
39
40#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
41pub struct AuthenticatorClientConfig {
42    pub client_id: String,
43    pub client_secret: String,
44    #[serde(default = "defaults::oauth2_scopes")]
45    pub scopes: String,
46    #[serde(default)]
47    pub issuer_url: Option<String>,
48
49    #[serde(default)]
50    pub tls_insecure: Option<bool>,
51    #[serde(default)]
52    pub tls_ca_certificates: Option<CommaSeparatedVec>,
53}
54
55mod defaults {
56    #[inline]
57    pub fn oauth2_scopes() -> String {
58        "openid profile email".into()
59    }
60}
61
62impl AuthenticatorConfig {
63    /// Create a client from a configuration. This respects the "disabled" field and returns
64    /// `None` in this case.
65    pub async fn into_client(self) -> anyhow::Result<Option<Authenticator>> {
66        if self.disabled {
67            Ok(None)
68        } else {
69            Ok(Some(Authenticator::new(self).await?))
70        }
71    }
72}
73
74impl ClientConfig for (&AuthenticatorGlobalConfig, &AuthenticatorClientConfig) {
75    fn client_id(&self) -> String {
76        self.1.client_id.clone()
77    }
78
79    fn client_secret(&self) -> String {
80        self.1.client_secret.clone()
81    }
82
83    fn redirect_url(&self) -> Option<String> {
84        self.0.redirect_url.clone()
85    }
86
87    /// Get the issuer URL, either the client's URL, or the global one.
88    fn issuer_url(&self) -> anyhow::Result<Url> {
89        let url = self
90            .1
91            .issuer_url
92            .clone()
93            .or_else(|| self.0.issuer_url.clone())
94            .ok_or_else(|| anyhow::anyhow!("Missing issuer or SSO URL"))?;
95
96        Url::parse(&url).context("Failed to parse issuer/SSO URL")
97    }
98
99    fn tls_insecure(&self) -> bool {
100        self.1.tls_insecure.unwrap_or(self.0.tls_insecure)
101    }
102
103    fn tls_ca_certificates(&self) -> Vec<String> {
104        self.1
105            .tls_ca_certificates
106            .clone()
107            .unwrap_or_else(|| self.0.tls_ca_certificates.clone())
108            .0
109    }
110}
111
112#[derive(Debug, Error)]
113pub enum AuthenticatorError {
114    #[error("Missing authenticator instance")]
115    Missing,
116    #[error("Authentication failed")]
117    Failed,
118}
119
120/// An authenticator to authenticate incoming requests.
121#[derive(Clone)]
122pub struct Authenticator {
123    clients: Vec<(String, Client<Discovered, ExtendedClaims>)>,
124}
125
126struct ClientsDebug<'a>(&'a [(String, Client<Discovered, ExtendedClaims>)]);
127
128impl<'a> Debug for ClientsDebug<'a> {
129    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
130        let mut d = f.debug_list();
131        for c in self.0 {
132            d.entry(&(&c.0, &c.1.client_id));
133        }
134        d.finish()
135    }
136}
137
138impl Debug for Authenticator {
139    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
140        let mut d = f.debug_struct("Authenticator");
141        d.field("clients", &ClientsDebug(self.clients.as_slice()));
142        d.finish()
143    }
144}
145
146impl Authenticator {
147    pub fn from_clients(clients: Vec<(String, Client<Discovered, ExtendedClaims>)>) -> Self {
148        Authenticator { clients }
149    }
150
151    /// Create a new authenticator by evaluating endpoints and SSO configuration.
152    pub async fn new(mut config: AuthenticatorConfig) -> anyhow::Result<Self> {
153        let configs = config.clients.drain();
154        Self::from_configs(config.global, configs).await
155    }
156
157    pub async fn from_configs<I>(
158        global: AuthenticatorGlobalConfig,
159        configs: I,
160    ) -> anyhow::Result<Self>
161    where
162        I: IntoIterator<Item = (String, AuthenticatorClientConfig)>,
163    {
164        let clients = stream::iter(configs)
165            .map(Ok)
166            .and_then(|config| {
167                let global = global.clone();
168                let name = config.0;
169                let config = config.1;
170                async move { create_client(&(&global, &config)).await.map(|c| (name, c)) }
171            })
172            .try_collect()
173            .await?;
174
175        Ok(Self::from_clients(clients))
176    }
177
178    /// Find a client by its configuration name.
179    ///
180    /// This is a brute force search and shouldn't be called that often.
181    pub fn client_by_name(&self, name: &str) -> Option<&Client<Discovered, ExtendedClaims>> {
182        self.clients
183            .iter()
184            .find(|client| client.0 == name)
185            .map(|client| &client.1)
186    }
187
188    fn find_client(
189        &self,
190        token: &Compact<ExtendedClaims, Empty>,
191    ) -> Result<Option<&Client<Discovered, ExtendedClaims>>, AuthenticatorError> {
192        let unverified_payload = token.unverified_payload().map_err(|err| {
193            log::info!("Failed to decode token payload: {}", err);
194            AuthenticatorError::Failed
195        })?;
196
197        let client_id = unverified_payload.standard_claims.azp.as_ref();
198
199        log::debug!(
200            "Searching client for: {} / {:?}",
201            unverified_payload.standard_claims.iss,
202            client_id
203        );
204
205        // find the client to use
206
207        let client = self.clients.iter().find(|client| {
208            let provider_iss = &client.1.provider.config().issuer;
209            let provider_id = &client.1.client_id;
210
211            log::debug!("Checking client: {} / {}", provider_iss, provider_id);
212            if provider_iss != &unverified_payload.standard_claims.iss {
213                return false;
214            }
215            if let Some(client_id) = client_id {
216                if client_id != provider_id {
217                    return false;
218                }
219            }
220
221            true
222        });
223
224        Ok(client.map(|c| &c.1))
225    }
226
227    /// Validate a bearer token.
228    #[instrument(level = "debug", skip_all, fields(token=token.as_ref()), ret)]
229    pub async fn validate_token<S: AsRef<str>>(
230        &self,
231        token: S,
232    ) -> Result<ExtendedClaims, AuthenticatorError> {
233        let mut token: Compact<ExtendedClaims, Empty> = Jws::new_encoded(token.as_ref());
234
235        let client = self.find_client(&token)?.ok_or_else(|| {
236            log::debug!("Unable to find client");
237            AuthenticatorError::Failed
238        })?;
239
240        log::debug!("Using client: {}", client.client_id);
241
242        // decode_token may panic if an unsupported algorithm is used. As that maybe user input,
243        // that could mean that a user could trigger a panic in this code. However, to us
244        // an unsupported algorithm simply means we reject the authentication.
245        client.decode_token(&mut token).map_err(|err| {
246            log::debug!("Failed to decode token: {}", err);
247            AuthenticatorError::Failed
248        })?;
249
250        log::debug!("Token: {:?}", token);
251
252        super::validate::validate_token(client, &token, None).map_err(|err| {
253            log::info!("Validation failed: {}", err);
254            AuthenticatorError::Failed
255        })?;
256
257        match token {
258            Compact::Decoded { payload, .. } => Ok(payload),
259            Compact::Encoded(_) => Err(AuthenticatorError::Failed),
260        }
261    }
262}
263
264pub trait ClientConfig {
265    fn client_id(&self) -> String;
266    fn client_secret(&self) -> String;
267    fn redirect_url(&self) -> Option<String>;
268    fn issuer_url(&self) -> anyhow::Result<Url>;
269    fn tls_insecure(&self) -> bool;
270    fn tls_ca_certificates(&self) -> Vec<String>;
271}
272
273pub async fn create_client<C: ClientConfig, P: CompactJson + Claims>(
274    config: &C,
275) -> anyhow::Result<Client<Discovered, P>> {
276    let mut client = crate::reqwest::ClientFactory::new();
277
278    if config.tls_insecure() {
279        client = client.make_insecure();
280    }
281
282    for ca in config.tls_ca_certificates() {
283        client = client.add_ca_cert(ca);
284    }
285
286    let client = Client::<Discovered, P>::discover_with_client(
287        client.build()?,
288        config.client_id(),
289        config.client_secret(),
290        config.redirect_url(),
291        config.issuer_url()?,
292    )
293    .await?;
294
295    log::info!("Discovered OpenID: {:#?}", client.config());
296
297    Ok(client)
298}
299
300#[cfg(test)]
301mod test {
302
303    use super::*;
304    use crate::core::config::ConfigFromEnv;
305    use openid::biscuit::ClaimsSet;
306
307    #[test]
308    fn test_decode() -> anyhow::Result<()> {
309        let token = r#"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJEZ2hoSVVwV2llSU5jX0Jtc0lDckhHbm1WTDNMMTMteURtVmp3N2MwUnlFIn0.eyJleHAiOjE2MTg0OTQ5MjYsImlhdCI6MTYxODQ5NDYyNiwianRpIjoiNjAzYTNhMGYtZTkzMC00ZjE1LTkwMDUtMTZjNzFiMTllNDdiIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay1kcm9ndWUtZGV2LmFwcHMud29uZGVyZnVsLmlvdC1wbGF5Z3JvdW5kLm9yZy9hdXRoL3JlYWxtcy9kcm9ndWUiLCJhdWQiOlsic2VydmljZXMiLCJncmFmYW5hIiwiZGl0dG8iLCJkcm9ndWUiLCJhY2NvdW50Il0sInN1YiI6ImI4ZWZjZjAwLTJmZmYtNDRlYS1hZGU5LWYzNWViMmY0ZmNlMSIsInR5cCI6IkJlYXJlciIsImF6cCI6InNlcnZpY2VzIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiZ3JhZmFuYSI6eyJyb2xlcyI6WyJncmFmYW5hLWVkaXRvciIsImdyYWZhbmEtYWRtaW4iXX0sImRpdHRvIjp7InJvbGVzIjpbImRpdHRvLXVzZXIiLCJkaXR0by1hZG1pbiJdfSwiZHJvZ3VlIjp7InJvbGVzIjpbImRyb2d1ZS11c2VyIiwiZHJvZ3VlLWFkbWluIl19LCJzZXJ2aWNlcyI6eyJyb2xlcyI6WyJkcm9ndWUtdXNlciIsImRyb2d1ZS1hZG1pbiJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJzZXJ2aWNlcyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjE5Mi4xNjguMTIuMSIsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1zZXJ2aWNlcyIsImNsaWVudEFkZHJlc3MiOiIxOTIuMTY4LjEyLjEifQ.JNvytxz-IqTXXoUKF8xZMw-diS7jtkz9GP4u6MRo9iny410zTxSl5Z_O9Mhy1LofxPBMYt65JWs6tRBdKAEXa0w5bLbZdyRgdr3SJpDAxIz6CezCHqSDl1OSQPrW_rWmaS_9XLWxl8fgADwLCNjWbrZrsls_E_rDdfjqhrvcE4f2__lIV_oeG7zcfyYJzNVoZ3Ukyadxq6fwAMf8kZwU_6R6hClb0Ya6jLpNE3miy3ZgugZ1QLJT3tSTyyxzSHMy8146ncBughepequ-zKSnbzQjhgwQsARjjv7bBeZgRjRY6kF3Wr8JalaR2DZU49RopfegZ-9PWO2AEH2dxe4OfQ"#;
310        let token: Compact<ClaimsSet<serde_json::Value>, serde_json::Value> =
311            Jws::new_encoded(token);
312
313        println!("Header: {:#?}", token.unverified_header());
314        println!("Payload: {:#?}", token.unverified_payload());
315
316        let token = match token {
317            Compact::Encoded(encoded) => {
318                let header = encoded.part(0)?;
319                let decoded_claims = encoded.part(1)?;
320                Jws::new_decoded(header, decoded_claims)
321            }
322            Compact::Decoded { .. } => token,
323        };
324
325        println!("Token: {:#?}", token);
326
327        Ok(())
328    }
329
330    #[test]
331    fn test_empty_config() {
332        AuthenticatorConfig::from_env().expect("Empty config is ok");
333    }
334
335    #[test]
336    fn test_standard_config() {
337        #[derive(Deserialize)]
338        struct Config {
339            pub oauth: AuthenticatorConfig,
340        }
341
342        let mut set = HashMap::new();
343        set.insert("OAUTH__ISSUER_URL", "http://sso.url");
344
345        set.insert("OAUTH__CLIENTS__FOO__CLIENT_ID", "client.id.1");
346        set.insert("OAUTH__CLIENTS__FOO__CLIENT_SECRET", "client.secret.1");
347
348        set.insert("OAUTH__CLIENTS__BAR__CLIENT_ID", "client.id.2");
349        set.insert("OAUTH__CLIENTS__BAR__CLIENT_SECRET", "");
350
351        let cfg = Config::from_set(set).expect("Config should be ok");
352
353        assert_eq!(cfg.oauth.global.issuer_url, Some("http://sso.url".into()));
354
355        assert_eq!(
356            cfg.oauth.clients.get("foo"),
357            Some(&AuthenticatorClientConfig {
358                client_id: "client.id.1".into(),
359                client_secret: "client.secret.1".into(),
360                scopes: defaults::oauth2_scopes(),
361                issuer_url: None,
362                tls_insecure: None,
363                tls_ca_certificates: None,
364            })
365        );
366
367        assert_eq!(
368            cfg.oauth.clients.get("bar"),
369            Some(&AuthenticatorClientConfig {
370                client_id: "client.id.2".into(),
371                client_secret: "".into(),
372                scopes: defaults::oauth2_scopes(),
373                issuer_url: None,
374                tls_insecure: None,
375                tls_ca_certificates: None,
376            })
377        );
378    }
379}