Skip to main content

auth_framework/protocols/
ws_trust.rs

1//! WS-Trust 1.3 Security Token Service (STS) Support
2//!
3//! This module provides WS-Trust 1.3 Security Token Service functionality for token exchange,
4//! issuance, and validation scenarios.
5
6use crate::errors::{AuthError, Result};
7use crate::protocols::saml_assertions::SamlAssertionBuilder;
8// SamlAssertion removed from import - not currently used but may be needed later
9use 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
16/// WS-Trust Security Token Service
17///
18/// **Note:** Issued tokens are stored in an in-memory `HashMap`. They will be
19/// lost on restart and are not shared across STS instances. For production
20/// multi-instance deployments, integrate with the `StorageBackend` KV store
21/// for persistent/shared token state.
22pub struct SecurityTokenService {
23    /// STS configuration
24    config: StsConfig,
25
26    /// WS-Security client for generating secure headers
27    ws_security: WsSecurityClient,
28
29    /// Issued tokens cache
30    issued_tokens: HashMap<String, IssuedToken>,
31}
32
33/// STS Configuration
34#[derive(Debug, Clone)]
35pub struct StsConfig {
36    /// STS issuer identifier
37    pub issuer: String,
38
39    /// Default token lifetime
40    pub default_token_lifetime: Duration,
41
42    /// Maximum token lifetime
43    pub max_token_lifetime: Duration,
44
45    /// Supported token types
46    pub supported_token_types: Vec<String>,
47
48    /// STS endpoint URL
49    pub endpoint_url: String,
50
51    /// Whether to include proof tokens
52    pub include_proof_tokens: bool,
53
54    /// Trust relationships
55    pub trust_relationships: Vec<TrustRelationship>,
56
57    /// HMAC-HS256 signing secret used when issuing JWT tokens.
58    ///
59    /// **Must be set to a strong, randomly-generated value in production.**
60    /// `StsConfig::default()` generates a cryptographically random secret
61    /// automatically; override this field when you need a stable / shared
62    /// signing key (e.g. for multi-node deployments that share the same
63    /// validator).
64    pub jwt_signing_secret: String,
65}
66
67/// Trust relationship with relying parties
68#[derive(Debug, Clone)]
69pub struct TrustRelationship {
70    /// Relying party identifier
71    pub rp_identifier: String,
72
73    /// Certificate for encryption/signing
74    pub certificate: Option<Vec<u8>>,
75
76    /// Allowed token types
77    pub allowed_token_types: Vec<String>,
78
79    /// Maximum token lifetime for this RP
80    pub max_token_lifetime: Option<Duration>,
81}
82
83/// Issued token information
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct IssuedToken {
86    /// Token ID
87    pub token_id: String,
88
89    /// Token type
90    pub token_type: String,
91
92    /// Token content (SAML assertion, JWT, etc.)
93    pub token_content: String,
94
95    /// Issue time
96    pub issued_at: DateTime<Utc>,
97
98    /// Expiration time
99    pub expires_at: DateTime<Utc>,
100
101    /// Subject identifier
102    pub subject: String,
103
104    /// Audience/relying party
105    pub audience: String,
106
107    /// Proof token (if any)
108    pub proof_token: Option<ProofToken>,
109}
110
111/// Proof token for holder-of-key scenarios
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ProofToken {
114    /// Proof token type (symmetric key, certificate, etc.)
115    pub token_type: String,
116
117    /// Key material
118    pub key_material: Vec<u8>,
119
120    /// Key identifier
121    pub key_identifier: String,
122}
123
124/// WS-Trust Request Security Token (RST)
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RequestSecurityToken {
127    /// Request type (Issue, Renew, Cancel, Validate)
128    pub request_type: String,
129
130    /// Token type being requested
131    pub token_type: String,
132
133    /// Applies to (target service/audience)
134    pub applies_to: Option<String>,
135
136    /// Lifetime requirements
137    pub lifetime: Option<TokenLifetime>,
138
139    /// Key type (Bearer, Symmetric, Asymmetric)
140    pub key_type: Option<String>,
141
142    /// Key size for symmetric keys
143    pub key_size: Option<u32>,
144
145    /// Existing token (for renew/validate operations)
146    pub existing_token: Option<String>,
147
148    /// Authentication context
149    pub auth_context: Option<AuthenticationContext>,
150}
151
152/// Token lifetime specification
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct TokenLifetime {
155    /// Created time
156    pub created: DateTime<Utc>,
157
158    /// Expires time
159    pub expires: DateTime<Utc>,
160}
161
162/// Authentication context for token requests
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct AuthenticationContext {
165    /// Username
166    pub username: String,
167
168    /// Authentication method
169    pub auth_method: String,
170
171    /// Additional claims
172    pub claims: HashMap<String, String>,
173}
174
175/// WS-Trust Request Security Token Response (RSTR)
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct RequestSecurityTokenResponse {
178    /// Request type being responded to
179    pub request_type: String,
180
181    /// Token type issued
182    pub token_type: String,
183
184    /// Lifetime of issued token
185    pub lifetime: TokenLifetime,
186
187    /// Applies to (target audience)
188    pub applies_to: Option<String>,
189
190    /// Requested security token
191    pub requested_security_token: String,
192
193    /// Requested proof token
194    pub requested_proof_token: Option<ProofToken>,
195
196    /// Token reference for future operations
197    pub requested_attached_reference: Option<String>,
198
199    /// Token reference for external use
200    pub requested_unattached_reference: Option<String>,
201}
202
203impl SecurityTokenService {
204    /// Create a new Security Token Service
205    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    /// Process a WS-Trust Request Security Token
217    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    /// Issue a new security token
236    fn issue_token(
237        &mut self,
238        request: RequestSecurityToken,
239    ) -> Result<RequestSecurityTokenResponse> {
240        // Validate authentication context
241        let auth_context = request
242            .auth_context
243            .as_ref()
244            .ok_or_else(|| AuthError::auth_method("wstrust", "Authentication context required"))?;
245
246        // Determine token lifetime
247        let now = Utc::now();
248        let lifetime = if let Some(ref requested_lifetime) = request.lifetime {
249            // Validate requested lifetime
250            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        // Generate token based on type
269        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        // Generate proof token if required
282        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        // Store issued token
292        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    /// Issue a SAML 2.0 assertion token
319    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        // Add audience if specified
331        if let Some(ref audience) = request.applies_to {
332            assertion_builder = assertion_builder.with_audience(audience);
333        }
334
335        // Add additional claims as attributes
336        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    /// Issue a JWT token signed with HMAC-HS256 using `StsConfig::jwt_signing_secret`.
345    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    /// Generate a proof token for holder-of-key scenarios
380    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]; // 256-bit symmetric key
384        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    /// Renew an existing token
394    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        // Find the token — try direct lookup first, then attempt JWT parsing
403        // to extract claims from the existing token if it looks like a JWT.
404        let mut renewal_claims = HashMap::new();
405        let token_id = if existing_token.matches('.').count() == 2 {
406            // Token looks like a JWT — extract claims from payload
407            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                        // Use "jti" or "sub" as token ID for lookup
421                        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        // Check if token is still valid
446        let now = Utc::now();
447        if now >= issued_token.expires_at {
448            return Err(AuthError::auth_method("wstrust", "Token has expired"));
449        }
450
451        // Create renewed token with new lifetime
452        let new_lifetime = TokenLifetime {
453            created: now,
454            expires: now + self.config.default_token_lifetime,
455        };
456
457        // Issue new token (carry forward original claims where available)
458        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    /// Cancel an existing token
483    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        // Remove token from cache
492        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    /// Validate an existing token
510    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        // Find and validate token
519        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    /// Create a complete WS-Trust SOAP request
546    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        // SAFETY: CSPRNG failure at initialization is terminal; the framework
596        // cannot operate without entropy.
597        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        // Verify the issued JWT has exactly 3 Base64URL parts.
693        let parts: Vec<&str> = response.requested_security_token.split('.').collect();
694        assert_eq!(parts.len(), 3, "JWT must have header.payload.signature");
695
696        // Verify the signature is valid by decoding with the same secret.
697        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), // way beyond max
816            }),
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        // Expires must be clamped to ~2 h from now, not 999 h
829        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        // Cancel of a non-existent token should succeed silently (idempotent)
852        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, // missing
868            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, // missing
911            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); // 256-bit key
951        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        // Issue
959        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        // Validate
980        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        // Issue
1004        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        // Cancel
1025        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        // Validate should now fail (token was removed)
1038        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}