oauth_toolkit/
oidc_validator.rs1use parking_lot::RwLock;
4use serde::de::DeserializeOwned;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::oidc::OidcError;
9
10#[derive(Debug, Clone)]
12pub struct OidcValidatorConfig {
13 pub issuer: String,
15 pub audience: String,
17 pub jwks_uri: Option<String>,
19}
20
21struct JwksCache {
22 keys: HashMap<String, jsonwebtoken::DecodingKey>,
23 fetched_at: std::time::Instant,
24}
25
26pub struct OidcValidator {
31 config: Arc<OidcValidatorConfig>,
32 jwks: Arc<RwLock<JwksCache>>,
33 http_client: reqwest::Client,
34}
35
36impl OidcValidator {
37 pub fn new(config: OidcValidatorConfig) -> Self {
39 Self {
40 config: Arc::new(config),
41 jwks: Arc::new(RwLock::new(JwksCache {
42 keys: HashMap::new(),
43 fetched_at: std::time::Instant::now() - std::time::Duration::from_secs(86400),
44 })),
45 http_client: reqwest::Client::new(),
46 }
47 }
48
49 pub fn config(&self) -> &OidcValidatorConfig {
51 &self.config
52 }
53
54 pub async fn validate_token<T: DeserializeOwned>(&self, token: &str) -> Result<T, OidcError> {
56 self.maybe_refresh_jwks().await?;
57
58 let cache = self.jwks.read();
59 for key in cache.keys.values() {
60 let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
61 validation.set_audience(&[&self.config.audience]);
62 validation.set_issuer(&[&self.config.issuer]);
63
64 if let Ok(data) = jsonwebtoken::decode::<T>(token, key, &validation) {
65 return Ok(data.claims);
66 }
67 }
68
69 Err(OidcError::TokenExchangeFailed(
70 "no matching key found".into(),
71 ))
72 }
73
74 pub async fn refresh_jwks(&self) -> Result<(), OidcError> {
76 let jwks_uri = match &self.config.jwks_uri {
77 Some(uri) => uri.clone(),
78 None => {
79 let discovery =
80 crate::oidc::fetch_discovery(&self.http_client, &self.config.issuer).await?;
81 discovery.jwks_uri
82 }
83 };
84
85 let resp = self
86 .http_client
87 .get(&jwks_uri)
88 .send()
89 .await
90 .map_err(|e| OidcError::DiscoveryFailed(e.to_string()))?;
91
92 let jwks_value: serde_json::Value = resp
93 .json()
94 .await
95 .map_err(|e| OidcError::DiscoveryFailed(e.to_string()))?;
96
97 let mut keys = HashMap::new();
98 if let Some(key_array) = jwks_value["keys"].as_array() {
99 for key_obj in key_array {
100 if let (Some(kty), Some(kid)) = (key_obj["kty"].as_str(), key_obj["kid"].as_str()) {
101 if kty == "RSA" {
102 if let (Some(n), Some(e)) = (key_obj["n"].as_str(), key_obj["e"].as_str()) {
103 let decoding_key =
104 jsonwebtoken::DecodingKey::from_rsa_components(n, e).ok();
105 if let Some(key) = decoding_key {
106 keys.insert(kid.to_string(), key);
107 }
108 }
109 }
110 }
111 }
112 }
113
114 let mut cache = self.jwks.write();
115 cache.keys = keys;
116 cache.fetched_at = std::time::Instant::now();
117
118 Ok(())
119 }
120
121 async fn maybe_refresh_jwks(&self) -> Result<(), OidcError> {
122 let needs_refresh = {
123 let cache = self.jwks.read();
124 cache.fetched_at.elapsed() > std::time::Duration::from_secs(86400)
125 };
126 if needs_refresh {
127 self.refresh_jwks().await?;
128 }
129 Ok(())
130 }
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used, clippy::expect_used)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn validator_config_creation() {
141 let config = OidcValidatorConfig {
142 issuer: "https://accounts.google.com".into(),
143 audience: "my-app".into(),
144 jwks_uri: None,
145 };
146 let _validator = OidcValidator::new(config);
147 }
148}