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