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.scope_order = OAuthScopeOrder::RequestedThenConfigured;
213 _ = policy
214 .configured_scopes
215 .splice(0..0, std::mem::take(&mut self.provider.scopes));
216 policy.end_session = if self.disable_provider_logout {
217 None
218 } else {
219 metadata
220 .end_session_endpoint
221 .clone()
222 .map(|endpoint| super::OAuthEndSessionConfig {
223 endpoint,
224 post_logout_redirect_uri: self.post_logout_redirect_uri,
225 })
226 };
227 policy.issuer.clone_from(&metadata.issuer);
228 policy.verify_grant_id_token = metadata.jwks_url.is_some();
229 policy.id_token_nonce_binding =
230 policy.verify_grant_id_token && !self.disable_id_token_nonce_binding;
231 policy.discovery_openid_scope = is_oidc;
232 policy.source_profile_exceptions = true;
233 self.provider.auth_url = self.authorization_url.unwrap_or_default();
234 self.provider.token_url = self.token_url.unwrap_or_default();
235 self.provider.user_info_url.clone_from(&self.user_info_url);
236 if self.provider.account_subject.is_none() {
237 self.provider.account_subject = Some(if policy.discovery_openid_scope {
238 oidc_subject
239 } else {
240 oauth_subject
241 });
242 }
243 if self.provider.get_user_info.is_none() {
244 self.provider.get_user_info = Some(Arc::new(GenericUserInfo {
245 url: self.user_info_url,
246 mapper: self.map_profile,
247 }));
248 }
249 Ok(Some(GenericOAuthResolved {
250 provider: self.provider,
251 metadata,
252 }))
253 }
254}
255fn string(value: &Value, key: &str) -> Option<String> {
256 value.get(key).and_then(Value::as_str).map(str::to_owned)
257}
258async fn fetch_discovery(url: &str, headers: &[(String, String)]) -> Option<Value> {
259 let mut request = reqwest::Client::new().get(url);
260 for (key, value) in headers {
261 request = request.header(key, value);
262 }
263 let document: Value = request
264 .send()
265 .await
266 .ok()?
267 .error_for_status()
268 .ok()?
269 .json()
270 .await
271 .ok()?;
272 if let Some(issuer) = string(&document, "issuer").filter(|issuer| !issuer.is_empty())
273 && url::Url::parse(&issuer).is_err()
274 {
275 return None;
276 }
277 Some(document)
278}
279fn oauth_subject(profile: &Value) -> Result<String, String> {
280 super::remaining_profile::raw_subject(profile.get("id"))
281}
282fn oidc_subject(profile: &Value) -> Result<String, String> {
283 super::remaining_profile::raw_subject(profile.get("sub"))
284}
285struct GenericUserInfo {
286 url: Option<String>,
287 mapper: Option<Arc<dyn OAuthProfileMapper>>,
288}
289#[async_trait]
290impl OAuthUserInfoHandler for GenericUserInfo {
291 fn errors_are_exceptions(&self) -> bool {
292 false
293 }
294 async fn get_user_info(
295 &self,
296 request: OAuthUserInfoRequest,
297 ) -> Result<OAuthUserInfoResponse, String> {
298 let decoded = request
301 .id_token
302 .as_deref()
303 .and_then(|token| token.split('.').nth(1))
304 .and_then(|payload| {
305 base64::engine::general_purpose::URL_SAFE_NO_PAD
306 .decode(payload)
307 .ok()
308 })
309 .and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
310 .filter(|profile| {
311 profile
312 .get("sub")
313 .is_some_and(super::remaining_profile::truthy)
314 && profile
315 .get("email")
316 .is_some_and(super::remaining_profile::truthy)
317 });
318 let mut raw = if let Some(mut profile) = decoded {
319 if let Some(object) = profile.as_object_mut() {
320 let id = object.get("sub").cloned().unwrap_or(Value::Null);
321 _ = object.entry("id").or_insert(id);
322 let verified = object.get("email_verified").cloned().unwrap_or(Value::Null);
323 _ = object.entry("emailVerified").or_insert(verified);
324 if let Some(image) = object.get("picture").cloned() {
325 _ = object.entry("image").or_insert(image);
326 }
327 }
328 profile
329 } else {
330 let url = self
331 .url
332 .as_ref()
333 .ok_or("Generic OAuth user info unavailable")?;
334 let mut profile: Value = reqwest::Client::new()
335 .get(url)
336 .bearer_auth(request.access_token.as_deref().unwrap_or("undefined"))
337 .send()
338 .await
339 .map_err(|_| "Generic OAuth user info unavailable")?
340 .error_for_status()
341 .map_err(|_| "Generic OAuth user info unavailable")?
342 .json()
343 .await
344 .map_err(|_| "Generic OAuth user info unavailable")?;
345 if let Some(object) = profile.as_object_mut() {
346 _ = object.insert(
347 "emailVerified".into(),
348 object
349 .get("email_verified")
350 .cloned()
351 .unwrap_or(Value::Bool(false)),
352 );
353 if let Some(image) = object.get("picture").cloned() {
354 _ = object.insert("image".into(), image);
355 } else {
356 _ = object.remove("image");
357 }
358 }
359 profile
360 };
361 let user = OAuthUserInfo {
362 additional_fields: Map::default(),
363 id: string(&raw, "id").unwrap_or_default(),
364 email: string(&raw, "email").unwrap_or_default(),
365 name: string(&raw, "name"),
366 image: string(&raw, "image"),
367 email_verified: raw
368 .get("emailVerified")
369 .is_some_and(super::remaining_profile::truthy),
370 };
371 let mut response = OAuthUserInfoResponse {
372 user_output: None,
373 user,
374 data: raw.take(),
375 };
376 if let Some(mapper) = &self.mapper {
377 let mapped = mapper
378 .map_profile(response.data.clone())
379 .await
380 .map_err(|_| {
381 format!(
382 "{}Generic OAuth mapping failed",
383 super::remaining_profile::PROFILE_EXCEPTION_PREFIX
384 )
385 })?;
386 super::apply_application_mapping(&mut response, mapped)?;
387 }
388 Ok(response)
389 }
390}
391struct DiscoveryVerifier {
392 keys: Arc<dyn OAuthJwksSource>,
393 issuer: String,
394 audience: String,
395 algorithms: Option<Vec<String>>,
396}
397#[async_trait]
398impl OAuthIdTokenVerifier for DiscoveryVerifier {
399 async fn verify_id_token(&self, token: &str, nonce: Option<&str>) -> Result<bool, String> {
400 Ok(self.verify(token, nonce).await.unwrap_or(false))
401 }
402}
403impl DiscoveryVerifier {
404 async fn verify(&self, token: &str, nonce: Option<&str>) -> Option<bool> {
405 let header = jsonwebtoken::decode_header(token).ok()?;
406 let alg_name = serde_json::to_value(header.alg).ok()?;
407 if self.algorithms.as_ref().is_some_and(|algorithms| {
408 !algorithms
409 .iter()
410 .any(|alg| Some(alg.as_str()) == alg_name.as_str())
411 }) {
412 return Some(false);
413 }
414 if matches!(
416 header.alg,
417 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
418 ) {
419 return Some(false);
420 }
421 let raw_header: Value = serde_json::from_slice(
422 &base64::engine::general_purpose::URL_SAFE_NO_PAD
423 .decode(token.split('.').next()?)
424 .ok()?,
425 )
426 .ok()?;
427 if let Some(critical) = raw_header.get("crit") {
428 let names = critical.as_array()?;
429 if names.is_empty()
430 || names.iter().any(|name| name.as_str() != Some("b64"))
431 || raw_header.get("b64").and_then(Value::as_bool) != Some(true)
432 {
433 return Some(false);
434 }
435 }
436 let keys = self.keys.fetch_keys().await.ok()?;
437 let matching: Vec<_> = keys
438 .iter()
439 .filter(|key| {
440 header
441 .kid
442 .as_ref()
443 .is_none_or(|kid| key.get("kid").and_then(Value::as_str) == Some(kid))
444 && key.get("d").is_none()
445 && key.get("alg").is_none_or(|alg| alg == &alg_name)
446 && key
447 .get("use")
448 .is_none_or(|usage| usage.as_str() == Some("sig"))
449 && key.get("key_ops").is_none_or(|ops| {
450 ops.as_array().is_some_and(|ops| {
451 ops.iter().any(|op| op.as_str() == Some("verify"))
452 && ops.iter().enumerate().all(|(index, op)| {
453 op.is_string()
454 && ops.iter().take(index).all(|previous| previous != op)
455 })
456 })
457 })
458 && match header.alg {
459 Algorithm::RS256
460 | Algorithm::RS384
461 | Algorithm::RS512
462 | Algorithm::PS256
463 | Algorithm::PS384
464 | Algorithm::PS512 => key.get("kty").and_then(Value::as_str) == Some("RSA"),
465 Algorithm::ES256 | Algorithm::ES384 => {
466 key.get("kty").and_then(Value::as_str) == Some("EC")
467 }
468 Algorithm::EdDSA => key.get("kty").and_then(Value::as_str) == Some("OKP"),
469 _ => false,
470 }
471 })
472 .collect();
473 if matching.len() != 1 {
474 return Some(false);
475 }
476 if matching.first()?.get("kty").and_then(Value::as_str) == Some("RSA")
477 && !crate::oauth::id_token::remote_rsa_public_key(matching.first()?)
478 {
479 return Some(false);
480 }
481 let jwk =
482 serde_json::from_value::<jsonwebtoken::jwk::Jwk>((*matching.first()?).clone()).ok()?;
483 let key = DecodingKey::from_jwk(&jwk).ok()?;
484 let (signed, signature) = token.rsplit_once('.')?;
485 if !jsonwebtoken::crypto::verify(signature, signed.as_bytes(), &key, header.alg).ok()? {
486 return Some(false);
487 }
488 let claims: Value = serde_json::from_slice(
489 &base64::engine::general_purpose::URL_SAFE_NO_PAD
490 .decode(token.split('.').nth(1)?)
491 .ok()?,
492 )
493 .ok()?;
494 if claims.get("iss").and_then(Value::as_str) != Some(self.issuer.as_str()) {
495 return Some(false);
496 }
497 let audience = claims.get("aud")?;
498 if audience.as_str() != Some(self.audience.as_str())
499 && !audience.as_array().is_some_and(|values| {
500 values
501 .iter()
502 .any(|value| value.as_str() == Some(self.audience.as_str()))
503 })
504 {
505 return Some(false);
506 }
507 if claims.get("iat").is_some_and(|iat| !iat.is_number()) {
508 return Some(false);
509 }
510 let now = std::time::SystemTime::now()
511 .duration_since(std::time::UNIX_EPOCH)
512 .ok()?
513 .as_secs_f64();
514 if let Some(exp) = claims.get("exp")
515 && exp.as_f64()? <= now
516 {
517 return Some(false);
518 }
519 if let Some(nbf) = claims.get("nbf")
520 && nbf.as_f64()? > now
521 {
522 return Some(false);
523 }
524 Some(
525 nonce
526 .filter(|nonce| !nonce.is_empty())
527 .is_none_or(|nonce| claims.get("nonce").and_then(Value::as_str) == Some(nonce)),
528 )
529 }
530}