drogue_bazaar/auth/openid/
authenticator.rs1use 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 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 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#[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 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 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 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 #[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 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}