1use async_trait::async_trait;
14use jsonwebtoken::{decode, decode_header, jwk::JwkSet, Algorithm, DecodingKey, Validation};
15use reqwest::Client;
16use std::{sync::Arc, time::Duration};
17use serde::Deserialize;
18use tokio::sync::RwLock;
19use globset::{Glob, GlobSet, GlobSetBuilder};
20use jsonwebtoken::jwk::{AlgorithmParameters, Jwk, OctetKeyParameters};
21use crate::{core::{ProxyError, ProxyRequest}, security::{SecurityProvider, SecurityStage}, ProxyResponse};
22
23pub const CLAIMS_ATTRIBUTE: &str = "oidc-claims";
24const BEARER: &str = "bearer ";
25const JWKS_REFRESH: Duration = Duration::from_secs(30 * 60);
26
27#[derive(Debug, Clone, serde::Deserialize)]
28pub struct RouteRuleConfig {
29 pub methods: Vec<String>,
30 pub path: String,
31}
32
33#[derive(Debug)]
34struct RouteRule {
35 methods: Vec<String>,
36 paths: GlobSet,
37}
38
39impl RouteRule {
40 fn matches(&self, method: &str, path: &str) -> bool {
41 (self.methods.iter().any(|m| m == "*" || m == method))
42 && self.paths.is_match(path)
43 }
44}
45
46#[derive(Debug, Clone, Deserialize)]
48pub struct OidcConfig {
49 #[serde(rename = "issuer-uri")]
50 pub issuer_uri: String,
51
52 #[serde(default)]
53 pub aud: Option<String>,
54
55 #[serde(default, rename = "shared-secret")]
57 pub shared_secret: Option<String>,
58
59 #[serde(default, rename = "bypass-routes")]
60 pub bypass: Vec<RouteRuleConfig>,
61}
62
63fn jwk_to_decoding_key(jwk: &Jwk) -> Result<DecodingKey, ProxyError> {
65 match &jwk.algorithm {
66 AlgorithmParameters::RSA(rsa) => {
67 Ok(DecodingKey::from_rsa_components(&rsa.n, &rsa.e)?)
68 }
69 AlgorithmParameters::EllipticCurve(ec) => {
70 Ok(DecodingKey::from_ec_components(&ec.x, &ec.y)?)
71 }
72 AlgorithmParameters::OctetKey(OctetKeyParameters { value, .. }) => {
73 Ok(DecodingKey::from_ed_components(value)?)
74 }
75 _ => Err(ProxyError::SecurityError("unsupported key type".into())),
76 }
77}
78
79#[derive(Debug)]
80pub struct OidcProvider {
81 issuer: String,
82 aud: Option<String>,
83 shared_secret: Option<String>,
84
85 jwks_uri: String,
86 jwks: Arc<RwLock<Option<JwkSet>>>,
87 last_refresh: Arc<RwLock<tokio::time::Instant>>,
88 http: Client,
89
90 rules: Vec<RouteRule>,
91}
92
93impl OidcProvider {
94 pub async fn discover(cfg: OidcConfig) -> Result<Self, ProxyError> {
97 let client = Client::builder()
99 .user_agent("foxy/oidc")
100 .build()?;
101 #[derive(Deserialize)]
102 struct Discovery { jwks_uri: String }
103 let meta: Discovery = client
104 .get(&cfg.issuer_uri)
105 .send()
106 .await?
107 .error_for_status()?
108 .json()
109 .await?;
110
111 let mut rules = Vec::with_capacity(cfg.bypass.len());
113 for raw in cfg.bypass {
114 let mut builder = GlobSetBuilder::new();
115 builder.add(Glob::new(&raw.path)?);
116 rules.push(RouteRule {
117 methods: raw.methods.iter().map(|m| m.to_ascii_uppercase()).collect(),
118 paths: builder.build()?,
119 });
120 }
121
122 Ok(Self {
123 issuer: cfg
124 .issuer_uri
125 .trim_end_matches("/.well-known/openid-configuration")
126 .to_owned(),
127 aud: cfg.aud,
128 shared_secret: cfg.shared_secret,
129 jwks_uri: meta.jwks_uri,
130 jwks: Arc::new(RwLock::new(None)),
131 last_refresh: Arc::new(RwLock::new(
132 tokio::time::Instant::now() - JWKS_REFRESH,
133 )),
134 http: client,
135 rules,
136 })
137 }
138
139 async fn refresh_jwks(&self) -> Result<(), ProxyError> {
142 let now = tokio::time::Instant::now();
143 if now.duration_since(*self.last_refresh.read().await) < JWKS_REFRESH {
144 return Ok(());
145 }
146 let set = self
147 .http
148 .get(&self.jwks_uri)
149 .send()
150 .await?
151 .error_for_status()?
152 .json::<JwkSet>()
153 .await?;
154 *self.jwks.write().await = Some(set);
155 *self.last_refresh.write().await = now;
156 Ok(())
157 }
158
159 fn validate_std_claims(&self, claims: &serde_json::Value) -> Result<(), ProxyError> {
160 if claims["iss"] != self.issuer {
161 return Err(ProxyError::SecurityError("bad issuer".into()));
162 }
163 if let Some(ref aud) = self.aud {
164 let ok = claims["aud"]
165 .as_str()
166 .map(|a| a == aud)
167 .unwrap_or(false);
168 if !ok {
169 return Err(ProxyError::SecurityError("bad audience".into()));
170 }
171 }
172 Ok(())
173 }
174
175 #[inline]
176 fn is_bypassed(&self, method: &str, path: &str) -> bool {
177 self.rules.iter().any(|r| r.matches(method, path))
178 }
179}
180
181#[async_trait]
182impl SecurityProvider for OidcProvider {
183 fn name(&self) -> &str { "OidcProvider" }
184
185 fn stage(&self) -> SecurityStage { SecurityStage::Pre }
186
187 async fn pre(&self, mut req: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
188 if self.is_bypassed(&req.method.to_string(), &req.path) {
190 return Ok(req);
191 }
192
193 let auth = req
195 .headers
196 .get("authorization")
197 .and_then(|v| v.to_str().ok())
198 .ok_or_else(|| ProxyError::SecurityError("missing Authorization".into()))?
199 .to_ascii_lowercase();
200 if !auth.starts_with(BEARER) {
201 return Err(ProxyError::SecurityError("unsupported auth scheme".into()));
202 }
203 let token = auth.trim_start_matches(BEARER).trim();
204
205 let header = decode_header(token)?;
207 let allowed_algs = [
208 Algorithm::RS256,
209 Algorithm::RS384,
210 Algorithm::RS512,
211 Algorithm::PS256,
212 Algorithm::PS384,
213 Algorithm::PS512,
214 Algorithm::ES256,
215 Algorithm::ES384,
216 Algorithm::EdDSA,
217 Algorithm::HS256,
218 Algorithm::HS384,
219 Algorithm::HS512,
220 ];
221 if !allowed_algs.contains(&header.alg) {
222 return Err(ProxyError::SecurityError("alg not allowed".into()));
223 }
224
225 let decoding_key = match header.alg {
227 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
228 let secret = self.shared_secret.as_deref().ok_or_else(|| {
229 ProxyError::SecurityError("shared-secret not configured".into())
230 })?;
231 DecodingKey::from_secret(secret.as_bytes())
232 }
233 _ => {
234 self.refresh_jwks().await?;
235 let kid = header
236 .kid
237 .ok_or_else(|| ProxyError::SecurityError("missing kid".into()))?;
238
239 let jwks_guard = self.jwks.read().await;
240 let set = jwks_guard
241 .as_ref()
242 .ok_or_else(|| ProxyError::SecurityError("no JWKS cache".into()))?;
243
244 let jwk = set
245 .find(&kid)
246 .ok_or_else(|| ProxyError::SecurityError("unknown kid".into()))?;
247
248 jwk_to_decoding_key(jwk)?
249 }
250 };
251
252 let mut validation = Validation::new(header.alg);
254 validation.set_required_spec_claims(&["exp", "iss"]);
255
256 let data = decode::<serde_json::Value>(token, &decoding_key, &validation)?;
257 self.validate_std_claims(&data.claims)?;
258
259 {
261 let mut ctx = req.context.write().await;
262 ctx.attributes.insert(CLAIMS_ATTRIBUTE.into(), data.claims);
263 }
264
265 Ok(req)
266 }
267}