auth_framework/
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::saml_assertions::SamlAssertionBuilder;
8// SamlAssertion removed from import - not currently used but may be needed later
9use 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
15/// WS-Trust Security Token Service
16pub struct SecurityTokenService {
17    /// STS configuration
18    config: StsConfig,
19
20    /// WS-Security client for generating secure headers
21    ws_security: WsSecurityClient,
22
23    /// Issued tokens cache
24    issued_tokens: HashMap<String, IssuedToken>,
25}
26
27/// STS Configuration
28#[derive(Debug, Clone)]
29pub struct StsConfig {
30    /// STS issuer identifier
31    pub issuer: String,
32
33    /// Default token lifetime
34    pub default_token_lifetime: Duration,
35
36    /// Maximum token lifetime
37    pub max_token_lifetime: Duration,
38
39    /// Supported token types
40    pub supported_token_types: Vec<String>,
41
42    /// STS endpoint URL
43    pub endpoint_url: String,
44
45    /// Whether to include proof tokens
46    pub include_proof_tokens: bool,
47
48    /// Trust relationships
49    pub trust_relationships: Vec<TrustRelationship>,
50}
51
52/// Trust relationship with relying parties
53#[derive(Debug, Clone)]
54pub struct TrustRelationship {
55    /// Relying party identifier
56    pub rp_identifier: String,
57
58    /// Certificate for encryption/signing
59    pub certificate: Option<Vec<u8>>,
60
61    /// Allowed token types
62    pub allowed_token_types: Vec<String>,
63
64    /// Maximum token lifetime for this RP
65    pub max_token_lifetime: Option<Duration>,
66}
67
68/// Issued token information
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct IssuedToken {
71    /// Token ID
72    pub token_id: String,
73
74    /// Token type
75    pub token_type: String,
76
77    /// Token content (SAML assertion, JWT, etc.)
78    pub token_content: String,
79
80    /// Issue time
81    pub issued_at: DateTime<Utc>,
82
83    /// Expiration time
84    pub expires_at: DateTime<Utc>,
85
86    /// Subject identifier
87    pub subject: String,
88
89    /// Audience/relying party
90    pub audience: String,
91
92    /// Proof token (if any)
93    pub proof_token: Option<ProofToken>,
94}
95
96/// Proof token for holder-of-key scenarios
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ProofToken {
99    /// Proof token type (symmetric key, certificate, etc.)
100    pub token_type: String,
101
102    /// Key material
103    pub key_material: Vec<u8>,
104
105    /// Key identifier
106    pub key_identifier: String,
107}
108
109/// WS-Trust Request Security Token (RST)
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct RequestSecurityToken {
112    /// Request type (Issue, Renew, Cancel, Validate)
113    pub request_type: String,
114
115    /// Token type being requested
116    pub token_type: String,
117
118    /// Applies to (target service/audience)
119    pub applies_to: Option<String>,
120
121    /// Lifetime requirements
122    pub lifetime: Option<TokenLifetime>,
123
124    /// Key type (Bearer, Symmetric, Asymmetric)
125    pub key_type: Option<String>,
126
127    /// Key size for symmetric keys
128    pub key_size: Option<u32>,
129
130    /// Existing token (for renew/validate operations)
131    pub existing_token: Option<String>,
132
133    /// Authentication context
134    pub auth_context: Option<AuthenticationContext>,
135}
136
137/// Token lifetime specification
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct TokenLifetime {
140    /// Created time
141    pub created: DateTime<Utc>,
142
143    /// Expires time
144    pub expires: DateTime<Utc>,
145}
146
147/// Authentication context for token requests
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct AuthenticationContext {
150    /// Username
151    pub username: String,
152
153    /// Authentication method
154    pub auth_method: String,
155
156    /// Additional claims
157    pub claims: HashMap<String, String>,
158}
159
160/// WS-Trust Request Security Token Response (RSTR)
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct RequestSecurityTokenResponse {
163    /// Request type being responded to
164    pub request_type: String,
165
166    /// Token type issued
167    pub token_type: String,
168
169    /// Lifetime of issued token
170    pub lifetime: TokenLifetime,
171
172    /// Applies to (target audience)
173    pub applies_to: Option<String>,
174
175    /// Requested security token
176    pub requested_security_token: String,
177
178    /// Requested proof token
179    pub requested_proof_token: Option<ProofToken>,
180
181    /// Token reference for future operations
182    pub requested_attached_reference: Option<String>,
183
184    /// Token reference for external use
185    pub requested_unattached_reference: Option<String>,
186}
187
188impl SecurityTokenService {
189    /// Create a new Security Token Service
190    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    /// Process a WS-Trust Request Security Token
202    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    /// Issue a new security token
221    fn issue_token(
222        &mut self,
223        request: RequestSecurityToken,
224    ) -> Result<RequestSecurityTokenResponse> {
225        // Validate authentication context
226        let auth_context = request
227            .auth_context
228            .as_ref()
229            .ok_or_else(|| AuthError::auth_method("wstrust", "Authentication context required"))?;
230
231        // Determine token lifetime
232        let now = Utc::now();
233        let lifetime = if let Some(ref requested_lifetime) = request.lifetime {
234            // Validate requested lifetime
235            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        // Generate token based on type
254        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        // Generate proof token if required
267        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        // Store issued token
277        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    /// Issue a SAML 2.0 assertion token
304    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        // Add audience if specified
316        if let Some(ref audience) = request.applies_to {
317            assertion_builder = assertion_builder.with_audience(audience);
318        }
319
320        // Add additional claims as attributes
321        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    /// Issue a JWT token
330    fn issue_jwt_token(
331        &self,
332        auth_context: &AuthenticationContext,
333        request: &RequestSecurityToken,
334        lifetime: &TokenLifetime,
335    ) -> Result<String> {
336        // Simplified JWT creation - would use proper JWT library in production
337        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"); // Would be real signature
351
352        Ok(format!("{}.{}.{}", header_b64, payload_b64, signature_b64))
353    }
354
355    /// Generate a proof token for holder-of-key scenarios
356    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]; // 256-bit symmetric key
360        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    /// Renew an existing token
370    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        // Find the token (simplified - would parse and validate the token)
379        let token_id = existing_token; // Assuming token ID is passed directly
380        let issued_token = self
381            .issued_tokens
382            .get(&token_id)
383            .ok_or_else(|| AuthError::auth_method("wstrust", "Token not found"))?;
384
385        // Check if token is still valid
386        let now = Utc::now();
387        if now >= issued_token.expires_at {
388            return Err(AuthError::auth_method("wstrust", "Token has expired"));
389        }
390
391        // Create renewed token with new lifetime
392        let new_lifetime = TokenLifetime {
393            created: now,
394            expires: now + self.config.default_token_lifetime,
395        };
396
397        // Issue new token (simplified - would copy original claims)
398        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    /// Cancel an existing token
419    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        // Remove token from cache
428        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    /// Validate an existing token
446    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        // Find and validate token
455        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    /// Create a complete WS-Trust SOAP request
482    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        // Decode JWT payload to verify content
609        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}