1use crate::errors::{AuthError, Result};
7use crate::protocols::saml_assertions::SamlAssertionBuilder;
8use crate::protocols::ws_security::{PasswordType, WsSecurityClient, WsSecurityConfig};
10use base64::Engine as _;
11use chrono::{DateTime, Duration, Utc};
12use jsonwebtoken::{Algorithm, EncodingKey, Header, encode as jwt_encode};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16pub struct SecurityTokenService {
23 config: StsConfig,
25
26 ws_security: WsSecurityClient,
28
29 issued_tokens: HashMap<String, IssuedToken>,
31}
32
33#[derive(Debug, Clone)]
35pub struct StsConfig {
36 pub issuer: String,
38
39 pub default_token_lifetime: Duration,
41
42 pub max_token_lifetime: Duration,
44
45 pub supported_token_types: Vec<String>,
47
48 pub endpoint_url: String,
50
51 pub include_proof_tokens: bool,
53
54 pub trust_relationships: Vec<TrustRelationship>,
56
57 pub jwt_signing_secret: String,
65}
66
67#[derive(Debug, Clone)]
69pub struct TrustRelationship {
70 pub rp_identifier: String,
72
73 pub certificate: Option<Vec<u8>>,
75
76 pub allowed_token_types: Vec<String>,
78
79 pub max_token_lifetime: Option<Duration>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct IssuedToken {
86 pub token_id: String,
88
89 pub token_type: String,
91
92 pub token_content: String,
94
95 pub issued_at: DateTime<Utc>,
97
98 pub expires_at: DateTime<Utc>,
100
101 pub subject: String,
103
104 pub audience: String,
106
107 pub proof_token: Option<ProofToken>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ProofToken {
114 pub token_type: String,
116
117 pub key_material: Vec<u8>,
119
120 pub key_identifier: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RequestSecurityToken {
127 pub request_type: String,
129
130 pub token_type: String,
132
133 pub applies_to: Option<String>,
135
136 pub lifetime: Option<TokenLifetime>,
138
139 pub key_type: Option<String>,
141
142 pub key_size: Option<u32>,
144
145 pub existing_token: Option<String>,
147
148 pub auth_context: Option<AuthenticationContext>,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct TokenLifetime {
155 pub created: DateTime<Utc>,
157
158 pub expires: DateTime<Utc>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct AuthenticationContext {
165 pub username: String,
167
168 pub auth_method: String,
170
171 pub claims: HashMap<String, String>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct RequestSecurityTokenResponse {
178 pub request_type: String,
180
181 pub token_type: String,
183
184 pub lifetime: TokenLifetime,
186
187 pub applies_to: Option<String>,
189
190 pub requested_security_token: String,
192
193 pub requested_proof_token: Option<ProofToken>,
195
196 pub requested_attached_reference: Option<String>,
198
199 pub requested_unattached_reference: Option<String>,
201}
202
203impl SecurityTokenService {
204 pub fn new(config: StsConfig) -> Self {
206 let ws_security_config = WsSecurityConfig::default();
207 let ws_security = WsSecurityClient::new(ws_security_config);
208
209 Self {
210 config,
211 ws_security,
212 issued_tokens: HashMap::new(),
213 }
214 }
215
216 pub fn process_request(
218 &mut self,
219 request: RequestSecurityToken,
220 ) -> Result<RequestSecurityTokenResponse> {
221 match request.request_type.as_str() {
222 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue" => self.issue_token(request),
223 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Renew" => self.renew_token(request),
224 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Cancel" => self.cancel_token(request),
225 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Validate" => {
226 self.validate_token(request)
227 }
228 _ => Err(AuthError::auth_method(
229 "wstrust",
230 "Unsupported request type",
231 )),
232 }
233 }
234
235 fn issue_token(
237 &mut self,
238 request: RequestSecurityToken,
239 ) -> Result<RequestSecurityTokenResponse> {
240 let auth_context = request
242 .auth_context
243 .as_ref()
244 .ok_or_else(|| AuthError::auth_method("wstrust", "Authentication context required"))?;
245
246 let now = Utc::now();
248 let lifetime = if let Some(ref requested_lifetime) = request.lifetime {
249 let max_expires = now + self.config.max_token_lifetime;
251 let expires = if requested_lifetime.expires > max_expires {
252 max_expires
253 } else {
254 requested_lifetime.expires
255 };
256
257 TokenLifetime {
258 created: now,
259 expires,
260 }
261 } else {
262 TokenLifetime {
263 created: now,
264 expires: now + self.config.default_token_lifetime,
265 }
266 };
267
268 let token_content = match request.token_type.as_str() {
270 "urn:oasis:names:tc:SAML:2.0:assertion" => {
271 self.issue_saml_token(auth_context, &request, &lifetime)?
272 }
273 "urn:ietf:params:oauth:token-type:jwt" => {
274 self.issue_jwt_token(auth_context, &request, &lifetime)?
275 }
276 _ => {
277 return Err(AuthError::auth_method("wstrust", "Unsupported token type"));
278 }
279 };
280
281 let proof_token = if self.config.include_proof_tokens
283 && request.key_type.as_deref()
284 == Some("http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey")
285 {
286 Some(self.generate_proof_token()?)
287 } else {
288 None
289 };
290
291 let token_id = format!("token-{}", uuid::Uuid::new_v4());
293 let issued_token = IssuedToken {
294 token_id: token_id.clone(),
295 token_type: request.token_type.clone(),
296 token_content: token_content.clone(),
297 issued_at: lifetime.created,
298 expires_at: lifetime.expires,
299 subject: auth_context.username.clone(),
300 audience: request.applies_to.clone().unwrap_or_default(),
301 proof_token: proof_token.clone(),
302 };
303
304 self.issued_tokens.insert(token_id.clone(), issued_token);
305
306 Ok(RequestSecurityTokenResponse {
307 request_type: request.request_type,
308 token_type: request.token_type,
309 lifetime,
310 applies_to: request.applies_to,
311 requested_security_token: token_content,
312 requested_proof_token: proof_token,
313 requested_attached_reference: Some(format!("#{}", token_id)),
314 requested_unattached_reference: Some(token_id),
315 })
316 }
317
318 fn issue_saml_token(
320 &self,
321 auth_context: &AuthenticationContext,
322 request: &RequestSecurityToken,
323 lifetime: &TokenLifetime,
324 ) -> Result<String> {
325 let mut assertion_builder = SamlAssertionBuilder::new(&self.config.issuer)
326 .with_validity_period(lifetime.created, lifetime.expires)
327 .with_attribute("username", &auth_context.username)
328 .with_attribute("auth_method", &auth_context.auth_method);
329
330 if let Some(ref audience) = request.applies_to {
332 assertion_builder = assertion_builder.with_audience(audience);
333 }
334
335 for (key, value) in &auth_context.claims {
337 assertion_builder = assertion_builder.with_attribute(key, value);
338 }
339
340 let assertion = assertion_builder.build();
341 assertion.to_xml()
342 }
343
344 fn issue_jwt_token(
346 &self,
347 auth_context: &AuthenticationContext,
348 request: &RequestSecurityToken,
349 lifetime: &TokenLifetime,
350 ) -> Result<String> {
351 #[derive(Serialize)]
352 struct WsTrustClaims<'a> {
353 iss: &'a str,
354 sub: &'a str,
355 aud: &'a str,
356 iat: i64,
357 exp: i64,
358 auth_method: &'a str,
359 #[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
360 claims: &'a HashMap<String, String>,
361 }
362
363 let jwt_claims = WsTrustClaims {
364 iss: &self.config.issuer,
365 sub: &auth_context.username,
366 aud: request.applies_to.as_deref().unwrap_or(""),
367 iat: lifetime.created.timestamp(),
368 exp: lifetime.expires.timestamp(),
369 auth_method: &auth_context.auth_method,
370 claims: &auth_context.claims,
371 };
372
373 let encoding_key = EncodingKey::from_secret(self.config.jwt_signing_secret.as_bytes());
374
375 jwt_encode(&Header::new(Algorithm::HS256), &jwt_claims, &encoding_key)
376 .map_err(|e| AuthError::internal(format!("WS-Trust JWT signing failed: {e}")))
377 }
378
379 fn generate_proof_token(&self) -> Result<ProofToken> {
381 use rand::Rng;
382 let mut rng = rand::rng();
383 let mut key_material = vec![0u8; 32]; rng.fill_bytes(&mut key_material);
385
386 Ok(ProofToken {
387 token_type: "SymmetricKey".to_string(),
388 key_material,
389 key_identifier: format!("key-{}", uuid::Uuid::new_v4()),
390 })
391 }
392
393 fn renew_token(
395 &mut self,
396 request: RequestSecurityToken,
397 ) -> Result<RequestSecurityTokenResponse> {
398 let existing_token = request.existing_token.ok_or_else(|| {
399 AuthError::auth_method("wstrust", "Existing token required for renewal")
400 })?;
401
402 let mut renewal_claims = HashMap::new();
405 let token_id = if existing_token.matches('.').count() == 2 {
406 if let Some(payload_b64) = existing_token.split('.').nth(1) {
408 if let Ok(payload_bytes) = base64::engine::general_purpose::URL_SAFE_NO_PAD
409 .decode(payload_b64)
410 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload_b64))
411 {
412 if let Ok(claims) =
413 serde_json::from_slice::<HashMap<String, serde_json::Value>>(&payload_bytes)
414 {
415 for (k, v) in &claims {
416 if let Some(s) = v.as_str() {
417 renewal_claims.insert(k.clone(), s.to_string());
418 }
419 }
420 claims
422 .get("jti")
423 .or_else(|| claims.get("sub"))
424 .and_then(|v| v.as_str())
425 .unwrap_or(&existing_token)
426 .to_string()
427 } else {
428 existing_token.clone()
429 }
430 } else {
431 existing_token.clone()
432 }
433 } else {
434 existing_token.clone()
435 }
436 } else {
437 existing_token.clone()
438 };
439
440 let issued_token = self
441 .issued_tokens
442 .get(&token_id)
443 .ok_or_else(|| AuthError::auth_method("wstrust", "Token not found"))?;
444
445 let now = Utc::now();
447 if now >= issued_token.expires_at {
448 return Err(AuthError::auth_method("wstrust", "Token has expired"));
449 }
450
451 let new_lifetime = TokenLifetime {
453 created: now,
454 expires: now + self.config.default_token_lifetime,
455 };
456
457 let auth_context = AuthenticationContext {
459 username: issued_token.subject.clone(),
460 auth_method: "token_renewal".to_string(),
461 claims: if renewal_claims.is_empty() {
462 HashMap::new()
463 } else {
464 renewal_claims
465 },
466 };
467
468 let new_request = RequestSecurityToken {
469 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
470 token_type: issued_token.token_type.clone(),
471 applies_to: Some(issued_token.audience.clone()),
472 lifetime: Some(new_lifetime.clone()),
473 key_type: None,
474 key_size: None,
475 existing_token: None,
476 auth_context: Some(auth_context),
477 };
478
479 self.issue_token(new_request)
480 }
481
482 fn cancel_token(
484 &mut self,
485 request: RequestSecurityToken,
486 ) -> Result<RequestSecurityTokenResponse> {
487 let existing_token = request
488 .existing_token
489 .ok_or_else(|| AuthError::auth_method("wstrust", "Token required for cancellation"))?;
490
491 self.issued_tokens.remove(&existing_token);
493
494 Ok(RequestSecurityTokenResponse {
495 request_type: request.request_type,
496 token_type: "Cancelled".to_string(),
497 lifetime: TokenLifetime {
498 created: Utc::now(),
499 expires: Utc::now(),
500 },
501 applies_to: None,
502 requested_security_token: "Token cancelled".to_string(),
503 requested_proof_token: None,
504 requested_attached_reference: None,
505 requested_unattached_reference: None,
506 })
507 }
508
509 fn validate_token(
511 &self,
512 request: RequestSecurityToken,
513 ) -> Result<RequestSecurityTokenResponse> {
514 let existing_token = request
515 .existing_token
516 .ok_or_else(|| AuthError::auth_method("wstrust", "Token required for validation"))?;
517
518 let token_id = existing_token;
520 let issued_token = self
521 .issued_tokens
522 .get(&token_id)
523 .ok_or_else(|| AuthError::auth_method("wstrust", "Token not found"))?;
524
525 let now = Utc::now();
526 let is_valid = now < issued_token.expires_at;
527
528 let status = if is_valid { "Valid" } else { "Invalid" };
529
530 Ok(RequestSecurityTokenResponse {
531 request_type: request.request_type,
532 token_type: "ValidationResponse".to_string(),
533 lifetime: TokenLifetime {
534 created: issued_token.issued_at,
535 expires: issued_token.expires_at,
536 },
537 applies_to: Some(issued_token.audience.clone()),
538 requested_security_token: status.to_string(),
539 requested_proof_token: None,
540 requested_attached_reference: None,
541 requested_unattached_reference: None,
542 })
543 }
544
545 pub fn create_rst_soap_request(
547 &self,
548 request: &RequestSecurityToken,
549 username: &str,
550 password: Option<&str>,
551 ) -> Result<String> {
552 let header = self.ws_security.create_username_token_header(
553 username,
554 password,
555 PasswordType::PasswordText,
556 )?;
557
558 let security_header = self.ws_security.header_to_xml(&header)?;
559
560 let soap_request = format!(
561 r#"<?xml version="1.0" encoding="UTF-8"?>
562<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
563 xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512"
564 xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
565 <soap:Header>
566 {}
567 </soap:Header>
568 <soap:Body>
569 <wst:RequestSecurityToken>
570 <wst:RequestType>{}</wst:RequestType>
571 <wst:TokenType>{}</wst:TokenType>
572 {}
573 {}
574 {}
575 </wst:RequestSecurityToken>
576 </soap:Body>
577</soap:Envelope>"#,
578 security_header,
579 request.request_type,
580 request.token_type,
581 request.applies_to.as_ref().map(|a| format!("<wsp:AppliesTo><wsp:EndpointReference><wsp:Address>{}</wsp:Address></wsp:EndpointReference></wsp:AppliesTo>", a)).unwrap_or_default(),
582 request.lifetime.as_ref().map(|l| format!("<wst:Lifetime><wsu:Created>{}</wsu:Created><wsu:Expires>{}</wsu:Expires></wst:Lifetime>",
583 l.created.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
584 l.expires.format("%Y-%m-%dT%H:%M:%S%.3fZ"))).unwrap_or_default(),
585 request.key_type.as_ref().map(|k| format!("<wst:KeyType>{}</wst:KeyType>", k)).unwrap_or_default()
586 );
587
588 Ok(soap_request)
589 }
590}
591
592impl Default for StsConfig {
593 fn default() -> Self {
594 use ring::rand::{SecureRandom, SystemRandom};
595 let rng = SystemRandom::new();
598 let mut bytes = [0u8; 32];
599 rng.fill(&mut bytes)
600 .expect("AuthFramework fatal: system CSPRNG unavailable — the operating system cannot provide cryptographic randomness");
601 let jwt_signing_secret = bytes.iter().fold(String::with_capacity(64), |mut s, b| {
602 s.push_str(&format!("{b:02x}"));
603 s
604 });
605
606 Self {
607 issuer: "https://sts.example.com".to_string(),
608 default_token_lifetime: Duration::hours(1),
609 max_token_lifetime: Duration::hours(8),
610 supported_token_types: vec![
611 "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
612 "urn:ietf:params:oauth:token-type:jwt".to_string(),
613 ],
614 endpoint_url: "https://sts.example.com/trust".to_string(),
615 include_proof_tokens: false,
616 trust_relationships: Vec::new(),
617 jwt_signing_secret,
618 }
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625
626 #[test]
627 fn test_sts_issue_saml_token() {
628 let config = StsConfig::default();
629 let mut sts = SecurityTokenService::new(config);
630
631 let auth_context = AuthenticationContext {
632 username: "testuser".to_string(),
633 auth_method: "password".to_string(),
634 claims: {
635 let mut claims = HashMap::new();
636 claims.insert("role".to_string(), "admin".to_string());
637 claims
638 },
639 };
640
641 let request = RequestSecurityToken {
642 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
643 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
644 applies_to: Some("https://rp.example.com".to_string()),
645 lifetime: None,
646 key_type: None,
647 key_size: None,
648 existing_token: None,
649 auth_context: Some(auth_context),
650 };
651
652 let response = sts.process_request(request).unwrap();
653
654 assert_eq!(response.token_type, "urn:oasis:names:tc:SAML:2.0:assertion");
655 assert!(
656 response
657 .requested_security_token
658 .contains("<saml:Assertion")
659 );
660 assert!(response.requested_security_token.contains("testuser"));
661 }
662
663 #[test]
664 fn test_sts_issue_jwt_token() {
665 use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode as jwt_decode};
666
667 let config = StsConfig::default();
668 let signing_secret = config.jwt_signing_secret.clone();
669 let mut sts = SecurityTokenService::new(config);
670
671 let auth_context = AuthenticationContext {
672 username: "testuser".to_string(),
673 auth_method: "certificate".to_string(),
674 claims: HashMap::new(),
675 };
676
677 let request = RequestSecurityToken {
678 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
679 token_type: "urn:ietf:params:oauth:token-type:jwt".to_string(),
680 applies_to: Some("https://api.example.com".to_string()),
681 lifetime: None,
682 key_type: None,
683 key_size: None,
684 existing_token: None,
685 auth_context: Some(auth_context),
686 };
687
688 let response = sts.process_request(request).unwrap();
689
690 assert_eq!(response.token_type, "urn:ietf:params:oauth:token-type:jwt");
691
692 let parts: Vec<&str> = response.requested_security_token.split('.').collect();
694 assert_eq!(parts.len(), 3, "JWT must have header.payload.signature");
695
696 let decoding_key = DecodingKey::from_secret(signing_secret.as_bytes());
698 let mut validation = Validation::new(Algorithm::HS256);
699 validation.set_audience(&["https://api.example.com"]);
700 let token_data = jwt_decode::<serde_json::Value>(
701 &response.requested_security_token,
702 &decoding_key,
703 &validation,
704 )
705 .expect("Issued WS-Trust JWT must be verifiable with the config signing secret");
706 assert_eq!(token_data.claims["sub"], "testuser");
707 assert_eq!(token_data.claims["auth_method"], "certificate");
708 }
709
710 #[test]
711 fn test_sts_soap_request_generation() {
712 let config = StsConfig::default();
713 let sts = SecurityTokenService::new(config);
714
715 let request = RequestSecurityToken {
716 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
717 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
718 applies_to: Some("https://rp.example.com".to_string()),
719 lifetime: None,
720 key_type: Some("http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer".to_string()),
721 key_size: None,
722 existing_token: None,
723 auth_context: None,
724 };
725
726 let soap_request = sts.create_rst_soap_request(&request, "test_user", Some("test_pass")).unwrap();
727
728 assert!(soap_request.contains("<soap:Envelope"));
729 assert!(soap_request.contains("<wsse:Security"));
730 assert!(soap_request.contains("<wst:RequestSecurityToken"));
731 assert!(soap_request.contains("https://rp.example.com"));
732 assert!(soap_request.contains("</soap:Envelope>"));
733 }
734
735 #[test]
736 fn test_unsupported_request_type() {
737 let mut sts = SecurityTokenService::new(StsConfig::default());
738
739 let request = RequestSecurityToken {
740 request_type: "http://invalid/BadRequest".to_string(),
741 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
742 applies_to: None,
743 lifetime: None,
744 key_type: None,
745 key_size: None,
746 existing_token: None,
747 auth_context: None,
748 };
749
750 let err = sts.process_request(request).unwrap_err();
751 let msg = format!("{err}");
752 assert!(msg.contains("Unsupported request type"), "got: {msg}");
753 }
754
755 #[test]
756 fn test_issue_missing_auth_context() {
757 let mut sts = SecurityTokenService::new(StsConfig::default());
758
759 let request = RequestSecurityToken {
760 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
761 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
762 applies_to: None,
763 lifetime: None,
764 key_type: None,
765 key_size: None,
766 existing_token: None,
767 auth_context: None,
768 };
769
770 let err = sts.process_request(request).unwrap_err();
771 let msg = format!("{err}");
772 assert!(
773 msg.contains("Authentication context required"),
774 "got: {msg}"
775 );
776 }
777
778 #[test]
779 fn test_issue_unsupported_token_type() {
780 let mut sts = SecurityTokenService::new(StsConfig::default());
781
782 let request = RequestSecurityToken {
783 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
784 token_type: "urn:unknown:token:type".to_string(),
785 applies_to: None,
786 lifetime: None,
787 key_type: None,
788 key_size: None,
789 existing_token: None,
790 auth_context: Some(AuthenticationContext {
791 username: "user".to_string(),
792 auth_method: "password".to_string(),
793 claims: HashMap::new(),
794 }),
795 };
796
797 let err = sts.process_request(request).unwrap_err();
798 let msg = format!("{err}");
799 assert!(msg.contains("Unsupported token type"), "got: {msg}");
800 }
801
802 #[test]
803 fn test_lifetime_clamped_to_max() {
804 let mut config = StsConfig::default();
805 config.max_token_lifetime = Duration::hours(2);
806 let mut sts = SecurityTokenService::new(config);
807
808 let now = Utc::now();
809 let request = RequestSecurityToken {
810 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
811 token_type: "urn:ietf:params:oauth:token-type:jwt".to_string(),
812 applies_to: Some("https://rp.example.com".to_string()),
813 lifetime: Some(TokenLifetime {
814 created: now,
815 expires: now + Duration::hours(999), }),
817 key_type: None,
818 key_size: None,
819 existing_token: None,
820 auth_context: Some(AuthenticationContext {
821 username: "user".to_string(),
822 auth_method: "password".to_string(),
823 claims: HashMap::new(),
824 }),
825 };
826
827 let resp = sts.process_request(request).unwrap();
828 let delta = resp.lifetime.expires - now;
830 assert!(
831 delta <= Duration::hours(2) + Duration::seconds(5),
832 "lifetime should be clamped to max_token_lifetime, got {delta}"
833 );
834 }
835
836 #[test]
837 fn test_cancel_nonexistent_token() {
838 let mut sts = SecurityTokenService::new(StsConfig::default());
839
840 let request = RequestSecurityToken {
841 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Cancel".to_string(),
842 token_type: "".to_string(),
843 applies_to: None,
844 lifetime: None,
845 key_type: None,
846 key_size: None,
847 existing_token: Some("nonexistent-id".to_string()),
848 auth_context: None,
849 };
850
851 let resp = sts.process_request(request).unwrap();
853 assert_eq!(resp.requested_security_token, "Token cancelled");
854 }
855
856 #[test]
857 fn test_cancel_missing_existing_token_field() {
858 let mut sts = SecurityTokenService::new(StsConfig::default());
859
860 let request = RequestSecurityToken {
861 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Cancel".to_string(),
862 token_type: "".to_string(),
863 applies_to: None,
864 lifetime: None,
865 key_type: None,
866 key_size: None,
867 existing_token: None, auth_context: None,
869 };
870
871 let err = sts.process_request(request).unwrap_err();
872 let msg = format!("{err}");
873 assert!(
874 msg.contains("Token required for cancellation"),
875 "got: {msg}"
876 );
877 }
878
879 #[test]
880 fn test_validate_nonexistent_token() {
881 let mut sts = SecurityTokenService::new(StsConfig::default());
882
883 let request = RequestSecurityToken {
884 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Validate".to_string(),
885 token_type: "".to_string(),
886 applies_to: None,
887 lifetime: None,
888 key_type: None,
889 key_size: None,
890 existing_token: Some("does-not-exist".to_string()),
891 auth_context: None,
892 };
893
894 let err = sts.process_request(request).unwrap_err();
895 let msg = format!("{err}");
896 assert!(msg.contains("Token not found"), "got: {msg}");
897 }
898
899 #[test]
900 fn test_renew_missing_existing_token() {
901 let mut sts = SecurityTokenService::new(StsConfig::default());
902
903 let request = RequestSecurityToken {
904 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Renew".to_string(),
905 token_type: "".to_string(),
906 applies_to: None,
907 lifetime: None,
908 key_type: None,
909 key_size: None,
910 existing_token: None, auth_context: None,
912 };
913
914 let err = sts.process_request(request).unwrap_err();
915 let msg = format!("{err}");
916 assert!(
917 msg.contains("Existing token required for renewal"),
918 "got: {msg}"
919 );
920 }
921
922 #[test]
923 fn test_issue_with_proof_token_symmetric_key() {
924 let mut config = StsConfig::default();
925 config.include_proof_tokens = true;
926 let mut sts = SecurityTokenService::new(config);
927
928 let request = RequestSecurityToken {
929 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
930 token_type: "urn:ietf:params:oauth:token-type:jwt".to_string(),
931 applies_to: Some("https://rp.example.com".to_string()),
932 lifetime: None,
933 key_type: Some(
934 "http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey".to_string(),
935 ),
936 key_size: None,
937 existing_token: None,
938 auth_context: Some(AuthenticationContext {
939 username: "keyuser".to_string(),
940 auth_method: "certificate".to_string(),
941 claims: HashMap::new(),
942 }),
943 };
944
945 let resp = sts.process_request(request).unwrap();
946 let proof = resp
947 .requested_proof_token
948 .expect("proof token should be present for symmetric key request");
949 assert_eq!(proof.token_type, "SymmetricKey");
950 assert_eq!(proof.key_material.len(), 32); assert!(proof.key_identifier.starts_with("key-"));
952 }
953
954 #[test]
955 fn test_issue_and_validate_roundtrip() {
956 let mut sts = SecurityTokenService::new(StsConfig::default());
957
958 let issue_req = RequestSecurityToken {
960 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
961 token_type: "urn:ietf:params:oauth:token-type:jwt".to_string(),
962 applies_to: Some("https://rp.example.com".to_string()),
963 lifetime: None,
964 key_type: None,
965 key_size: None,
966 existing_token: None,
967 auth_context: Some(AuthenticationContext {
968 username: "roundtrip_user".to_string(),
969 auth_method: "password".to_string(),
970 claims: HashMap::new(),
971 }),
972 };
973
974 let issue_resp = sts.process_request(issue_req).unwrap();
975 let token_id = issue_resp
976 .requested_unattached_reference
977 .expect("token id should be returned");
978
979 let validate_req = RequestSecurityToken {
981 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Validate".to_string(),
982 token_type: "".to_string(),
983 applies_to: None,
984 lifetime: None,
985 key_type: None,
986 key_size: None,
987 existing_token: Some(token_id),
988 auth_context: None,
989 };
990
991 let validate_resp = sts.process_request(validate_req).unwrap();
992 assert_eq!(validate_resp.requested_security_token, "Valid");
993 assert_eq!(
994 validate_resp.applies_to.as_deref(),
995 Some("https://rp.example.com")
996 );
997 }
998
999 #[test]
1000 fn test_issue_and_cancel_then_validate_fails() {
1001 let mut sts = SecurityTokenService::new(StsConfig::default());
1002
1003 let issue_req = RequestSecurityToken {
1005 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue".to_string(),
1006 token_type: "urn:oasis:names:tc:SAML:2.0:assertion".to_string(),
1007 applies_to: None,
1008 lifetime: None,
1009 key_type: None,
1010 key_size: None,
1011 existing_token: None,
1012 auth_context: Some(AuthenticationContext {
1013 username: "cancelme".to_string(),
1014 auth_method: "password".to_string(),
1015 claims: HashMap::new(),
1016 }),
1017 };
1018
1019 let issue_resp = sts.process_request(issue_req).unwrap();
1020 let token_id = issue_resp
1021 .requested_unattached_reference
1022 .expect("should have token id");
1023
1024 let cancel_req = RequestSecurityToken {
1026 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Cancel".to_string(),
1027 token_type: "".to_string(),
1028 applies_to: None,
1029 lifetime: None,
1030 key_type: None,
1031 key_size: None,
1032 existing_token: Some(token_id.clone()),
1033 auth_context: None,
1034 };
1035 sts.process_request(cancel_req).unwrap();
1036
1037 let validate_req = RequestSecurityToken {
1039 request_type: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Validate".to_string(),
1040 token_type: "".to_string(),
1041 applies_to: None,
1042 lifetime: None,
1043 key_type: None,
1044 key_size: None,
1045 existing_token: Some(token_id),
1046 auth_context: None,
1047 };
1048
1049 let err = sts.process_request(validate_req).unwrap_err();
1050 let msg = format!("{err}");
1051 assert!(msg.contains("Token not found"), "got: {msg}");
1052 }
1053}