alibi_api/plugins/oauth/providers/
generic.rs1use super::{
3 OAuthAuthorizationPolicy, OAuthIdTokenVerifier, OAuthProfileMapper, OAuthProvider,
4 OAuthScopeOrder, OAuthUserInfo, OAuthUserInfoHandler, OAuthUserInfoRequest,
5 OAuthUserInfoResponse,
6};
7use crate::plugins::oauth::{HttpOAuthJwksSource, OAuthJwksSource};
8use async_trait::async_trait;
9use base64::Engine;
10use jsonwebtoken::{Algorithm, DecodingKey};
11use serde_json::Value;
12use std::sync::Arc;
13
14pub struct GenericOAuthConfig {
17 pub provider: OAuthProvider,
18 pub discovery_url: Option<String>,
19 pub discovery_headers: Vec<(String, String)>,
20 pub authorization_url: Option<String>,
22 pub token_url: Option<String>,
23 pub user_info_url: Option<String>,
24 pub end_session_endpoint: Option<String>,
25 pub post_logout_redirect_uri: Option<String>,
27 pub disable_provider_logout: bool,
29 pub require_id_token_verification: bool,
30 pub disable_id_token_nonce_binding: bool,
31 pub map_profile: Option<Arc<dyn OAuthProfileMapper>>,
32 pub access_token_expires_in: Option<f64>,
34 pub account_key: Option<super::OAuthAccountKey>,
35}
36#[derive(Debug, Clone, Default)]
38pub struct GenericOAuthMetadata {
39 pub issuer: Option<String>,
40 pub jwks_url: Option<String>,
41 pub signing_algorithms: Vec<String>,
42 pub end_session_endpoint: Option<String>,
43}
44pub struct GenericOAuthResolved {
45 pub provider: OAuthProvider,
46 pub metadata: GenericOAuthMetadata,
47}
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum GenericOAuthError {
51 RequiredVerificationUnavailable,
52 InvalidTokenAuthentication,
53}
54impl std::fmt::Display for GenericOAuthError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str(match self {
57 Self::RequiredVerificationUnavailable => {
58 "Generic OAuth requires discovery ID-token verification metadata"
59 }
60 Self::InvalidTokenAuthentication => {
61 "Generic OAuth token authentication conflicts with configured credentials"
62 }
63 })
64 }
65}
66impl std::error::Error for GenericOAuthError {}
67impl GenericOAuthConfig {
68 #[must_use]
69 pub fn new(client_id: impl Into<String>, client_secret: impl Into<String>) -> Self {
70 Self {
71 provider: OAuthProvider {
72 client_id: client_id.into(),
73 client_secret: client_secret.into(),
74 additional_client_ids: Vec::new(),
75 hosted_domain: None,
76 require_email_verification: false,
77 auth_url: String::new(),
78 token_url: String::new(),
79 user_info_url: None,
80 scopes: Vec::new(),
81 authorization: Some(OAuthAuthorizationPolicy {
82 scope_order: OAuthScopeOrder::RequestedThenConfigured,
83 ..Default::default()
84 }),
85 authorization_params: Vec::new(),
86 account_subject: None,
87 map_user_info: None,
88 get_user_info: None,
89 refresh_access_token: None,
90 verify_id_token: None,
91 id_token: None,
92 disable_id_token_sign_in: false,
93 disable_implicit_sign_up: false,
94 disable_sign_up: false,
95 override_user_info_on_sign_in: false,
96 },
97 discovery_url: None,
98 discovery_headers: Vec::new(),
99 authorization_url: None,
100 token_url: None,
101 user_info_url: None,
102 end_session_endpoint: None,
103 post_logout_redirect_uri: None,
104 disable_provider_logout: false,
105 require_id_token_verification: false,
106 disable_id_token_nonce_binding: false,
107 map_profile: None,
108 access_token_expires_in: None,
109 account_key: None,
110 }
111 }
112 pub async fn resolve(mut self) -> Result<Option<GenericOAuthResolved>, GenericOAuthError> {
115 let mut metadata = GenericOAuthMetadata::default();
116 let mut is_oidc = false;
117 let discovered = if let Some(url) = &self.discovery_url {
118 fetch_discovery(url, &self.discovery_headers).await
119 } else {
120 None
121 };
122 if let Some(document) = &discovered {
123 self.authorization_url = self
124 .authorization_url
125 .or_else(|| string(document, "authorization_endpoint"));
126 self.token_url = self
127 .token_url
128 .or_else(|| string(document, "token_endpoint"));
129 self.user_info_url = self
130 .user_info_url
131 .or_else(|| string(document, "userinfo_endpoint"));
132 metadata.issuer = string(document, "issuer");
133 is_oidc = document
134 .get("id_token_signing_alg_values_supported")
135 .and_then(Value::as_array)
136 .is_some_and(|values| !values.is_empty());
137 metadata.signing_algorithms = document
138 .get("id_token_signing_alg_values_supported")
139 .and_then(Value::as_array)
140 .map(|values| {
141 values
142 .iter()
143 .filter_map(Value::as_str)
144 .map(str::to_owned)
145 .collect()
146 })
147 .unwrap_or_default();
148 metadata.end_session_endpoint = self
149 .end_session_endpoint
150 .or_else(|| string(document, "end_session_endpoint"));
151 if let (Some(jwks), Some(issuer)) = (string(document, "jwks_uri"), &metadata.issuer)
152 && !jwks.is_empty()
153 && !issuer.is_empty()
154 {
155 let Ok(base) = url::Url::parse(self.discovery_url.as_deref().unwrap_or_default())
156 else {
157 return Ok(None);
158 };
159 let Ok(url) = base.join(&jwks) else {
160 return Ok(None);
161 };
162 metadata.jwks_url = Some(url.to_string());
163 self.provider.verify_id_token = Some(Arc::new(DiscoveryVerifier {
164 keys: Arc::new(HttpOAuthJwksSource::new(url.to_string())),
165 issuer: issuer.clone(),
166 audience: self.provider.client_id.clone(),
167 algorithms: is_oidc.then(|| metadata.signing_algorithms.clone()),
168 }));
169 }
170 } else {
171 metadata.end_session_endpoint = self.end_session_endpoint;
172 }
173 let policy = self
174 .provider
175 .authorization
176 .get_or_insert_with(Default::default);
177 if self.account_key.is_some() {
178 policy.account_key = self.account_key;
179 }
180 policy.allow_missing_access_token = true;
181 policy.default_access_token_expires_in = self.access_token_expires_in;
182 if self.discovery_url.is_some()
183 && (self.authorization_url.as_ref().is_none_or(String::is_empty)
184 || self.token_url.as_ref().is_none_or(String::is_empty)
185 && policy.authorization_code.is_none())
186 {
187 return Ok(None);
188 }
189 if self.require_id_token_verification && metadata.jwks_url.is_none() {
190 return if self.discovery_url.is_some() {
191 Ok(None)
192 } else {
193 Err(GenericOAuthError::RequiredVerificationUnavailable)
194 };
195 }
196 use super::OAuthTokenEndpointAuth::{
197 ClientSecretBasic, ClientSecretPost, None as Public, PrivateKeyJwt,
198 };
199 if matches!(policy.token_endpoint_auth, Some(Public | PrivateKeyJwt))
200 && !self.provider.client_secret.is_empty()
201 || matches!(
202 policy.token_endpoint_auth,
203 Some(ClientSecretBasic | ClientSecretPost)
204 ) && self.provider.client_secret.is_empty()
205 {
206 return Err(GenericOAuthError::InvalidTokenAuthentication);
207 }
208 drop(
210 policy
211 .configured_scopes
212 .splice(0..0, std::mem::take(&mut self.provider.scopes)),
213 );
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 let _entry = object.entry("id").or_insert(id);
319 let verified = object.get("email_verified").cloned().unwrap_or(Value::Null);
320 let _entry = object.entry("emailVerified").or_insert(verified);
321 if let Some(image) = object.get("picture").cloned() {
322 let _entry = 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 drop(
344 object.insert(
345 "emailVerified".into(),
346 object
347 .get("email_verified")
348 .cloned()
349 .unwrap_or(Value::Bool(false)),
350 ),
351 );
352 if let Some(image) = object.get("picture").cloned() {
353 drop(object.insert("image".into(), image));
354 } else {
355 drop(object.remove("image"));
356 }
357 }
358 profile
359 };
360 let user = OAuthUserInfo {
361 additional_fields: Default::default(),
362 id: string(&raw, "id").unwrap_or_default(),
363 email: string(&raw, "email").unwrap_or_default(),
364 name: string(&raw, "name"),
365 image: string(&raw, "image"),
366 email_verified: raw
367 .get("emailVerified")
368 .is_some_and(super::remaining_profile::truthy),
369 };
370 let mut response = OAuthUserInfoResponse {
371 user_output: None,
372 user,
373 data: raw.take(),
374 };
375 if let Some(mapper) = &self.mapper {
376 let mapped = mapper
377 .map_profile(response.data.clone())
378 .await
379 .map_err(|_| {
380 format!(
381 "{}Generic OAuth mapping failed",
382 super::remaining_profile::PROFILE_EXCEPTION_PREFIX
383 )
384 })?;
385 super::apply_application_mapping(&mut response, mapped)?;
386 }
387 Ok(response)
388 }
389}
390struct DiscoveryVerifier {
391 keys: Arc<dyn OAuthJwksSource>,
392 issuer: String,
393 audience: String,
394 algorithms: Option<Vec<String>>,
395}
396#[async_trait]
397impl OAuthIdTokenVerifier for DiscoveryVerifier {
398 async fn verify_id_token(&self, token: &str, nonce: Option<&str>) -> Result<bool, String> {
399 Ok(self.verify(token, nonce).await.unwrap_or(false))
400 }
401}
402impl DiscoveryVerifier {
403 async fn verify(&self, token: &str, nonce: Option<&str>) -> Option<bool> {
404 let header = jsonwebtoken::decode_header(token).ok()?;
405 let alg_name = serde_json::to_value(header.alg).ok()?;
406 if self.algorithms.as_ref().is_some_and(|algorithms| {
407 !algorithms
408 .iter()
409 .any(|alg| Some(alg.as_str()) == alg_name.as_str())
410 }) {
411 return Some(false);
412 }
413 if matches!(
415 header.alg,
416 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
417 ) {
418 return Some(false);
419 }
420 let raw_header: Value = serde_json::from_slice(
421 &base64::engine::general_purpose::URL_SAFE_NO_PAD
422 .decode(token.split('.').next()?)
423 .ok()?,
424 )
425 .ok()?;
426 if let Some(critical) = raw_header.get("crit") {
427 let names = critical.as_array()?;
428 if names.is_empty()
429 || names.iter().any(|name| name.as_str() != Some("b64"))
430 || raw_header.get("b64").and_then(Value::as_bool) != Some(true)
431 {
432 return Some(false);
433 }
434 }
435 let keys = self.keys.fetch_keys().await.ok()?;
436 let matching: Vec<_> = keys
437 .iter()
438 .filter(|key| {
439 header
440 .kid
441 .as_ref()
442 .is_none_or(|kid| key.get("kid").and_then(Value::as_str) == Some(kid))
443 && key.get("d").is_none()
444 && key.get("alg").is_none_or(|alg| alg == &alg_name)
445 && key
446 .get("use")
447 .is_none_or(|usage| usage.as_str() == Some("sig"))
448 && key.get("key_ops").is_none_or(|ops| {
449 ops.as_array().is_some_and(|ops| {
450 ops.iter().any(|op| op.as_str() == Some("verify"))
451 && ops.iter().enumerate().all(|(index, op)| {
452 op.is_string()
453 && ops.iter().take(index).all(|previous| previous != op)
454 })
455 })
456 })
457 && match header.alg {
458 Algorithm::RS256
459 | Algorithm::RS384
460 | Algorithm::RS512
461 | Algorithm::PS256
462 | Algorithm::PS384
463 | Algorithm::PS512 => key.get("kty").and_then(Value::as_str) == Some("RSA"),
464 Algorithm::ES256 | Algorithm::ES384 => {
465 key.get("kty").and_then(Value::as_str) == Some("EC")
466 }
467 Algorithm::EdDSA => key.get("kty").and_then(Value::as_str) == Some("OKP"),
468 _ => false,
469 }
470 })
471 .collect();
472 if matching.len() != 1 {
473 return Some(false);
474 }
475 if matching.first()?.get("kty").and_then(Value::as_str) == Some("RSA")
476 && !super::super::id_token::remote_rsa_public_key(matching.first()?)
477 {
478 return Some(false);
479 }
480 let jwk =
481 serde_json::from_value::<jsonwebtoken::jwk::Jwk>((*matching.first()?).clone()).ok()?;
482 let key = DecodingKey::from_jwk(&jwk).ok()?;
483 let (signed, signature) = token.rsplit_once('.')?;
484 if !jsonwebtoken::crypto::verify(signature, signed.as_bytes(), &key, header.alg).ok()? {
485 return Some(false);
486 }
487 let claims: Value = serde_json::from_slice(
488 &base64::engine::general_purpose::URL_SAFE_NO_PAD
489 .decode(token.split('.').nth(1)?)
490 .ok()?,
491 )
492 .ok()?;
493 if claims.get("iss").and_then(Value::as_str) != Some(self.issuer.as_str()) {
494 return Some(false);
495 }
496 let audience = claims.get("aud")?;
497 if audience.as_str() != Some(self.audience.as_str())
498 && !audience.as_array().is_some_and(|values| {
499 values
500 .iter()
501 .any(|value| value.as_str() == Some(self.audience.as_str()))
502 })
503 {
504 return Some(false);
505 }
506 if claims.get("iat").is_some_and(|iat| !iat.is_number()) {
507 return Some(false);
508 }
509 let now = std::time::SystemTime::now()
510 .duration_since(std::time::UNIX_EPOCH)
511 .ok()?
512 .as_secs_f64();
513 if let Some(exp) = claims.get("exp")
514 && exp.as_f64()? <= now
515 {
516 return Some(false);
517 }
518 if let Some(nbf) = claims.get("nbf")
519 && nbf.as_f64()? > now
520 {
521 return Some(false);
522 }
523 Some(
524 nonce
525 .filter(|nonce| !nonce.is_empty())
526 .is_none_or(|nonce| claims.get("nonce").and_then(Value::as_str) == Some(nonce)),
527 )
528 }
529}