auth_framework/server/oidc/
oidc_response_modes.rs

1//! OpenID Connect Response Modes Extension
2//!
3//! This module implements additional OAuth 2.0 and OpenID Connect response modes
4//! beyond the standard 'query' and 'fragment' modes defined in the core specifications.
5//!
6//! # Implemented Response Modes
7//!
8//! - **Form Post Response Mode** (OAuth 2.0 Form Post Response Mode)
9//! - **JWT Response Mode** (JARM - JWT Secured Authorization Response Mode)
10//! - **Multiple Response Types** (OAuth 2.0 Multiple Response Types)
11//!
12//! These form the foundation for many other OpenID specifications.
13
14use crate::errors::{AuthError, Result};
15use html_escape;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19/// Response Mode types supported
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub enum ResponseMode {
22    /// Standard query parameter response
23    Query,
24    /// Fragment-based response
25    Fragment,
26    /// Form POST response
27    FormPost,
28    /// JWT-secured response (JARM)
29    JwtQuery,
30    /// JWT-secured fragment response
31    JwtFragment,
32    /// JWT-secured form POST response
33    JwtFormPost,
34}
35
36/// Multiple Response Types handler
37#[derive(Debug, Clone)]
38pub struct MultipleResponseTypesManager {
39    /// Configuration for response types
40    config: MultipleResponseTypesConfig,
41}
42
43#[derive(Debug, Clone)]
44pub struct MultipleResponseTypesConfig {
45    /// Supported response types
46    pub supported_response_types: Vec<String>,
47    /// Enable multiple response types in single request
48    pub enable_multiple_types: bool,
49}
50
51impl Default for MultipleResponseTypesConfig {
52    fn default() -> Self {
53        Self {
54            supported_response_types: vec![
55                "code".to_string(),
56                "token".to_string(),
57                "id_token".to_string(),
58                "code token".to_string(),
59                "code id_token".to_string(),
60                "token id_token".to_string(),
61                "code token id_token".to_string(),
62            ],
63            enable_multiple_types: true,
64        }
65    }
66}
67
68/// Form Post Response Mode implementation
69#[derive(Debug, Clone)]
70pub struct FormPostResponseMode {
71    /// Target redirect URI
72    pub redirect_uri: String,
73    /// Form parameters to post
74    pub parameters: HashMap<String, String>,
75}
76
77/// JARM (JWT Response Mode) implementation
78#[derive(Debug, Clone)]
79pub struct JarmResponseMode {
80    /// JWT response token
81    pub response_token: String,
82    /// Response mode for JWT delivery
83    pub delivery_mode: ResponseMode,
84}
85
86impl MultipleResponseTypesManager {
87    /// Create new manager
88    pub fn new(config: MultipleResponseTypesConfig) -> Self {
89        Self { config }
90    }
91
92    /// Parse and validate response type parameter
93    pub fn parse_response_type(&self, response_type: &str) -> Result<Vec<String>> {
94        let types: Vec<String> = response_type
95            .split_whitespace()
96            .map(|s| s.to_string())
97            .collect();
98
99        // Validate each type
100        for response_type in &types {
101            if !self.is_supported_response_type(response_type) {
102                return Err(AuthError::validation(format!(
103                    "Unsupported response_type: {}",
104                    response_type
105                )));
106            }
107        }
108
109        // Validate combinations
110        self.validate_response_type_combination(&types)?;
111
112        Ok(types)
113    }
114
115    /// Check if response type is supported
116    pub fn is_supported_response_type(&self, response_type: &str) -> bool {
117        let full_type = match response_type {
118            "code" | "token" | "id_token" => response_type.to_string(),
119            _ => return false,
120        };
121
122        self.config.supported_response_types.contains(&full_type)
123            || self
124                .config
125                .supported_response_types
126                .iter()
127                .any(|t| t.contains(response_type))
128    }
129
130    /// Validate response type combinations
131    fn validate_response_type_combination(&self, types: &[String]) -> Result<()> {
132        if types.is_empty() {
133            return Err(AuthError::validation("Empty response_type"));
134        }
135
136        // Specific validation rules for combinations
137        if types.contains(&"token".to_string()) || types.contains(&"id_token".to_string()) {
138            // Implicit flow requirements - should have nonce for id_token
139            // This will be validated at the authorization level
140        }
141
142        if types.len() > 3 {
143            return Err(AuthError::validation("Too many response types"));
144        }
145
146        Ok(())
147    }
148
149    /// Generate response based on types
150    pub async fn generate_response(
151        &self,
152        response_types: &[String],
153        authorization_code: Option<String>,
154        access_token: Option<String>,
155        id_token: Option<String>,
156    ) -> Result<HashMap<String, String>> {
157        let mut response = HashMap::new();
158
159        for response_type in response_types {
160            match response_type.as_str() {
161                "code" => {
162                    if let Some(code) = &authorization_code {
163                        response.insert("code".to_string(), code.clone());
164                    }
165                }
166                "token" => {
167                    if let Some(token) = &access_token {
168                        response.insert("access_token".to_string(), token.clone());
169                        response.insert("token_type".to_string(), "Bearer".to_string());
170                        // Add expires_in, scope, etc.
171                        response.insert("expires_in".to_string(), "3600".to_string());
172                    }
173                }
174                "id_token" => {
175                    if let Some(token) = &id_token {
176                        response.insert("id_token".to_string(), token.clone());
177                    }
178                }
179                _ => {
180                    return Err(AuthError::validation(format!(
181                        "Unsupported response type: {}",
182                        response_type
183                    )));
184                }
185            }
186        }
187
188        Ok(response)
189    }
190}
191
192impl FormPostResponseMode {
193    /// Create new form post response
194    pub fn new(redirect_uri: String, parameters: HashMap<String, String>) -> Self {
195        Self {
196            redirect_uri,
197            parameters,
198        }
199    }
200
201    /// Generate HTML form for auto-submission
202    pub fn generate_html_form(&self) -> String {
203        let mut form = format!(
204            r#"<!DOCTYPE html>
205<html>
206<head>
207    <title>Authorization Response</title>
208</head>
209<body>
210    <form method="post" action="{}" id="response_form">
211"#,
212            self.redirect_uri
213        );
214
215        for (name, value) in &self.parameters {
216            form.push_str(&format!(
217                r#"        <input type="hidden" name="{}" value="{}" />
218"#,
219                html_escape::encode_text(name),
220                html_escape::encode_text(value)
221            ));
222        }
223
224        form.push_str(
225            r#"    </form>
226    <script>
227        window.onload = function() {
228            document.getElementById('response_form').submit();
229        };
230    </script>
231</body>
232</html>"#,
233        );
234
235        form
236    }
237}
238
239impl JarmResponseMode {
240    /// Create new JARM response
241    pub fn new(response_token: String, delivery_mode: ResponseMode) -> Self {
242        Self {
243            response_token,
244            delivery_mode,
245        }
246    }
247
248    /// Generate JARM response parameters
249    pub fn generate_response_parameters(&self) -> HashMap<String, String> {
250        let mut params = HashMap::new();
251        params.insert("response".to_string(), self.response_token.clone());
252        params
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_multiple_response_types_parsing() {
262        let manager = MultipleResponseTypesManager::new(MultipleResponseTypesConfig::default());
263
264        // Test single response type
265        let result = manager.parse_response_type("code").unwrap();
266        assert_eq!(result, vec!["code"]);
267
268        // Test multiple response types
269        let result = manager.parse_response_type("code token").unwrap();
270        assert_eq!(result, vec!["code", "token"]);
271
272        // Test invalid response type
273        assert!(manager.parse_response_type("invalid").is_err());
274    }
275
276    #[test]
277    fn test_form_post_html_generation() {
278        let mut params = HashMap::new();
279        params.insert("code".to_string(), "auth_code_123".to_string());
280        params.insert("state".to_string(), "client_state".to_string());
281
282        let form_post =
283            FormPostResponseMode::new("https://client.example.com/callback".to_string(), params);
284
285        let html = form_post.generate_html_form();
286        assert!(html.contains("auth_code_123"));
287        assert!(html.contains("client_state"));
288        assert!(html.contains("https://client.example.com/callback"));
289    }
290
291    #[test]
292    fn test_jarm_response_generation() {
293        let jarm = JarmResponseMode::new(
294            "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...".to_string(),
295            ResponseMode::JwtQuery,
296        );
297
298        let params = jarm.generate_response_parameters();
299        assert!(params.contains_key("response"));
300        assert!(params["response"].starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9"));
301    }
302}
303
304