auth_framework/server/oidc/
oidc_error_extensions.rs

1//! OpenID Connect Core Error Code Extensions
2//!
3//! This module implements additional error codes for OpenID Connect,
4//! including the `unmet_authentication_requirements` error code and other
5//! enhanced error handling capabilities.
6//!
7//! # Implemented Error Extensions
8//!
9//! - `unmet_authentication_requirements` - Authentication requirements not met
10//! - Enhanced error descriptions and URIs
11//! - Structured error reporting
12//! - Error code validation and mapping
13//! - Custom error code mappings for extensible error handling
14//!
15//! # Custom Error Mappings
16//!
17//! The `OidcErrorManager` supports custom error code mappings that allow:
18//! - Mapping custom string identifiers to standard or extended error codes
19//! - Runtime extensibility for domain-specific error codes
20//! - Override standard error code mappings for specialized behavior
21//! - Error code resolution from string identifiers
22//!
23//! # Usage Examples
24//!
25//! ```rust
26//! use auth_framework::server::oidc_error_extensions::{OidcErrorManager, OidcErrorCode};
27//!
28//! let mut manager = OidcErrorManager::default();
29//!
30//! // Add custom error mapping
31//! manager.add_custom_error_mapping(
32//!     "payment_required".to_string(),
33//!     OidcErrorCode::InsufficientIdentityAssurance,
34//! );
35//!
36//! // Resolve error from identifier
37//! let error_code = manager.resolve_error_code("payment_required");
38//!
39//! // Create error response from identifier
40//! let response = manager.create_error_response_from_identifier(
41//!     "payment_required",
42//!     Some("Payment verification required".to_string()),
43//!     Some("state123".to_string()),
44//!     std::collections::HashMap::new(),
45//! ).unwrap();
46//! ```
47
48use crate::errors::{AuthError, Result};
49use serde::{Deserialize, Serialize};
50use std::collections::HashMap;
51
52/// Extended OpenID Connect error codes
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "snake_case")]
55pub enum OidcErrorCode {
56    // Standard OAuth 2.0 errors
57    InvalidRequest,
58    InvalidClient,
59    InvalidGrant,
60    UnauthorizedClient,
61    UnsupportedGrantType,
62    InvalidScope,
63
64    // Standard OpenID Connect errors
65    InteractionRequired,
66    LoginRequired,
67    AccountSelectionRequired,
68    ConsentRequired,
69    InvalidRequestUri,
70    InvalidRequestObject,
71    RequestNotSupported,
72    RequestUriNotSupported,
73    RegistrationNotSupported,
74
75    // Extended error codes
76    /// Authentication requirements specified in the request were not met
77    UnmetAuthenticationRequirements,
78    /// The requested authentication context class reference values were not satisfied
79    UnmetAuthenticationContextRequirements,
80    /// Session selection required for multi-session scenarios
81    SessionSelectionRequired,
82    /// The authorization server requires user authentication via a different method
83    AuthenticationMethodRequired,
84    /// The requested identity verification level could not be satisfied
85    InsufficientIdentityAssurance,
86    /// The authorization server temporarily cannot service the request
87    TemporarilyUnavailable,
88    /// The request requires user registration/enrollment
89    RegistrationRequired,
90    /// The requested prompt value is not supported
91    UnsupportedPromptValue,
92    /// Multiple matching users found, selection required
93    UserSelectionRequired,
94}
95
96/// OpenID Connect error response
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct OidcErrorResponse {
99    /// The error code
100    pub error: OidcErrorCode,
101    /// Human-readable error description
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub error_description: Option<String>,
104    /// URI to error documentation
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub error_uri: Option<String>,
107    /// State parameter from the request
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub state: Option<String>,
110    /// Additional error details
111    #[serde(flatten)]
112    pub additional_details: HashMap<String, serde_json::Value>,
113}
114
115/// Authentication requirements details
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct AuthenticationRequirements {
118    /// Required authentication context class references
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub acr_values: Option<Vec<String>>,
121    /// Required authentication methods references
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub amr_values: Option<Vec<String>>,
124    /// Maximum authentication age
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub max_age: Option<u64>,
127    /// Required identity assurance level
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub identity_assurance_level: Option<String>,
130}
131
132/// Error handling manager for OpenID Connect
133#[derive(Debug, Clone)]
134pub struct OidcErrorManager {
135    /// Base error documentation URI
136    error_base_uri: String,
137    /// Custom error mappings
138    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    /// Get standard error description for error code
153    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            // Extended error codes
197            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    /// Check if this error code requires user interaction
228    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    /// Check if this error code indicates an authentication issue
243    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    /// Create new error manager
257    pub fn new(error_base_uri: String) -> Self {
258        Self {
259            error_base_uri,
260            custom_error_mappings: HashMap::new(),
261        }
262    }
263
264    /// Create error response for unmet authentication requirements
265    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    /// Create error response for insufficient ACR
310    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    /// Create generic error response
340    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    /// Add custom error mapping
358    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    /// Remove custom error mapping
363    pub fn remove_custom_error_mapping(&mut self, identifier: &str) -> Option<OidcErrorCode> {
364        self.custom_error_mappings.remove(identifier)
365    }
366
367    /// Get error code from string identifier (checks custom mappings first, then standard codes)
368    pub fn resolve_error_code(&self, identifier: &str) -> Option<OidcErrorCode> {
369        // Check custom mappings first
370        if let Some(error_code) = self.custom_error_mappings.get(identifier) {
371            return Some(error_code.clone());
372        }
373
374        // Check standard error codes
375        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    /// Create error response from string identifier
409    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    /// Get all custom error mappings
431    pub fn get_custom_mappings(&self) -> &HashMap<String, OidcErrorCode> {
432        &self.custom_error_mappings
433    }
434
435    /// Clear all custom error mappings
436    pub fn clear_custom_mappings(&mut self) {
437        self.custom_error_mappings.clear();
438    }
439
440    /// Check if custom mapping exists
441    pub fn has_custom_mapping(&self, identifier: &str) -> bool {
442        self.custom_error_mappings.contains_key(identifier)
443    }
444
445    /// Validate authentication requirements against performed authentication
446    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        // Check ACR requirements
455        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        // Check AMR requirements
473        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        // Check max_age requirement
491        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        // Test adding custom error mapping
549        manager.add_custom_error_mapping(
550            "custom_validation_failed".to_string(),
551            OidcErrorCode::InvalidRequest,
552        );
553
554        // Test resolving custom error code
555        let resolved = manager.resolve_error_code("custom_validation_failed");
556        assert_eq!(resolved, Some(OidcErrorCode::InvalidRequest));
557
558        // Test resolving standard error code
559        let standard = manager.resolve_error_code("login_required");
560        assert_eq!(standard, Some(OidcErrorCode::LoginRequired));
561
562        // Test resolving unknown error code
563        let unknown = manager.resolve_error_code("nonexistent_error");
564        assert_eq!(unknown, None);
565
566        // Test has_custom_mapping
567        assert!(manager.has_custom_mapping("custom_validation_failed"));
568        assert!(!manager.has_custom_mapping("login_required"));
569
570        // Test creating error response from identifier
571        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        // Test remove custom mapping
584        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        // Test clear all mappings
589        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        // Add domain-specific error mappings for a banking application
622        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        // Demonstrate custom error response creation
636        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        // Verify custom mappings take precedence over standard ones
675        manager.add_custom_error_mapping(
676            "login_required".to_string(),
677            OidcErrorCode::RegistrationRequired, // Override standard behavior
678        );
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        // Verify management functions
695        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        // Clean up specific mapping
700        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        // Test clear all
705        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        // Test all standard error codes
714        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        // Custom mappings take precedence over standard codes
728        let mut manager = OidcErrorManager::default();
729        manager.add_custom_error_mapping(
730            "login_required".to_string(),
731            OidcErrorCode::ConsentRequired, // Override standard mapping
732        );
733
734        assert_eq!(
735            manager.resolve_error_code("login_required"),
736            Some(OidcErrorCode::ConsentRequired) // Should return custom mapping
737        );
738    }
739}
740
741