1use crate::errors::{AuthError, Result};
7use crate::saml_assertions::SamlAssertionBuilder;
8use crate::ws_security::{PasswordType, WsSecurityClient, WsSecurityConfig};
10use base64::{Engine as _, engine::general_purpose::STANDARD};
11use chrono::{DateTime, Duration, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15pub struct SecurityTokenService {
17 config: StsConfig,
19
20 ws_security: WsSecurityClient,
22
23 issued_tokens: HashMap<String, IssuedToken>,
25}
26
27#[derive(Debug, Clone)]
29pub struct StsConfig {
30 pub issuer: String,
32
33 pub default_token_lifetime: Duration,
35
36 pub max_token_lifetime: Duration,
38
39 pub supported_token_types: Vec<String>,
41
42 pub endpoint_url: String,
44
45 pub include_proof_tokens: bool,
47
48 pub trust_relationships: Vec<TrustRelationship>,
50}
51
52#[derive(Debug, Clone)]
54pub struct TrustRelationship {
55 pub rp_identifier: String,
57
58 pub certificate: Option<Vec<u8>>,
60
61 pub allowed_token_types: Vec<String>,
63
64 pub max_token_lifetime: Option<Duration>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct IssuedToken {
71 pub token_id: String,
73
74 pub token_type: String,
76
77 pub token_content: String,
79
80 pub issued_at: DateTime<Utc>,
82
83 pub expires_at: DateTime<Utc>,
85
86 pub subject: String,
88
89 pub audience: String,
91
92 pub proof_token: Option<ProofToken>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ProofToken {
99 pub token_type: String,
101
102 pub key_material: Vec<u8>,
104
105 pub key_identifier: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct RequestSecurityToken {
112 pub request_type: String,
114
115 pub token_type: String,
117
118 pub applies_to: Option<String>,
120
121 pub lifetime: Option<TokenLifetime>,
123
124 pub key_type: Option<String>,
126
127 pub key_size: Option<u32>,
129
130 pub existing_token: Option<String>,
132
133 pub auth_context: Option<AuthenticationContext>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TokenLifetime {
140 pub created: DateTime<Utc>,
142
143 pub expires: DateTime<Utc>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct AuthenticationContext {
150 pub username: String,
152
153 pub auth_method: String,
155
156 pub claims: HashMap<String, String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RequestSecurityTokenResponse {
163 pub request_type: String,
165
166 pub token_type: String,
168
169 pub lifetime: TokenLifetime,
171
172 pub applies_to: Option<String>,
174
175 pub requested_security_token: String,
177
178 pub requested_proof_token: Option<ProofToken>,
180
181 pub requested_attached_reference: Option<String>,
183
184 pub requested_unattached_reference: Option<String>,
186}
187
188impl SecurityTokenService {
189 pub fn new(config: StsConfig) -> Self {
191 let ws_security_config = WsSecurityConfig::default();
192 let ws_security = WsSecurityClient::new(ws_security_config);
193
194 Self {
195 config,
196 ws_security,
197 issued_tokens: HashMap::new(),
198 }
199 }
200
201 pub fn process_request(
203 &mut self,
204 request: RequestSecurityToken,
205 ) -> Result<RequestSecurityTokenResponse> {
206 match request.request_type.as_str() {
207 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue" => self.issue_token(request),
208 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Renew" => self.renew_token(request),
209 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Cancel" => self.cancel_token(request),
210 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Validate" => {
211 self.validate_token(request)
212 }
213 _ => Err(AuthError::auth_method(
214 "wstrust",
215 "Unsupported request type",
216 )),
217 }
218 }
219
220 fn issue_token(
222 &mut self,
223 request: RequestSecurityToken,
224 ) -> Result<RequestSecurityTokenResponse> {
225 let auth_context = request
227 .auth_context
228 .as_ref()
229 .ok_or_else(|| AuthError::auth_method("wstrust", "Authentication context required"))?;
230
231 let now = Utc::now();
233 let lifetime = if let Some(ref requested_lifetime) = request.lifetime {
234 let max_expires = now + self.config.max_token_lifetime;
236 let expires = if requested_lifetime.expires > max_expires {
237 max_expires
238 } else {
239 requested_lifetime.expires
240 };
241
242 TokenLifetime {
243 created: now,
244 expires,
245 }
246 } else {
247 TokenLifetime {
248 created: now,
249 expires: now + self.config.default_token_lifetime,
250 }
251 };
252
253 let token_content = match request.token_type.as_str() {
255 "urn:oasis:names:tc:SAML:2.0:assertion" => {
256 self.issue_saml_token(auth_context, &request, &lifetime)?
257 }
258 "urn:ietf:params:oauth:token-type:jwt" => {
259 self.issue_jwt_token(auth_context, &request, &lifetime)?
260 }
261 _ => {
262 return Err(AuthError::auth_method("wstrust", "Unsupported token type"));
263 }
264 };
265
266 let proof_token = if self.config.include_proof_tokens
268 && request.key_type.as_deref()
269 == Some("http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey")
270 {
271 Some(self.generate_proof_token()?)
272 } else {
273 None
274 };
275
276 let token_id = format!("token-{}", uuid::Uuid::new_v4());
278 let issued_token = IssuedToken {
279 token_id: token_id.clone(),
280 token_type: request.token_type.clone(),
281 token_content: token_content.clone(),
282 issued_at: lifetime.created,
283 expires_at: lifetime.expires,
284 subject: auth_context.username.clone(),
285 audience: request.applies_to.clone().unwrap_or_default(),
286 proof_token: proof_token.clone(),
287 };
288
289 self.issued_tokens.insert(token_id.clone(), issued_token);
290
291 Ok(RequestSecurityTokenResponse {
292 request_type: request.request_type,
293 token_type: request.token_type,
294 lifetime,
295 applies_to: request.applies_to,
296 requested_security_token: token_content,
297 requested_proof_token: proof_token,
298 requested_attached_reference: Some(format!("#{}", token_id)),
299 requested_unattached_reference: Some(token_id),
300 })
301 }
302
303 fn issue_saml_token(
305 &self,
306 auth_context: &AuthenticationContext,
307 request: &RequestSecurityToken,
308 lifetime: &TokenLifetime,
309 ) -> Result<String> {
310 let mut assertion_builder = SamlAssertionBuilder::new(&self.config.issuer)
311 .with_validity_period(lifetime.created, lifetime.expires)
312 .with_attribute("username", &auth_context.username)
313 .with_attribute("auth_method", &auth_context.auth_method);
314
315 if let Some(ref audience) = request.applies_to {
317 assertion_builder = assertion_builder.with_audience(audience);
318 }
319
320 for (key, value) in &auth_context.claims {
322 assertion_builder = assertion_builder.with_attribute(key, value);
323 }
324
325 let assertion = assertion_builder.build();
326 assertion.to_xml()
327 }
328
329 fn issue_jwt_token(
331 &self,
332 auth_context: &AuthenticationContext,
333 request: &RequestSecurityToken,
334 lifetime: &TokenLifetime,
335 ) -> Result<String> {
336 let header = r#"{"alg":"HS256","typ":"JWT"}"#;
338 let payload = format!(
339 r#"{{"iss":"{}","sub":"{}","aud":"{}","iat":{},"exp":{},"auth_method":"{}"}}"#,
340 self.config.issuer,
341 auth_context.username,
342 request.applies_to.as_deref().unwrap_or(""),
343 lifetime.created.timestamp(),
344 lifetime.expires.timestamp(),
345 auth_context.auth_method
346 );
347
348 let header_b64 = STANDARD.encode(header);
349 let payload_b64 = STANDARD.encode(payload);
350 let signature_b64 = STANDARD.encode("dummy_signature"); Ok(format!("{}.{}.{}", header_b64, payload_b64, signature_b64))
353 }
354
355 fn generate_proof_token(&self) -> Result<ProofToken> {
357 use rand::RngCore;
358 let mut rng = rand::rng();
359 let mut key_material = vec![0u8; 32]; rng.fill_bytes(&mut key_material);
361
362 Ok(ProofToken {
363 token_type: "SymmetricKey".to_string(),
364 key_material,
365 key_identifier: format!("key-{}", uuid::Uuid::new_v4()),
366 })
367 }
368
369 fn renew_token(
371 &mut self,
372 request: RequestSecurityToken,
373 ) -> Result<RequestSecurityTokenResponse> {
374 let existing_token = request.existing_token.ok_or_else(|| {
375 AuthError::auth_method("wstrust", "Existing token required for renewal")
376 })?;
377
378 let token_id = existing_token; let issued_token = self
381 .issued_tokens
382 .get(&token_id)
383 .ok_or_else(|| AuthError::auth_method("wstrust", "Token not found"))?;
384
385 let now = Utc::now();
387 if now >= issued_token.expires_at {
388 return Err(AuthError::auth_method("wstrust", "Token has expired"));
389 }
390
391 let new_lifetime = TokenLifetime {
393 created: now,
394 expires: now + self.config.default_token_lifetime,
395 };
396
397 let auth_context = AuthenticationContext {
399 username: issued_token.subject.clone(),
400 auth_method: "token_renewal".to_string(),
401 claims: HashMap::new(),
402 };
403
404 let new_request = RequestSecurityToken {
405 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
406 token_type: issued_token.token_type.clone(),
407 applies_to: Some(issued_token.audience.clone()),
408 lifetime: Some(new_lifetime.clone()),
409 key_type: None,
410 key_size: None,
411 existing_token: None,
412 auth_context: Some(auth_context),
413 };
414
415 self.issue_token(new_request)
416 }
417
418 fn cancel_token(
420 &mut self,
421 request: RequestSecurityToken,
422 ) -> Result<RequestSecurityTokenResponse> {
423 let existing_token = request
424 .existing_token
425 .ok_or_else(|| AuthError::auth_method("wstrust", "Token required for cancellation"))?;
426
427 self.issued_tokens.remove(&existing_token);
429
430 Ok(RequestSecurityTokenResponse {
431 request_type: request.request_type,
432 token_type: "Cancelled".to_string(),
433 lifetime: TokenLifetime {
434 created: Utc::now(),
435 expires: Utc::now(),
436 },
437 applies_to: None,
438 requested_security_token: "Token cancelled".to_string(),
439 requested_proof_token: None,
440 requested_attached_reference: None,
441 requested_unattached_reference: None,
442 })
443 }
444
445 fn validate_token(
447 &self,
448 request: RequestSecurityToken,
449 ) -> Result<RequestSecurityTokenResponse> {
450 let existing_token = request
451 .existing_token
452 .ok_or_else(|| AuthError::auth_method("wstrust", "Token required for validation"))?;
453
454 let token_id = existing_token;
456 let issued_token = self
457 .issued_tokens
458 .get(&token_id)
459 .ok_or_else(|| AuthError::auth_method("wstrust", "Token not found"))?;
460
461 let now = Utc::now();
462 let is_valid = now < issued_token.expires_at;
463
464 let status = if is_valid { "Valid" } else { "Invalid" };
465
466 Ok(RequestSecurityTokenResponse {
467 request_type: request.request_type,
468 token_type: "ValidationResponse".to_string(),
469 lifetime: TokenLifetime {
470 created: issued_token.issued_at,
471 expires: issued_token.expires_at,
472 },
473 applies_to: Some(issued_token.audience.clone()),
474 requested_security_token: status.to_string(),
475 requested_proof_token: None,
476 requested_attached_reference: None,
477 requested_unattached_reference: None,
478 })
479 }
480
481 pub fn create_rst_soap_request(&self, request: &RequestSecurityToken) -> Result<String> {
483 let header = self.ws_security.create_username_token_header(
484 "client_user",
485 Some("client_password"),
486 PasswordType::PasswordText,
487 )?;
488
489 let security_header = self.ws_security.header_to_xml(&header)?;
490
491 let soap_request = format!(
492 r#"<?xml version="1.0" encoding="UTF-8"?>
493<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
494 xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512"
495 xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
496 <soap:Header>
497 {}
498 </soap:Header>
499 <soap:Body>
500 <wst:RequestSecurityToken>
501 <wst:RequestType>{}</wst:RequestType>
502 <wst:TokenType>{}</wst:TokenType>
503 {}
504 {}
505 {}
506 </wst:RequestSecurityToken>
507 </soap:Body>
508</soap:Envelope>"#,
509 security_header,
510 request.request_type,
511 request.token_type,
512 request.applies_to.as_ref().map(|a| format!("<wsp:AppliesTo><wsp:EndpointReference><wsp:Address>{}</wsp:Address></wsp:EndpointReference></wsp:AppliesTo>", a)).unwrap_or_default(),
513 request.lifetime.as_ref().map(|l| format!("<wst:Lifetime><wsu:Created>{}</wsu:Created><wsu:Expires>{}</wsu:Expires></wst:Lifetime>",
514 l.created.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
515 l.expires.format("%Y-%m-%dT%H:%M:%S%.3fZ"))).unwrap_or_default(),
516 request.key_type.as_ref().map(|k| format!("<wst:KeyType>{}</wst:KeyType>", k)).unwrap_or_default()
517 );
518
519 Ok(soap_request)
520 }
521}
522
523impl Default for StsConfig {
524 fn default() -> Self {
525 Self {
526 issuer: "https://sts.example.com".to_string(),
527 default_token_lifetime: Duration::hours(1),
528 max_token_lifetime: Duration::hours(8),
529 supported_token_types: vec![
530 "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
531 "urn:ietf:params:oauth:token-type:jwt".to_string(),
532 ],
533 endpoint_url: "https://sts.example.com/trust".to_string(),
534 include_proof_tokens: false,
535 trust_relationships: Vec::new(),
536 }
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn test_sts_issue_saml_token() {
546 let config = StsConfig::default();
547 let mut sts = SecurityTokenService::new(config);
548
549 let auth_context = AuthenticationContext {
550 username: "testuser".to_string(),
551 auth_method: "password".to_string(),
552 claims: {
553 let mut claims = HashMap::new();
554 claims.insert("role".to_string(), "admin".to_string());
555 claims
556 },
557 };
558
559 let request = RequestSecurityToken {
560 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
561 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
562 applies_to: Some("https://rp.example.com".to_string()),
563 lifetime: None,
564 key_type: None,
565 key_size: None,
566 existing_token: None,
567 auth_context: Some(auth_context),
568 };
569
570 let response = sts.process_request(request).unwrap();
571
572 assert_eq!(response.token_type, "urn:oasis:names:tc:SAML:2.0:assertion");
573 assert!(
574 response
575 .requested_security_token
576 .contains("<saml:Assertion")
577 );
578 assert!(response.requested_security_token.contains("testuser"));
579 }
580
581 #[test]
582 fn test_sts_issue_jwt_token() {
583 let config = StsConfig::default();
584 let mut sts = SecurityTokenService::new(config);
585
586 let auth_context = AuthenticationContext {
587 username: "testuser".to_string(),
588 auth_method: "certificate".to_string(),
589 claims: HashMap::new(),
590 };
591
592 let request = RequestSecurityToken {
593 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
594 token_type: "urn:ietf:params:oauth:token-type:jwt".to_string(),
595 applies_to: Some("https://api.example.com".to_string()),
596 lifetime: None,
597 key_type: None,
598 key_size: None,
599 existing_token: None,
600 auth_context: Some(auth_context),
601 };
602
603 let response = sts.process_request(request).unwrap();
604
605 assert_eq!(response.token_type, "urn:ietf:params:oauth:token-type:jwt");
606 assert!(response.requested_security_token.contains("."));
607
608 let parts: Vec<&str> = response.requested_security_token.split('.').collect();
610 assert_eq!(parts.len(), 3);
611 }
612
613 #[test]
614 fn test_sts_soap_request_generation() {
615 let config = StsConfig::default();
616 let sts = SecurityTokenService::new(config);
617
618 let request = RequestSecurityToken {
619 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
620 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
621 applies_to: Some("https://rp.example.com".to_string()),
622 lifetime: None,
623 key_type: Some("http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer".to_string()),
624 key_size: None,
625 existing_token: None,
626 auth_context: None,
627 };
628
629 let soap_request = sts.create_rst_soap_request(&request).unwrap();
630
631 assert!(soap_request.contains("<soap:Envelope"));
632 assert!(soap_request.contains("<wsse:Security"));
633 assert!(soap_request.contains("<wst:RequestSecurityToken"));
634 assert!(soap_request.contains("https://rp.example.com"));
635 assert!(soap_request.contains("</soap:Envelope>"));
636 }
637}