alibi_plugins/oauth/providers/
generic.rs1use super::OAuthTokenEndpointAuth::{
3 ClientSecretBasic, ClientSecretPost, None as Public, PrivateKeyJwt,
4};
5use super::{
6 OAuthAuthorizationPolicy, OAuthIdTokenVerifier, OAuthProfileMapper, OAuthProvider,
7 OAuthScopeOrder, OAuthUserInfo, OAuthUserInfoHandler, OAuthUserInfoRequest,
8 OAuthUserInfoResponse,
9};
10use crate::oauth::{HttpOAuthJwksSource, OAuthJwksSource};
11use async_trait::async_trait;
12use base64::Engine;
13use jsonwebtoken::{Algorithm, DecodingKey};
14use serde_json::Map;
15use serde_json::Value;
16use std::sync::Arc;
17
18pub struct GenericOAuthConfig {
21 pub provider: OAuthProvider,
22 pub discovery_url: Option<String>,
23 pub discovery_headers: Vec<(String, String)>,
24 pub authorization_url: Option<String>,
26 pub token_url: Option<String>,
27 pub user_info_url: Option<String>,
28 pub end_session_endpoint: Option<String>,
29 pub post_logout_redirect_uri: Option<String>,
31 pub disable_provider_logout: bool,
33 pub require_id_token_verification: bool,
34 pub disable_id_token_nonce_binding: bool,
35 pub map_profile: Option<Arc<dyn OAuthProfileMapper>>,
36 pub access_token_expires_in: Option<f64>,
38 pub account_key: Option<super::OAuthAccountKey>,
39}
40#[derive(Debug, Clone, Default)]
42pub struct GenericOAuthMetadata {
43 pub issuer: Option<String>,
44 pub jwks_url: Option<String>,
45 pub signing_algorithms: Vec<String>,
46 pub end_session_endpoint: Option<String>,
47}
48pub struct GenericOAuthResolved {
49 pub provider: OAuthProvider,
50 pub metadata: GenericOAuthMetadata,
51}
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum GenericOAuthError {
55 RequiredVerificationUnavailable,
56 InvalidTokenAuthentication,
57}
58impl std::fmt::Display for GenericOAuthError {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.write_str(match self {
61 Self::RequiredVerificationUnavailable => {
62 "Generic OAuth requires discovery ID-token verification metadata"
63 }
64 Self::InvalidTokenAuthentication => {
65 "Generic OAuth token authentication conflicts with configured credentials"
66 }
67 })
68 }
69}
70impl std::error::Error for GenericOAuthError {}
71impl GenericOAuthConfig {
72 #[must_use]
73 pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
74 Self {
75 provider: OAuthProvider {
76 client_id: client_id.into(),
77 client_secret: client_secret.into(),
78 additional_client_ids: Vec::new(),
79 hosted_domain: None,
80 require_email_verification: false,
81 auth_url: String::new(),
82 token_url: String::new(),
83 user_info_url: None,
84 scopes: Vec::new(),
85 authorization: Some(OAuthAuthorizationPolicy {
86 scope_order: OAuthScopeOrder::RequestedThenConfigured,
87 ..Default::default()
88 }),
89 authorization_params: Vec::new(),
90 account_subject: None,
91 map_user_info: None,
92 get_user_info: None,
93 refresh_access_token: None,
94 verify_id_token: None,
95 id_token: None,
96 disable_id_token_sign_in: false,
97 disable_implicit_sign_up: false,
98 disable_sign_up: false,
99 allow_idp_initiated: false,
100 override_user_info_on_sign_in: false,
101 },
102 discovery_url: None,
103 discovery_headers: Vec::new(),
104 authorization_url: None,
105 token_url: None,
106 user_info_url: None,
107 end_session_endpoint: None,
108 post_logout_redirect_uri: None,
109 disable_provider_logout: false,
110 require_id_token_verification: false,
111 disable_id_token_nonce_binding: false,
112 map_profile: None,
113 access_token_expires_in: None,
114 account_key: None,
115 }
116 }
117 pub async fn resolve(mut self) -> Result<Option<GenericOAuthResolved>, GenericOAuthError> {
120 let mut metadata = GenericOAuthMetadata::default();
121 let mut is_oidc = false;
122 let discovered = if let Some(url) = &self.discovery_url {
123 fetch_discovery(url, &self.discovery_headers).await
124 } else {
125 None
126 };
127 if let Some(document) = &discovered {
128 self.authorization_url = self
129 .authorization_url
130 .or_else(|| string(document, "authorization_endpoint"));
131 self.token_url = self
132 .token_url
133 .or_else(|| string(document, "token_endpoint"));
134 self.user_info_url = self
135 .user_info_url
136 .or_else(|| string(document, "userinfo_endpoint"));
137 metadata.issuer = string(document, "issuer");
138 is_oidc = document
139 .get("id_token_signing_alg_values_supported")
140 .and_then(Value::as_array)
141 .is_some_and(|values| !values.is_empty());
142 metadata.signing_algorithms = document
143 .get("id_token_signing_alg_values_supported")
144 .and_then(Value::as_array)
145 .map(|values| {
146 values
147 .iter()
148 .filter_map(Value::as_str)
149 .map(str::to_owned)
150 .collect()
151 })
152 .unwrap_or_default();
153 metadata.end_session_endpoint = self
154 .end_session_endpoint
155 .or_else(|| string(document, "end_session_endpoint"));
156 if let (Some(jwks), Some(issuer)) = (string(document, "jwks_uri"), &metadata.issuer)
157 && !jwks.is_empty()
158 && !issuer.is_empty()
159 {
160 let Ok(base) = url::Url::parse(self.discovery_url.as_deref().unwrap_or_default())
161 else {
162 return Ok(None);
163 };
164 let Ok(url) = base.join(&jwks) else {
165 return Ok(None);
166 };
167 metadata.jwks_url = Some(url.to_string());
168 self.provider.verify_id_token = Some(Arc::new(DiscoveryVerifier {
169 keys: Arc::new(HttpOAuthJwksSource::new(url.to_string())),
170 issuer: issuer.clone(),
171 audience: self.provider.client_id.clone(),
172 algorithms: is_oidc.then(|| metadata.signing_algorithms.clone()),
173 }));
174 }
175 } else {
176 metadata.end_session_endpoint = self.end_session_endpoint;
177 }
178 let policy = self
179 .provider
180 .authorization
181 .get_or_insert_with(Default::default);
182 if self.account_key.is_some() {
183 policy.account_key = self.account_key;
184 }
185 policy.allow_missing_access_token = true;
186 policy.default_access_token_expires_in = self.access_token_expires_in;
187 if self.discovery_url.is_some()
188 && (self.authorization_url.as_ref().is_none_or(String::is_empty)
189 || self.token_url.as_ref().is_none_or(String::is_empty)
190 && policy.authorization_code.is_none())
191 {
192 return Ok(None);
193 }
194 if self.require_id_token_verification && metadata.jwks_url.is_none() {
195 return if self.discovery_url.is_some() {
196 Ok(None)
197 } else {
198 Err(GenericOAuthError::RequiredVerificationUnavailable)
199 };
200 }
201 if matches!(policy.token_endpoint_auth, Some(Public | PrivateKeyJwt))
202 && !self.provider.client_secret.is_empty()
203 || matches!(
204 policy.token_endpoint_auth,
205 Some(ClientSecretBasic | ClientSecretPost)
206 ) && self.provider.client_secret.is_empty()
207 {
208 return Err(GenericOAuthError::InvalidTokenAuthentication);
209 }
210 _ = policy
212 .configured_scopes
213 .splice(0..0, std::mem::take(&mut self.provider.scopes));
214 policy.end_session = if self.disable_provider_logout {
215 None
216 } else {
217 metadata
218 .end_session_endpoint
219 .clone()
220 .map(|endpoint| super::OAuthEndSessionConfig {
221 endpoint,
222 post_logout_redirect_uri: self.post_logout_redirect_uri,
223 })
224 };
225 policy.verify_grant_id_token = metadata.jwks_url.is_some();
226 policy.id_token_nonce_binding =
227 policy.verify_grant_id_token && !self.disable_id_token_nonce_binding;
228 policy.discovery_openid_scope = is_oidc;
229 policy.source_profile_exceptions = true;
230 self.provider.auth_url = self.authorization_url.unwrap_or_default();
231 self.provider.token_url = self.token_url.unwrap_or_default();
232 self.provider.user_info_url.clone_from(&self.user_info_url);
233 if self.provider.account_subject.is_none() {
234 self.provider.account_subject = Some(if policy.discovery_openid_scope {
235 oidc_subject
236 } else {
237 oauth_subject
238 });
239 }
240 if self.provider.get_user_info.is_none() {
241 self.provider.get_user_info = Some(Arc::new(GenericUserInfo {
242 url: self.user_info_url,
243 mapper: self.map_profile,
244 }));
245 }
246 Ok(Some(GenericOAuthResolved {
247 provider: self.provider,
248 metadata,
249 }))
250 }
251}
252fn string(value: &Value, key: &str) -> Option<String> {
253 value.get(key).and_then(Value::as_str).map(str::to_owned)
254}
255async fn fetch_discovery(url: &str, headers: &[(String, String)]) -> Option<Value> {
256 let mut request = reqwest::Client::new().get(url);
257 for (key, value) in headers {
258 request = request.header(key, value);
259 }
260 let document: Value = request
261 .send()
262 .await
263 .ok()?
264 .error_for_status()
265 .ok()?
266 .json()
267 .await
268 .ok()?;
269 if let Some(issuer) = string(&document, "issuer").filter(|issuer| !issuer.is_empty())
270 && url::Url::parse(&issuer).is_err()
271 {
272 return None;
273 }
274 Some(document)
275}
276fn oauth_subject(profile: &Value) -> Result<String, String> {
277 super::remaining_profile::raw_subject(profile.get("id"))
278}
279fn oidc_subject(profile: &Value) -> Result<String, String> {
280 super::remaining_profile::raw_subject(profile.get("sub"))
281}
282struct GenericUserInfo {
283 url: Option<String>,
284 mapper: Option<Arc<dyn OAuthProfileMapper>>,
285}
286#[async_trait]
287impl OAuthUserInfoHandler for GenericUserInfo {
288 fn errors_are_exceptions(&self) -> bool {
289 false
290 }
291 async fn get_user_info(
292 &self,
293 request: OAuthUserInfoRequest,
294 ) -> Result<OAuthUserInfoResponse, String> {
295 let decoded = request
298 .id_token
299 .as_deref()
300 .and_then(|token| token.split('.').nth(1))
301 .and_then(|payload| {
302 base64::engine::general_purpose::URL_SAFE_NO_PAD
303 .decode(payload)
304 .ok()
305 })
306 .and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
307 .filter(|profile| {
308 profile
309 .get("sub")
310 .is_some_and(super::remaining_profile::truthy)
311 && profile
312 .get("email")
313 .is_some_and(super::remaining_profile::truthy)
314 });
315 let mut raw = if let Some(mut profile) = decoded {
316 if let Some(object) = profile.as_object_mut() {
317 let id = object.get("sub").cloned().unwrap_or(Value::Null);
318 _ = object.entry("id").or_insert(id);
319 let verified = object.get("email_verified").cloned().unwrap_or(Value::Null);
320 _ = object.entry("emailVerified").or_insert(verified);
321 if let Some(image) = object.get("picture").cloned() {
322 _ = object.entry("image").or_insert(image);
323 }
324 }
325 profile
326 } else {
327 let url = self
328 .url
329 .as_ref()
330 .ok_or("Generic OAuth user info unavailable")?;
331 let mut profile: Value = reqwest::Client::new()
332 .get(url)
333 .bearer_auth(request.access_token.as_deref().unwrap_or("undefined"))
334 .send()
335 .await
336 .map_err(|_| "Generic OAuth user info unavailable")?
337 .error_for_status()
338 .map_err(|_| "Generic OAuth user info unavailable")?
339 .json()
340 .await
341 .map_err(|_| "Generic OAuth user info unavailable")?;
342 if let Some(object) = profile.as_object_mut() {
343 _ = object.insert(
344 "emailVerified".into(),
345 object
346 .get("email_verified")
347 .cloned()
348 .unwrap_or(Value::Bool(false)),
349 );
350 if let Some(image) = object.get("picture").cloned() {
351 _ = object.insert("image".into(), image);
352 } else {
353 _ = object.remove("image");
354 }
355 }
356 profile
357 };
358 let user = OAuthUserInfo {
359 additional_fields: Map::default(),
360 id: string(&raw, "id").unwrap_or_default(),
361 email: string(&raw, "email").unwrap_or_default(),
362 name: string(&raw, "name"),
363 image: string(&raw, "image"),
364 email_verified: raw
365 .get("emailVerified")
366 .is_some_and(super::remaining_profile::truthy),
367 };
368 let mut response = OAuthUserInfoResponse {
369 user_output: None,
370 user,
371 data: raw.take(),
372 };
373 if let Some(mapper) = &self.mapper {
374 let mapped = mapper
375 .map_profile(response.data.clone())
376 .await
377 .map_err(|_| {
378 format!(
379 "{}Generic OAuth mapping failed",
380 super::remaining_profile::PROFILE_EXCEPTION_PREFIX
381 )
382 })?;
383 super::apply_application_mapping(&mut response, mapped)?;
384 }
385 Ok(response)
386 }
387}
388struct DiscoveryVerifier {
389 keys: Arc<dyn OAuthJwksSource>,
390 issuer: String,
391 audience: String,
392 algorithms: Option<Vec<String>>,
393}
394#[async_trait]
395impl OAuthIdTokenVerifier for DiscoveryVerifier {
396 async fn verify_id_token(&self, token: &str, nonce: Option<&str>) -> Result<bool, String> {
397 Ok(self.verify(token, nonce).await.unwrap_or(false))
398 }
399}
400impl DiscoveryVerifier {
401 async fn verify(&self, token: &str, nonce: Option<&str>) -> Option<bool> {
402 let header = jsonwebtoken::decode_header(token).ok()?;
403 let alg_name = serde_json::to_value(header.alg).ok()?;
404 if self.algorithms.as_ref().is_some_and(|algorithms| {
405 !algorithms
406 .iter()
407 .any(|alg| Some(alg.as_str()) == alg_name.as_str())
408 }) {
409 return Some(false);
410 }
411 if matches!(
413 header.alg,
414 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
415 ) {
416 return Some(false);
417 }
418 let raw_header: Value = serde_json::from_slice(
419 &base64::engine::general_purpose::URL_SAFE_NO_PAD
420 .decode(token.split('.').next()?)
421 .ok()?,
422 )
423 .ok()?;
424 if let Some(critical) = raw_header.get("crit") {
425 let names = critical.as_array()?;
426 if names.is_empty()
427 || names.iter().any(|name| name.as_str() != Some("b64"))
428 || raw_header.get("b64").and_then(Value::as_bool) != Some(true)
429 {
430 return Some(false);
431 }
432 }
433 let keys = self.keys.fetch_keys().await.ok()?;
434 let matching: Vec<_> = keys
435 .iter()
436 .filter(|key| {
437 header
438 .kid
439 .as_ref()
440 .is_none_or(|kid| key.get("kid").and_then(Value::as_str) == Some(kid))
441 && key.get("d").is_none()
442 && key.get("alg").is_none_or(|alg| alg == &alg_name)
443 && key
444 .get("use")
445 .is_none_or(|usage| usage.as_str() == Some("sig"))
446 && key.get("key_ops").is_none_or(|ops| {
447 ops.as_array().is_some_and(|ops| {
448 ops.iter().any(|op| op.as_str() == Some("verify"))
449 && ops.iter().enumerate().all(|(index, op)| {
450 op.is_string()
451 && ops.iter().take(index).all(|previous| previous != op)
452 })
453 })
454 })
455 && match header.alg {
456 Algorithm::RS256
457 | Algorithm::RS384
458 | Algorithm::RS512
459 | Algorithm::PS256
460 | Algorithm::PS384
461 | Algorithm::PS512 => key.get("kty").and_then(Value::as_str) == Some("RSA"),
462 Algorithm::ES256 | Algorithm::ES384 => {
463 key.get("kty").and_then(Value::as_str) == Some("EC")
464 }
465 Algorithm::EdDSA => key.get("kty").and_then(Value::as_str) == Some("OKP"),
466 _ => false,
467 }
468 })
469 .collect();
470 if matching.len() != 1 {
471 return Some(false);
472 }
473 if matching.first()?.get("kty").and_then(Value::as_str) == Some("RSA")
474 && !crate::oauth::id_token::remote_rsa_public_key(matching.first()?)
475 {
476 return Some(false);
477 }
478 let jwk =
479 serde_json::from_value::<jsonwebtoken::jwk::Jwk>((*matching.first()?).clone()).ok()?;
480 let key = DecodingKey::from_jwk(&jwk).ok()?;
481 let (signed, signature) = token.rsplit_once('.')?;
482 if !jsonwebtoken::crypto::verify(signature, signed.as_bytes(), &key, header.alg).ok()? {
483 return Some(false);
484 }
485 let claims: Value = serde_json::from_slice(
486 &base64::engine::general_purpose::URL_SAFE_NO_PAD
487 .decode(token.split('.').nth(1)?)
488 .ok()?,
489 )
490 .ok()?;
491 if claims.get("iss").and_then(Value::as_str) != Some(self.issuer.as_str()) {
492 return Some(false);
493 }
494 let audience = claims.get("aud")?;
495 if audience.as_str() != Some(self.audience.as_str())
496 && !audience.as_array().is_some_and(|values| {
497 values
498 .iter()
499 .any(|value| value.as_str() == Some(self.audience.as_str()))
500 })
501 {
502 return Some(false);
503 }
504 if claims.get("iat").is_some_and(|iat| !iat.is_number()) {
505 return Some(false);
506 }
507 let now = std::time::SystemTime::now()
508 .duration_since(std::time::UNIX_EPOCH)
509 .ok()?
510 .as_secs_f64();
511 if let Some(exp) = claims.get("exp")
512 && exp.as_f64()? <= now
513 {
514 return Some(false);
515 }
516 if let Some(nbf) = claims.get("nbf")
517 && nbf.as_f64()? > now
518 {
519 return Some(false);
520 }
521 Some(
522 nonce
523 .filter(|nonce| !nonce.is_empty())
524 .is_none_or(|nonce| claims.get("nonce").and_then(Value::as_str) == Some(nonce)),
525 )
526 }
527}