auth_framework/server/oidc/
oidc_response_modes.rs1use crate::errors::{AuthError, Result};
15use html_escape;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21pub enum ResponseMode {
22 Query,
24 Fragment,
26 FormPost,
28 JwtQuery,
30 JwtFragment,
32 JwtFormPost,
34}
35
36#[derive(Debug, Clone)]
38pub struct MultipleResponseTypesManager {
39 config: MultipleResponseTypesConfig,
41}
42
43#[derive(Debug, Clone)]
44pub struct MultipleResponseTypesConfig {
45 pub supported_response_types: Vec<String>,
47 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#[derive(Debug, Clone)]
70pub struct FormPostResponseMode {
71 pub redirect_uri: String,
73 pub parameters: HashMap<String, String>,
75}
76
77#[derive(Debug, Clone)]
79pub struct JarmResponseMode {
80 pub response_token: String,
82 pub delivery_mode: ResponseMode,
84}
85
86impl MultipleResponseTypesManager {
87 pub fn new(config: MultipleResponseTypesConfig) -> Self {
89 Self { config }
90 }
91
92 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 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 self.validate_response_type_combination(&types)?;
111
112 Ok(types)
113 }
114
115 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 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 if types.contains(&"token".to_string()) || types.contains(&"id_token".to_string()) {
138 }
141
142 if types.len() > 3 {
143 return Err(AuthError::validation("Too many response types"));
144 }
145
146 Ok(())
147 }
148
149 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 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 pub fn new(redirect_uri: String, parameters: HashMap<String, String>) -> Self {
195 Self {
196 redirect_uri,
197 parameters,
198 }
199 }
200
201 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 pub fn new(response_token: String, delivery_mode: ResponseMode) -> Self {
242 Self {
243 response_token,
244 delivery_mode,
245 }
246 }
247
248 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 let result = manager.parse_response_type("code").unwrap();
266 assert_eq!(result, vec!["code"]);
267
268 let result = manager.parse_response_type("code token").unwrap();
270 assert_eq!(result, vec!["code", "token"]);
271
272 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