1pub mod jwks;
30pub mod request;
31pub mod responses;
32pub mod route_handler;
33pub mod sealed_token;
34pub mod sts;
35
36pub use jwks::JwksCache;
37use multistore::error::ProxyError;
38use multistore::registry::CredentialRegistry;
39use multistore::types::TemporaryCredentials;
40pub use request::try_parse_sts_request;
41use request::StsRequest;
42pub use responses::{build_sts_error_response, build_sts_response};
43pub use sealed_token::TokenKey;
44
45pub async fn try_handle_sts<C: CredentialRegistry>(
58 query: Option<&str>,
59 form_body: Option<&str>,
60 config: &C,
61 jwks_cache: &JwksCache,
62 token_key: Option<&TokenKey>,
63) -> Option<(u16, String)> {
64 let sts_result = try_parse_sts_request(query).or_else(|| try_parse_sts_request(form_body))?;
65 let (status, xml) = match sts_result {
66 Ok(sts_request) => {
67 let Some(key) = token_key else {
68 tracing::error!("STS request received but SESSION_TOKEN_KEY is not configured");
69 return Some(build_sts_error_response(&ProxyError::ConfigError(
70 "STS requires SESSION_TOKEN_KEY to be configured".into(),
71 )));
72 };
73 match assume_role_with_web_identity(config, &sts_request, "STSPRXY", jwks_cache, key)
74 .await
75 {
76 Ok(creds) => build_sts_response(&creds),
77 Err(e) => {
78 tracing::warn!(error = %e, "STS request failed");
79 build_sts_error_response(&e)
80 }
81 }
82 }
83 Err(e) => build_sts_error_response(&e),
84 };
85 Some((status, xml))
86}
87
88fn jwt_decode_unverified(
90 token: &str,
91) -> Result<(serde_json::Value, serde_json::Value), ProxyError> {
92 let mut parts = token.splitn(3, '.');
93 let header_b64 = parts
94 .next()
95 .ok_or_else(|| ProxyError::InvalidOidcToken("malformed JWT".into()))?;
96 let payload_b64 = parts
97 .next()
98 .ok_or_else(|| ProxyError::InvalidOidcToken("malformed JWT".into()))?;
99
100 Ok((
101 jwks::decode_jwt_segment(header_b64)?,
102 jwks::decode_jwt_segment(payload_b64)?,
103 ))
104}
105
106pub async fn assume_role_with_web_identity<C: CredentialRegistry>(
111 config: &C,
112 sts_request: &StsRequest,
113 key_prefix: &str,
114 jwks_cache: &JwksCache,
115 token_key: &TokenKey,
116) -> Result<TemporaryCredentials, ProxyError> {
117 let role = config
119 .get_role(&sts_request.role_arn)
120 .await?
121 .ok_or_else(|| ProxyError::RoleNotFound(sts_request.role_arn.to_string()))?;
122
123 let (header, insecure_claims) = jwt_decode_unverified(&sts_request.web_identity_token)?;
125
126 let issuer = insecure_claims
127 .get("iss")
128 .and_then(|v| v.as_str())
129 .ok_or_else(|| ProxyError::InvalidOidcToken("missing iss claim".into()))?;
130
131 if !role.trusted_oidc_issuers.iter().any(|i| i == issuer) {
133 return Err(ProxyError::InvalidOidcToken(format!(
134 "untrusted issuer: {}",
135 issuer
136 )));
137 }
138
139 let alg = header.get("alg").and_then(|v| v.as_str()).unwrap_or("");
141 if alg != "RS256" {
142 return Err(ProxyError::InvalidOidcToken(format!(
143 "unsupported JWT algorithm: {}",
144 alg
145 )));
146 }
147
148 let jwks = jwks_cache.get_or_fetch(issuer).await?;
150 let kid = header
151 .get("kid")
152 .and_then(|v| v.as_str())
153 .ok_or_else(|| ProxyError::InvalidOidcToken("JWT missing kid".into()))?;
154
155 let key = jwks::find_key(&jwks, kid)?;
156 let claims = jwks::verify_token(&sts_request.web_identity_token, key, issuer, &role)?;
157
158 let subject = claims.get("sub").and_then(|v| v.as_str()).unwrap_or("");
160
161 if !role.subject_conditions.is_empty() {
162 let matches = role
163 .subject_conditions
164 .iter()
165 .any(|pattern| subject_matches(subject, pattern));
166 if !matches {
167 return Err(ProxyError::InvalidOidcToken(format!(
168 "subject '{}' does not match any conditions",
169 subject
170 )));
171 }
172 }
173
174 const MIN_SESSION_DURATION_SECS: u64 = 900;
176 let duration = sts_request
177 .duration_seconds
178 .unwrap_or(3600)
179 .clamp(MIN_SESSION_DURATION_SECS, role.max_session_duration_secs);
180
181 let mut creds = sts::mint_temporary_credentials(&role, subject, duration, key_prefix, &claims);
182
183 creds.session_token = token_key.seal(&creds)?;
185
186 Ok(creds)
187}
188
189fn subject_matches(subject: &str, pattern: &str) -> bool {
192 if pattern == "*" {
193 return true;
194 }
195
196 let parts: Vec<&str> = pattern.split('*').collect();
197 if parts.len() == 1 {
198 return subject == pattern;
199 }
200
201 let mut remaining = subject;
202
203 if !parts[0].is_empty() {
205 if !remaining.starts_with(parts[0]) {
206 return false;
207 }
208 remaining = &remaining[parts[0].len()..];
209 }
210
211 for part in &parts[1..parts.len() - 1] {
213 if part.is_empty() {
214 continue;
215 }
216 match remaining.find(part) {
217 Some(idx) => remaining = &remaining[idx + part.len()..],
218 None => return false,
219 }
220 }
221
222 let last = parts.last().unwrap();
224 if !last.is_empty() {
225 return remaining.ends_with(last);
226 }
227
228 true
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_subject_matching() {
237 assert!(subject_matches(
239 "repo:org/repo:ref:refs/heads/main",
240 "repo:org/repo:*"
241 ));
242
243 assert!(subject_matches("repo:org/repo:ref:refs/heads/main", "*"));
245
246 assert!(subject_matches(
248 "repo:org/repo:ref:refs/heads/main",
249 "repo:org/repo:ref:refs/heads/main"
250 ));
251
252 assert!(!subject_matches(
254 "repo:org/repo:ref:refs/heads/main",
255 "repo:other/*"
256 ));
257
258 assert!(subject_matches(
260 "repo:org/repo:ref:refs/heads/main",
261 "repo:org/*:ref:refs/heads/*"
262 ));
263 }
264
265 #[test]
266 fn test_subject_matching_exact() {
267 assert!(subject_matches("abc", "abc"));
268 assert!(!subject_matches("abc", "abcd"));
269 assert!(!subject_matches("abcd", "abc"));
270 assert!(!subject_matches("", "abc"));
271 assert!(subject_matches("", ""));
272 }
273
274 #[test]
275 fn test_subject_matching_leading_wildcard() {
276 assert!(subject_matches("anything", "*"));
277 assert!(subject_matches("", "*"));
278 assert!(subject_matches("foo", "*foo"));
279 assert!(subject_matches("xfoo", "*foo"));
280 assert!(!subject_matches("foox", "*foo"));
281 }
282
283 #[test]
284 fn test_subject_matching_trailing_wildcard() {
285 assert!(subject_matches("foo", "foo*"));
286 assert!(subject_matches("foobar", "foo*"));
287 assert!(!subject_matches("xfoo", "foo*"));
288 }
289
290 #[test]
291 fn test_subject_matching_middle_wildcard() {
292 assert!(subject_matches("foobar", "foo*bar"));
293 assert!(subject_matches("fooXbar", "foo*bar"));
294 assert!(subject_matches("fooXYZbar", "foo*bar"));
295 assert!(!subject_matches("fooXbaz", "foo*bar"));
296 assert!(!subject_matches("xfoobar", "foo*bar"));
297 }
298
299 #[test]
300 fn test_subject_matching_multiple_wildcards() {
301 assert!(subject_matches("axbb", "a*b*b"));
303 assert!(!subject_matches("axb", "a*b*b"));
304
305 assert!(!subject_matches("abc", "a*bc*c"));
307 assert!(subject_matches("abcc", "a*bc*c"));
308
309 assert!(subject_matches("aab", "*a*ab"));
311 assert!(!subject_matches("xab", "*a*ab"));
312
313 assert!(subject_matches("xababab", "*ab*ab"));
315 assert!(!subject_matches("xab", "*ab*ab"));
316 }
317
318 #[test]
319 fn test_subject_matching_double_wildcard() {
320 assert!(subject_matches("anything", "**"));
321 assert!(subject_matches("", "**"));
322 }
323
324 #[test]
325 fn test_subject_matching_empty_subject() {
326 assert!(subject_matches("", "*"));
327 assert!(!subject_matches("", "a"));
328 assert!(subject_matches("", ""));
329 }
330}