1use crate::errors::{AuthError, Result};
49use serde::{Deserialize, Serialize};
50use std::collections::HashMap;
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "snake_case")]
55pub enum OidcErrorCode {
56 InvalidRequest,
58 InvalidClient,
59 InvalidGrant,
60 UnauthorizedClient,
61 UnsupportedGrantType,
62 InvalidScope,
63
64 InteractionRequired,
66 LoginRequired,
67 AccountSelectionRequired,
68 ConsentRequired,
69 InvalidRequestUri,
70 InvalidRequestObject,
71 RequestNotSupported,
72 RequestUriNotSupported,
73 RegistrationNotSupported,
74
75 UnmetAuthenticationRequirements,
78 UnmetAuthenticationContextRequirements,
80 SessionSelectionRequired,
82 AuthenticationMethodRequired,
84 InsufficientIdentityAssurance,
86 TemporarilyUnavailable,
88 RegistrationRequired,
90 UnsupportedPromptValue,
92 UserSelectionRequired,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct OidcErrorResponse {
99 pub error: OidcErrorCode,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub error_description: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub error_uri: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub state: Option<String>,
110 #[serde(flatten)]
112 pub additional_details: HashMap<String, serde_json::Value>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct AuthenticationRequirements {
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub acr_values: Option<Vec<String>>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub amr_values: Option<Vec<String>>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub max_age: Option<u64>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub identity_assurance_level: Option<String>,
130}
131
132#[derive(Debug, Clone)]
134pub struct OidcErrorManager {
135 error_base_uri: String,
137 custom_error_mappings: HashMap<String, OidcErrorCode>,
139}
140
141impl Default for OidcErrorManager {
142 fn default() -> Self {
143 Self {
144 error_base_uri: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError"
145 .to_string(),
146 custom_error_mappings: HashMap::new(),
147 }
148 }
149}
150
151impl OidcErrorCode {
152 pub fn get_description(&self) -> &'static str {
154 match self {
155 Self::InvalidRequest => {
156 "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
157 }
158 Self::InvalidClient => {
159 "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."
160 }
161 Self::InvalidGrant => {
162 "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
163 }
164 Self::UnauthorizedClient => {
165 "The authenticated client is not authorized to use this authorization grant type."
166 }
167 Self::UnsupportedGrantType => {
168 "The authorization grant type is not supported by the authorization server."
169 }
170 Self::InvalidScope => "The requested scope is invalid, unknown, or malformed.",
171
172 Self::InteractionRequired => {
173 "The authorization server requires end-user interaction of some form to proceed."
174 }
175 Self::LoginRequired => "The authorization server requires end-user authentication.",
176 Self::AccountSelectionRequired => {
177 "The end-user is required to select a session at the authorization server."
178 }
179 Self::ConsentRequired => "The authorization server requires end-user consent.",
180 Self::InvalidRequestUri => {
181 "The request_uri in the authorization request returns an error or contains invalid data."
182 }
183 Self::InvalidRequestObject => {
184 "The request parameter contains an invalid request object."
185 }
186 Self::RequestNotSupported => {
187 "The authorization server does not support use of the request parameter."
188 }
189 Self::RequestUriNotSupported => {
190 "The authorization server does not support use of the request_uri parameter."
191 }
192 Self::RegistrationNotSupported => {
193 "The authorization server does not support use of the registration parameter."
194 }
195
196 Self::UnmetAuthenticationRequirements => {
198 "The authentication performed does not meet the authentication requirements specified in the request."
199 }
200 Self::UnmetAuthenticationContextRequirements => {
201 "The requested authentication context class reference values were not satisfied by the performed authentication."
202 }
203 Self::SessionSelectionRequired => {
204 "Multiple active sessions exist, and the end-user must select which session to use."
205 }
206 Self::AuthenticationMethodRequired => {
207 "The authorization server requires the end-user to authenticate using a specific authentication method."
208 }
209 Self::InsufficientIdentityAssurance => {
210 "The level of identity assurance achieved does not meet the requirements for this request."
211 }
212 Self::TemporarilyUnavailable => {
213 "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server."
214 }
215 Self::RegistrationRequired => {
216 "The end-user must complete a registration process before authentication can proceed."
217 }
218 Self::UnsupportedPromptValue => {
219 "The authorization server does not support the requested prompt value."
220 }
221 Self::UserSelectionRequired => {
222 "Multiple users match the provided identification, and selection is required."
223 }
224 }
225 }
226
227 pub fn requires_interaction(&self) -> bool {
229 matches!(
230 self,
231 Self::InteractionRequired
232 | Self::LoginRequired
233 | Self::AccountSelectionRequired
234 | Self::ConsentRequired
235 | Self::SessionSelectionRequired
236 | Self::AuthenticationMethodRequired
237 | Self::RegistrationRequired
238 | Self::UserSelectionRequired
239 )
240 }
241
242 pub fn is_authentication_error(&self) -> bool {
244 matches!(
245 self,
246 Self::LoginRequired
247 | Self::UnmetAuthenticationRequirements
248 | Self::UnmetAuthenticationContextRequirements
249 | Self::AuthenticationMethodRequired
250 | Self::InsufficientIdentityAssurance
251 )
252 }
253}
254
255impl OidcErrorManager {
256 pub fn new(error_base_uri: String) -> Self {
258 Self {
259 error_base_uri,
260 custom_error_mappings: HashMap::new(),
261 }
262 }
263
264 pub fn create_unmet_auth_requirements_error(
266 &self,
267 requirements: AuthenticationRequirements,
268 state: Option<String>,
269 ) -> OidcErrorResponse {
270 let mut additional_details = HashMap::new();
271
272 if let Some(acr_values) = &requirements.acr_values {
273 additional_details.insert(
274 "required_acr_values".to_string(),
275 serde_json::to_value(acr_values).unwrap(),
276 );
277 }
278
279 if let Some(amr_values) = &requirements.amr_values {
280 additional_details.insert(
281 "required_amr_values".to_string(),
282 serde_json::to_value(amr_values).unwrap(),
283 );
284 }
285
286 if let Some(max_age) = requirements.max_age {
287 additional_details.insert(
288 "max_age".to_string(),
289 serde_json::Value::Number(serde_json::Number::from(max_age)),
290 );
291 }
292
293 OidcErrorResponse {
294 error: OidcErrorCode::UnmetAuthenticationRequirements,
295 error_description: Some(
296 OidcErrorCode::UnmetAuthenticationRequirements
297 .get_description()
298 .to_string(),
299 ),
300 error_uri: Some(format!(
301 "{}#UnmetAuthenticationRequirements",
302 self.error_base_uri
303 )),
304 state,
305 additional_details,
306 }
307 }
308
309 pub fn create_insufficient_acr_error(
311 &self,
312 required_acr: Vec<String>,
313 achieved_acr: Option<String>,
314 state: Option<String>,
315 ) -> OidcErrorResponse {
316 let mut additional_details = HashMap::new();
317 additional_details.insert(
318 "required_acr_values".to_string(),
319 serde_json::to_value(required_acr).unwrap(),
320 );
321
322 if let Some(acr) = achieved_acr {
323 additional_details.insert("achieved_acr".to_string(), serde_json::Value::String(acr));
324 }
325
326 OidcErrorResponse {
327 error: OidcErrorCode::UnmetAuthenticationContextRequirements,
328 error_description: Some(
329 OidcErrorCode::UnmetAuthenticationContextRequirements
330 .get_description()
331 .to_string(),
332 ),
333 error_uri: Some(format!("{}#ACRRequirements", self.error_base_uri)),
334 state,
335 additional_details,
336 }
337 }
338
339 pub fn create_error_response(
341 &self,
342 error_code: OidcErrorCode,
343 custom_description: Option<String>,
344 state: Option<String>,
345 additional_details: HashMap<String, serde_json::Value>,
346 ) -> OidcErrorResponse {
347 OidcErrorResponse {
348 error: error_code.clone(),
349 error_description: custom_description
350 .or_else(|| Some(error_code.get_description().to_string())),
351 error_uri: Some(format!("{}#{:?}", self.error_base_uri, error_code)),
352 state,
353 additional_details,
354 }
355 }
356
357 pub fn add_custom_error_mapping(&mut self, identifier: String, error_code: OidcErrorCode) {
359 self.custom_error_mappings.insert(identifier, error_code);
360 }
361
362 pub fn remove_custom_error_mapping(&mut self, identifier: &str) -> Option<OidcErrorCode> {
364 self.custom_error_mappings.remove(identifier)
365 }
366
367 pub fn resolve_error_code(&self, identifier: &str) -> Option<OidcErrorCode> {
369 if let Some(error_code) = self.custom_error_mappings.get(identifier) {
371 return Some(error_code.clone());
372 }
373
374 match identifier {
376 "invalid_request" => Some(OidcErrorCode::InvalidRequest),
377 "invalid_client" => Some(OidcErrorCode::InvalidClient),
378 "invalid_grant" => Some(OidcErrorCode::InvalidGrant),
379 "unauthorized_client" => Some(OidcErrorCode::UnauthorizedClient),
380 "unsupported_grant_type" => Some(OidcErrorCode::UnsupportedGrantType),
381 "invalid_scope" => Some(OidcErrorCode::InvalidScope),
382 "interaction_required" => Some(OidcErrorCode::InteractionRequired),
383 "login_required" => Some(OidcErrorCode::LoginRequired),
384 "account_selection_required" => Some(OidcErrorCode::AccountSelectionRequired),
385 "consent_required" => Some(OidcErrorCode::ConsentRequired),
386 "invalid_request_uri" => Some(OidcErrorCode::InvalidRequestUri),
387 "invalid_request_object" => Some(OidcErrorCode::InvalidRequestObject),
388 "request_not_supported" => Some(OidcErrorCode::RequestNotSupported),
389 "request_uri_not_supported" => Some(OidcErrorCode::RequestUriNotSupported),
390 "registration_not_supported" => Some(OidcErrorCode::RegistrationNotSupported),
391 "unmet_authentication_requirements" => {
392 Some(OidcErrorCode::UnmetAuthenticationRequirements)
393 }
394 "unmet_authentication_context_requirements" => {
395 Some(OidcErrorCode::UnmetAuthenticationContextRequirements)
396 }
397 "session_selection_required" => Some(OidcErrorCode::SessionSelectionRequired),
398 "authentication_method_required" => Some(OidcErrorCode::AuthenticationMethodRequired),
399 "insufficient_identity_assurance" => Some(OidcErrorCode::InsufficientIdentityAssurance),
400 "temporarily_unavailable" => Some(OidcErrorCode::TemporarilyUnavailable),
401 "registration_required" => Some(OidcErrorCode::RegistrationRequired),
402 "unsupported_prompt_value" => Some(OidcErrorCode::UnsupportedPromptValue),
403 "user_selection_required" => Some(OidcErrorCode::UserSelectionRequired),
404 _ => None,
405 }
406 }
407
408 pub fn create_error_response_from_identifier(
410 &self,
411 error_identifier: &str,
412 custom_description: Option<String>,
413 state: Option<String>,
414 additional_details: HashMap<String, serde_json::Value>,
415 ) -> Result<OidcErrorResponse> {
416 match self.resolve_error_code(error_identifier) {
417 Some(error_code) => Ok(self.create_error_response(
418 error_code,
419 custom_description,
420 state,
421 additional_details,
422 )),
423 None => Err(AuthError::validation(format!(
424 "Unknown error code identifier: {}",
425 error_identifier
426 ))),
427 }
428 }
429
430 pub fn get_custom_mappings(&self) -> &HashMap<String, OidcErrorCode> {
432 &self.custom_error_mappings
433 }
434
435 pub fn clear_custom_mappings(&mut self) {
437 self.custom_error_mappings.clear();
438 }
439
440 pub fn has_custom_mapping(&self, identifier: &str) -> bool {
442 self.custom_error_mappings.contains_key(identifier)
443 }
444
445 pub fn validate_authentication_requirements(
447 &self,
448 requirements: &AuthenticationRequirements,
449 performed_acr: Option<&str>,
450 performed_amr: Option<&[String]>,
451 auth_time: Option<u64>,
452 current_time: u64,
453 ) -> Result<()> {
454 if let Some(required_acr) = &requirements.acr_values {
456 match performed_acr {
457 Some(acr) => {
458 if !required_acr.contains(&acr.to_string()) {
459 return Err(AuthError::validation(
460 "Authentication context class requirements not met",
461 ));
462 }
463 }
464 None => {
465 return Err(AuthError::validation(
466 "No authentication context class provided",
467 ));
468 }
469 }
470 }
471
472 if let Some(required_amr) = &requirements.amr_values {
474 match performed_amr {
475 Some(amr) => {
476 for required in required_amr {
477 if !amr.contains(required) {
478 return Err(AuthError::validation(
479 "Authentication method requirements not met",
480 ));
481 }
482 }
483 }
484 None => {
485 return Err(AuthError::validation("No authentication methods provided"));
486 }
487 }
488 }
489
490 if let Some(max_age) = requirements.max_age {
492 if let Some(auth_time) = auth_time {
493 if current_time - auth_time > max_age {
494 return Err(AuthError::validation(
495 "Authentication is too old (exceeds max_age)",
496 ));
497 }
498 } else {
499 return Err(AuthError::validation(
500 "Authentication time not available for max_age validation",
501 ));
502 }
503 }
504
505 Ok(())
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn test_error_code_descriptions() {
515 assert!(
516 !OidcErrorCode::UnmetAuthenticationRequirements
517 .get_description()
518 .is_empty()
519 );
520 assert!(OidcErrorCode::LoginRequired.requires_interaction());
521 assert!(OidcErrorCode::UnmetAuthenticationRequirements.is_authentication_error());
522 }
523
524 #[test]
525 fn test_unmet_auth_requirements_error() {
526 let manager = OidcErrorManager::default();
527 let requirements = AuthenticationRequirements {
528 acr_values: Some(vec!["urn:mace:incommon:iap:silver".to_string()]),
529 amr_values: Some(vec!["pwd".to_string(), "mfa".to_string()]),
530 max_age: Some(3600),
531 identity_assurance_level: None,
532 };
533
534 let error = manager
535 .create_unmet_auth_requirements_error(requirements, Some("state123".to_string()));
536
537 assert_eq!(error.error, OidcErrorCode::UnmetAuthenticationRequirements);
538 assert!(error.error_description.is_some());
539 assert_eq!(error.state.as_ref().unwrap(), "state123");
540 assert!(error.additional_details.contains_key("required_acr_values"));
541 assert!(error.additional_details.contains_key("required_amr_values"));
542 }
543
544 #[test]
545 fn test_custom_error_mappings() {
546 let mut manager = OidcErrorManager::default();
547
548 manager.add_custom_error_mapping(
550 "custom_validation_failed".to_string(),
551 OidcErrorCode::InvalidRequest,
552 );
553
554 let resolved = manager.resolve_error_code("custom_validation_failed");
556 assert_eq!(resolved, Some(OidcErrorCode::InvalidRequest));
557
558 let standard = manager.resolve_error_code("login_required");
560 assert_eq!(standard, Some(OidcErrorCode::LoginRequired));
561
562 let unknown = manager.resolve_error_code("nonexistent_error");
564 assert_eq!(unknown, None);
565
566 assert!(manager.has_custom_mapping("custom_validation_failed"));
568 assert!(!manager.has_custom_mapping("login_required"));
569
570 let error_response = manager
572 .create_error_response_from_identifier(
573 "custom_validation_failed",
574 Some("Custom validation error".to_string()),
575 Some("state123".to_string()),
576 HashMap::new(),
577 )
578 .unwrap();
579
580 assert_eq!(error_response.error, OidcErrorCode::InvalidRequest);
581 assert_eq!(error_response.state.as_ref().unwrap(), "state123");
582
583 let removed = manager.remove_custom_error_mapping("custom_validation_failed");
585 assert_eq!(removed, Some(OidcErrorCode::InvalidRequest));
586 assert!(!manager.has_custom_mapping("custom_validation_failed"));
587
588 manager.add_custom_error_mapping("test1".to_string(), OidcErrorCode::InvalidScope);
590 manager.add_custom_error_mapping("test2".to_string(), OidcErrorCode::ConsentRequired);
591 assert_eq!(manager.get_custom_mappings().len(), 2);
592
593 manager.clear_custom_mappings();
594 assert_eq!(manager.get_custom_mappings().len(), 0);
595 }
596
597 #[test]
598 fn test_error_response_from_unknown_identifier() {
599 let manager = OidcErrorManager::default();
600
601 let result = manager.create_error_response_from_identifier(
602 "unknown_error_code",
603 None,
604 None,
605 HashMap::new(),
606 );
607
608 assert!(result.is_err());
609 assert!(
610 result
611 .unwrap_err()
612 .to_string()
613 .contains("Unknown error code identifier")
614 );
615 }
616
617 #[test]
618 fn test_custom_error_mappings_real_world_scenario() {
619 let mut manager = OidcErrorManager::default();
620
621 manager.add_custom_error_mapping(
623 "account_frozen".to_string(),
624 OidcErrorCode::AuthenticationMethodRequired,
625 );
626 manager.add_custom_error_mapping(
627 "kyc_verification_required".to_string(),
628 OidcErrorCode::InsufficientIdentityAssurance,
629 );
630 manager.add_custom_error_mapping(
631 "payment_limit_exceeded".to_string(),
632 OidcErrorCode::ConsentRequired,
633 );
634
635 let mut additional_details = HashMap::new();
637 additional_details.insert(
638 "account_id".to_string(),
639 serde_json::Value::String("acc-12345".to_string()),
640 );
641 additional_details.insert(
642 "freeze_reason".to_string(),
643 serde_json::Value::String("Suspicious activity detected".to_string()),
644 );
645
646 let error_response = manager
647 .create_error_response_from_identifier(
648 "account_frozen",
649 Some("Account authentication required due to security freeze".to_string()),
650 Some("banking-session-456".to_string()),
651 additional_details,
652 )
653 .unwrap();
654
655 assert_eq!(
656 error_response.error,
657 OidcErrorCode::AuthenticationMethodRequired
658 );
659 assert_eq!(
660 error_response.error_description.as_ref().unwrap(),
661 "Account authentication required due to security freeze"
662 );
663 assert_eq!(
664 error_response.state.as_ref().unwrap(),
665 "banking-session-456"
666 );
667 assert!(error_response.additional_details.contains_key("account_id"));
668 assert!(
669 error_response
670 .additional_details
671 .contains_key("freeze_reason")
672 );
673
674 manager.add_custom_error_mapping(
676 "login_required".to_string(),
677 OidcErrorCode::RegistrationRequired, );
679
680 let overridden_response = manager
681 .create_error_response_from_identifier(
682 "login_required",
683 Some("User registration required before login".to_string()),
684 None,
685 HashMap::new(),
686 )
687 .unwrap();
688
689 assert_eq!(
690 overridden_response.error,
691 OidcErrorCode::RegistrationRequired
692 );
693
694 assert_eq!(manager.get_custom_mappings().len(), 4);
696 assert!(manager.has_custom_mapping("account_frozen"));
697 assert!(!manager.has_custom_mapping("nonexistent_mapping"));
698
699 let removed = manager.remove_custom_error_mapping("account_frozen");
701 assert_eq!(removed, Some(OidcErrorCode::AuthenticationMethodRequired));
702 assert!(!manager.has_custom_mapping("account_frozen"));
703
704 manager.clear_custom_mappings();
706 assert_eq!(manager.get_custom_mappings().len(), 0);
707 }
708
709 #[test]
710 fn test_standard_error_code_resolution() {
711 let manager = OidcErrorManager::default();
712
713 assert_eq!(
715 manager.resolve_error_code("invalid_request"),
716 Some(OidcErrorCode::InvalidRequest)
717 );
718 assert_eq!(
719 manager.resolve_error_code("unmet_authentication_requirements"),
720 Some(OidcErrorCode::UnmetAuthenticationRequirements)
721 );
722 assert_eq!(
723 manager.resolve_error_code("session_selection_required"),
724 Some(OidcErrorCode::SessionSelectionRequired)
725 );
726
727 let mut manager = OidcErrorManager::default();
729 manager.add_custom_error_mapping(
730 "login_required".to_string(),
731 OidcErrorCode::ConsentRequired, );
733
734 assert_eq!(
735 manager.resolve_error_code("login_required"),
736 Some(OidcErrorCode::ConsentRequired) );
738 }
739}
740
741