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