Skip to main content

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,no_run
26//! use auth_framework::server::oidc::oidc_error_extensions::{OidcErrorManager, OidcErrorCode, OidcErrorResponse};
27//!
28//! // Parse error codes from strings (e.g. from HTTP query params):
29//! let code: OidcErrorCode = "invalid_request".parse().unwrap();
30//! assert_eq!(code.to_string(), "invalid_request");
31//!
32//! // Build an error response fluently:
33//! let response = OidcErrorResponse::new(OidcErrorCode::LoginRequired)
34//!     .description("Session expired, please log in again")
35//!     .state("state123")
36//!     .detail("session_age", serde_json::json!(7200))
37//!     .build();
38//!
39//! // OidcErrorManager with custom mappings:
40//! let mut manager = OidcErrorManager::default();
41//! manager.add_custom_error_mapping(
42//!     "payment_required".to_string(),
43//!     OidcErrorCode::InsufficientIdentityAssurance,
44//! );
45//! let error_code = manager.resolve_error_code("payment_required");
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
96impl std::fmt::Display for OidcErrorCode {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        let s = match self {
99            Self::InvalidRequest => "invalid_request",
100            Self::InvalidClient => "invalid_client",
101            Self::InvalidGrant => "invalid_grant",
102            Self::UnauthorizedClient => "unauthorized_client",
103            Self::UnsupportedGrantType => "unsupported_grant_type",
104            Self::InvalidScope => "invalid_scope",
105            Self::InteractionRequired => "interaction_required",
106            Self::LoginRequired => "login_required",
107            Self::AccountSelectionRequired => "account_selection_required",
108            Self::ConsentRequired => "consent_required",
109            Self::InvalidRequestUri => "invalid_request_uri",
110            Self::InvalidRequestObject => "invalid_request_object",
111            Self::RequestNotSupported => "request_not_supported",
112            Self::RequestUriNotSupported => "request_uri_not_supported",
113            Self::RegistrationNotSupported => "registration_not_supported",
114            Self::UnmetAuthenticationRequirements => "unmet_authentication_requirements",
115            Self::UnmetAuthenticationContextRequirements => "unmet_authentication_context_requirements",
116            Self::SessionSelectionRequired => "session_selection_required",
117            Self::AuthenticationMethodRequired => "authentication_method_required",
118            Self::InsufficientIdentityAssurance => "insufficient_identity_assurance",
119            Self::TemporarilyUnavailable => "temporarily_unavailable",
120            Self::RegistrationRequired => "registration_required",
121            Self::UnsupportedPromptValue => "unsupported_prompt_value",
122            Self::UserSelectionRequired => "user_selection_required",
123        };
124        f.write_str(s)
125    }
126}
127
128impl std::str::FromStr for OidcErrorCode {
129    type Err = AuthError;
130
131    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
132        match s {
133            "invalid_request" => Ok(Self::InvalidRequest),
134            "invalid_client" => Ok(Self::InvalidClient),
135            "invalid_grant" => Ok(Self::InvalidGrant),
136            "unauthorized_client" => Ok(Self::UnauthorizedClient),
137            "unsupported_grant_type" => Ok(Self::UnsupportedGrantType),
138            "invalid_scope" => Ok(Self::InvalidScope),
139            "interaction_required" => Ok(Self::InteractionRequired),
140            "login_required" => Ok(Self::LoginRequired),
141            "account_selection_required" => Ok(Self::AccountSelectionRequired),
142            "consent_required" => Ok(Self::ConsentRequired),
143            "invalid_request_uri" => Ok(Self::InvalidRequestUri),
144            "invalid_request_object" => Ok(Self::InvalidRequestObject),
145            "request_not_supported" => Ok(Self::RequestNotSupported),
146            "request_uri_not_supported" => Ok(Self::RequestUriNotSupported),
147            "registration_not_supported" => Ok(Self::RegistrationNotSupported),
148            "unmet_authentication_requirements" => Ok(Self::UnmetAuthenticationRequirements),
149            "unmet_authentication_context_requirements" => Ok(Self::UnmetAuthenticationContextRequirements),
150            "session_selection_required" => Ok(Self::SessionSelectionRequired),
151            "authentication_method_required" => Ok(Self::AuthenticationMethodRequired),
152            "insufficient_identity_assurance" => Ok(Self::InsufficientIdentityAssurance),
153            "temporarily_unavailable" => Ok(Self::TemporarilyUnavailable),
154            "registration_required" => Ok(Self::RegistrationRequired),
155            "unsupported_prompt_value" => Ok(Self::UnsupportedPromptValue),
156            "user_selection_required" => Ok(Self::UserSelectionRequired),
157            other => Err(AuthError::validation(format!("Unknown OIDC error code: {other}"))),
158        }
159    }
160}
161
162/// OpenID Connect error response
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct OidcErrorResponse {
165    /// The error code
166    pub error: OidcErrorCode,
167    /// Human-readable error description
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub error_description: Option<String>,
170    /// URI to error documentation
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub error_uri: Option<String>,
173    /// State parameter from the request
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub state: Option<String>,
176    /// Additional error details
177    #[serde(flatten)]
178    pub additional_details: HashMap<String, serde_json::Value>,
179}
180
181impl OidcErrorResponse {
182    /// Start building an error response for the given error code.
183    ///
184    /// # Example
185    ///
186    /// ```rust,no_run
187    /// use auth_framework::server::oidc::oidc_error_extensions::{OidcErrorCode, OidcErrorResponse};
188    ///
189    /// let resp = OidcErrorResponse::new(OidcErrorCode::LoginRequired)
190    ///     .description("Session expired")
191    ///     .state("abc123")
192    ///     .build();
193    /// ```
194    pub fn new(error: OidcErrorCode) -> OidcErrorResponseBuilder {
195        OidcErrorResponseBuilder {
196            inner: OidcErrorResponse {
197                error,
198                error_description: None,
199                error_uri: None,
200                state: None,
201                additional_details: HashMap::new(),
202            },
203        }
204    }
205}
206
207/// Fluent builder for [`OidcErrorResponse`].
208pub struct OidcErrorResponseBuilder {
209    inner: OidcErrorResponse,
210}
211
212impl OidcErrorResponseBuilder {
213    /// Set a human-readable error description.
214    pub fn description(mut self, desc: impl Into<String>) -> Self {
215        self.inner.error_description = Some(desc.into());
216        self
217    }
218
219    /// Set the error documentation URI.
220    pub fn error_uri(mut self, uri: impl Into<String>) -> Self {
221        self.inner.error_uri = Some(uri.into());
222        self
223    }
224
225    /// Set the OAuth `state` parameter.
226    pub fn state(mut self, state: impl Into<String>) -> Self {
227        self.inner.state = Some(state.into());
228        self
229    }
230
231    /// Add a single additional detail.
232    pub fn detail(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
233        self.inner.additional_details.insert(key.into(), value);
234        self
235    }
236
237    /// Set all additional details at once (replaces any previously added).
238    pub fn details(mut self, details: HashMap<String, serde_json::Value>) -> Self {
239        self.inner.additional_details = details;
240        self
241    }
242
243    /// Consume the builder and return the [`OidcErrorResponse`].
244    pub fn build(self) -> OidcErrorResponse {
245        self.inner
246    }
247}
248
249/// Authentication requirements details
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct AuthenticationRequirements {
252    /// Required authentication context class references
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub acr_values: Option<Vec<String>>,
255    /// Required authentication methods references
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub amr_values: Option<Vec<String>>,
258    /// Maximum authentication age
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub max_age: Option<u64>,
261    /// Required identity assurance level
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub identity_assurance_level: Option<String>,
264}
265
266/// Error handling manager for OpenID Connect
267#[derive(Debug, Clone)]
268pub struct OidcErrorManager {
269    /// Base error documentation URI
270    error_base_uri: String,
271    /// Custom error mappings
272    custom_error_mappings: HashMap<String, OidcErrorCode>,
273}
274
275impl Default for OidcErrorManager {
276    fn default() -> Self {
277        Self {
278            error_base_uri: "https://openid.net/specs/openid-connect-core-1_0.html#AuthError"
279                .to_string(),
280            custom_error_mappings: HashMap::new(),
281        }
282    }
283}
284
285impl OidcErrorCode {
286    /// Get standard error description for error code
287    pub fn get_description(&self) -> &'static str {
288        match self {
289            Self::InvalidRequest => {
290                "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
291            }
292            Self::InvalidClient => {
293                "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method)."
294            }
295            Self::InvalidGrant => {
296                "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."
297            }
298            Self::UnauthorizedClient => {
299                "The authenticated client is not authorized to use this authorization grant type."
300            }
301            Self::UnsupportedGrantType => {
302                "The authorization grant type is not supported by the authorization server."
303            }
304            Self::InvalidScope => "The requested scope is invalid, unknown, or malformed.",
305
306            Self::InteractionRequired => {
307                "The authorization server requires end-user interaction of some form to proceed."
308            }
309            Self::LoginRequired => "The authorization server requires end-user authentication.",
310            Self::AccountSelectionRequired => {
311                "The end-user is required to select a session at the authorization server."
312            }
313            Self::ConsentRequired => "The authorization server requires end-user consent.",
314            Self::InvalidRequestUri => {
315                "The request_uri in the authorization request returns an error or contains invalid data."
316            }
317            Self::InvalidRequestObject => {
318                "The request parameter contains an invalid request object."
319            }
320            Self::RequestNotSupported => {
321                "The authorization server does not support use of the request parameter."
322            }
323            Self::RequestUriNotSupported => {
324                "The authorization server does not support use of the request_uri parameter."
325            }
326            Self::RegistrationNotSupported => {
327                "The authorization server does not support use of the registration parameter."
328            }
329
330            // Extended error codes
331            Self::UnmetAuthenticationRequirements => {
332                "The authentication performed does not meet the authentication requirements specified in the request."
333            }
334            Self::UnmetAuthenticationContextRequirements => {
335                "The requested authentication context class reference values were not satisfied by the performed authentication."
336            }
337            Self::SessionSelectionRequired => {
338                "Multiple active sessions exist, and the end-user must select which session to use."
339            }
340            Self::AuthenticationMethodRequired => {
341                "The authorization server requires the end-user to authenticate using a specific authentication method."
342            }
343            Self::InsufficientIdentityAssurance => {
344                "The level of identity assurance achieved does not meet the requirements for this request."
345            }
346            Self::TemporarilyUnavailable => {
347                "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server."
348            }
349            Self::RegistrationRequired => {
350                "The end-user must complete a registration process before authentication can proceed."
351            }
352            Self::UnsupportedPromptValue => {
353                "The authorization server does not support the requested prompt value."
354            }
355            Self::UserSelectionRequired => {
356                "Multiple users match the provided identification, and selection is required."
357            }
358        }
359    }
360
361    /// Check if this error code requires user interaction
362    pub fn requires_interaction(&self) -> bool {
363        matches!(
364            self,
365            Self::InteractionRequired
366                | Self::LoginRequired
367                | Self::AccountSelectionRequired
368                | Self::ConsentRequired
369                | Self::SessionSelectionRequired
370                | Self::AuthenticationMethodRequired
371                | Self::RegistrationRequired
372                | Self::UserSelectionRequired
373        )
374    }
375
376    /// Check if this error code indicates an authentication issue
377    pub fn is_authentication_error(&self) -> bool {
378        matches!(
379            self,
380            Self::LoginRequired
381                | Self::UnmetAuthenticationRequirements
382                | Self::UnmetAuthenticationContextRequirements
383                | Self::AuthenticationMethodRequired
384                | Self::InsufficientIdentityAssurance
385        )
386    }
387}
388
389impl OidcErrorManager {
390    /// Create new error manager
391    pub fn new(error_base_uri: String) -> Self {
392        Self {
393            error_base_uri,
394            custom_error_mappings: HashMap::new(),
395        }
396    }
397
398    /// Create error response for unmet authentication requirements
399    pub fn create_unmet_auth_requirements_error(
400        &self,
401        requirements: AuthenticationRequirements,
402        state: Option<String>,
403    ) -> OidcErrorResponse {
404        let mut builder = OidcErrorResponse::new(OidcErrorCode::UnmetAuthenticationRequirements)
405            .description(
406                OidcErrorCode::UnmetAuthenticationRequirements
407                    .get_description()
408                    .to_string(),
409            )
410            .error_uri(format!(
411                "{}#UnmetAuthenticationRequirements",
412                self.error_base_uri
413            ));
414
415        if let Some(s) = state {
416            builder = builder.state(s);
417        }
418
419        if let Some(acr_values) = &requirements.acr_values {
420            builder = builder.detail(
421                "required_acr_values",
422                serde_json::to_value(acr_values).unwrap_or_default(),
423            );
424        }
425
426        if let Some(amr_values) = &requirements.amr_values {
427            builder = builder.detail(
428                "required_amr_values",
429                serde_json::to_value(amr_values).unwrap_or_default(),
430            );
431        }
432
433        if let Some(max_age) = requirements.max_age {
434            builder = builder.detail(
435                "max_age",
436                serde_json::Value::Number(serde_json::Number::from(max_age)),
437            );
438        }
439
440        builder.build()
441    }
442
443    /// Create error response for insufficient ACR
444    pub fn create_insufficient_acr_error(
445        &self,
446        required_acr: Vec<String>,
447        achieved_acr: Option<String>,
448        state: Option<String>,
449    ) -> OidcErrorResponse {
450        let mut builder =
451            OidcErrorResponse::new(OidcErrorCode::UnmetAuthenticationContextRequirements)
452                .description(
453                    OidcErrorCode::UnmetAuthenticationContextRequirements
454                        .get_description()
455                        .to_string(),
456                )
457                .error_uri(format!("{}#ACRRequirements", self.error_base_uri))
458                .detail(
459                    "required_acr_values",
460                    serde_json::to_value(required_acr).unwrap_or_default(),
461                );
462
463        if let Some(acr) = achieved_acr {
464            builder = builder.detail("achieved_acr", serde_json::Value::String(acr));
465        }
466
467        if let Some(s) = state {
468            builder = builder.state(s);
469        }
470
471        builder.build()
472    }
473
474    /// Create generic error response
475    pub fn create_error_response(
476        &self,
477        error_code: OidcErrorCode,
478        custom_description: Option<String>,
479        state: Option<String>,
480        additional_details: HashMap<String, serde_json::Value>,
481    ) -> OidcErrorResponse {
482        let mut builder = OidcErrorResponse::new(error_code.clone())
483            .description(
484                custom_description.unwrap_or_else(|| error_code.get_description().to_string()),
485            )
486            .error_uri(format!("{}#{:?}", self.error_base_uri, error_code))
487            .details(additional_details);
488
489        if let Some(s) = state {
490            builder = builder.state(s);
491        }
492
493        builder.build()
494    }
495
496    /// Add custom error mapping
497    pub fn add_custom_error_mapping(&mut self, identifier: String, error_code: OidcErrorCode) {
498        self.custom_error_mappings.insert(identifier, error_code);
499    }
500
501    /// Remove custom error mapping
502    pub fn remove_custom_error_mapping(&mut self, identifier: &str) -> Option<OidcErrorCode> {
503        self.custom_error_mappings.remove(identifier)
504    }
505
506    /// Get error code from string identifier (checks custom mappings first, then standard codes)
507    pub fn resolve_error_code(&self, identifier: &str) -> Option<OidcErrorCode> {
508        // Check custom mappings first
509        if let Some(error_code) = self.custom_error_mappings.get(identifier) {
510            return Some(error_code.clone());
511        }
512
513        // Delegate to FromStr for standard error codes
514        identifier.parse::<OidcErrorCode>().ok()
515    }
516
517    /// Create error response from string identifier
518    pub fn create_error_response_from_identifier(
519        &self,
520        error_identifier: &str,
521        custom_description: Option<String>,
522        state: Option<String>,
523        additional_details: HashMap<String, serde_json::Value>,
524    ) -> Result<OidcErrorResponse> {
525        match self.resolve_error_code(error_identifier) {
526            Some(error_code) => Ok(self.create_error_response(
527                error_code,
528                custom_description,
529                state,
530                additional_details,
531            )),
532            None => Err(AuthError::validation(format!(
533                "Unknown error code identifier: {}",
534                error_identifier
535            ))),
536        }
537    }
538
539    /// Get all custom error mappings
540    pub fn get_custom_mappings(&self) -> &HashMap<String, OidcErrorCode> {
541        &self.custom_error_mappings
542    }
543
544    /// Clear all custom error mappings
545    pub fn clear_custom_mappings(&mut self) {
546        self.custom_error_mappings.clear();
547    }
548
549    /// Check if custom mapping exists
550    pub fn has_custom_mapping(&self, identifier: &str) -> bool {
551        self.custom_error_mappings.contains_key(identifier)
552    }
553
554    /// Validate authentication requirements against performed authentication
555    pub fn validate_authentication_requirements(
556        &self,
557        requirements: &AuthenticationRequirements,
558        performed_acr: Option<&str>,
559        performed_amr: Option<&[String]>,
560        auth_time: Option<u64>,
561        current_time: u64,
562    ) -> Result<()> {
563        // Check ACR requirements
564        if let Some(required_acr) = &requirements.acr_values {
565            match performed_acr {
566                Some(acr) => {
567                    if !required_acr.contains(&acr.to_string()) {
568                        return Err(AuthError::validation(
569                            "Authentication context class requirements not met",
570                        ));
571                    }
572                }
573                None => {
574                    return Err(AuthError::validation(
575                        "No authentication context class provided",
576                    ));
577                }
578            }
579        }
580
581        // Check AMR requirements
582        if let Some(required_amr) = &requirements.amr_values {
583            match performed_amr {
584                Some(amr) => {
585                    for required in required_amr {
586                        if !amr.contains(required) {
587                            return Err(AuthError::validation(
588                                "Authentication method requirements not met",
589                            ));
590                        }
591                    }
592                }
593                None => {
594                    return Err(AuthError::validation("No authentication methods provided"));
595                }
596            }
597        }
598
599        // Check max_age requirement
600        if let Some(max_age) = requirements.max_age {
601            if let Some(auth_time) = auth_time {
602                if current_time - auth_time > max_age {
603                    return Err(AuthError::validation(
604                        "Authentication is too old (exceeds max_age)",
605                    ));
606                }
607            } else {
608                return Err(AuthError::validation(
609                    "Authentication time not available for max_age validation",
610                ));
611            }
612        }
613
614        Ok(())
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621
622    #[test]
623    fn test_error_code_descriptions() {
624        assert!(
625            !OidcErrorCode::UnmetAuthenticationRequirements
626                .get_description()
627                .is_empty()
628        );
629        assert!(OidcErrorCode::LoginRequired.requires_interaction());
630        assert!(OidcErrorCode::UnmetAuthenticationRequirements.is_authentication_error());
631    }
632
633    #[test]
634    fn test_oidc_error_code_display_roundtrip() {
635        // Every variant should round-trip through Display → FromStr
636        let codes = vec![
637            OidcErrorCode::InvalidRequest,
638            OidcErrorCode::InvalidClient,
639            OidcErrorCode::InvalidGrant,
640            OidcErrorCode::UnauthorizedClient,
641            OidcErrorCode::UnsupportedGrantType,
642            OidcErrorCode::InvalidScope,
643            OidcErrorCode::InteractionRequired,
644            OidcErrorCode::LoginRequired,
645            OidcErrorCode::AccountSelectionRequired,
646            OidcErrorCode::ConsentRequired,
647            OidcErrorCode::InvalidRequestUri,
648            OidcErrorCode::InvalidRequestObject,
649            OidcErrorCode::RequestNotSupported,
650            OidcErrorCode::RequestUriNotSupported,
651            OidcErrorCode::RegistrationNotSupported,
652            OidcErrorCode::UnmetAuthenticationRequirements,
653            OidcErrorCode::UnmetAuthenticationContextRequirements,
654            OidcErrorCode::SessionSelectionRequired,
655            OidcErrorCode::AuthenticationMethodRequired,
656            OidcErrorCode::InsufficientIdentityAssurance,
657            OidcErrorCode::TemporarilyUnavailable,
658            OidcErrorCode::RegistrationRequired,
659            OidcErrorCode::UnsupportedPromptValue,
660            OidcErrorCode::UserSelectionRequired,
661        ];
662
663        for code in codes {
664            let s = code.to_string();
665            let parsed: OidcErrorCode = s.parse().unwrap();
666            assert_eq!(parsed, code);
667        }
668    }
669
670    #[test]
671    fn test_oidc_error_code_from_str_invalid() {
672        let result = "not_a_real_error".parse::<OidcErrorCode>();
673        assert!(result.is_err());
674    }
675
676    #[test]
677    fn test_oidc_error_response_builder() {
678        let resp = OidcErrorResponse::new(OidcErrorCode::LoginRequired)
679            .description("Session expired")
680            .error_uri("https://example.com/errors#login")
681            .state("abc123")
682            .detail("session_id", serde_json::json!("sess-42"))
683            .build();
684
685        assert_eq!(resp.error, OidcErrorCode::LoginRequired);
686        assert_eq!(resp.error_description.as_deref(), Some("Session expired"));
687        assert_eq!(
688            resp.error_uri.as_deref(),
689            Some("https://example.com/errors#login")
690        );
691        assert_eq!(resp.state.as_deref(), Some("abc123"));
692        assert_eq!(
693            resp.additional_details.get("session_id"),
694            Some(&serde_json::json!("sess-42"))
695        );
696    }
697
698    #[test]
699    fn test_unmet_auth_requirements_error() {
700        let manager = OidcErrorManager::default();
701        let requirements = AuthenticationRequirements {
702            acr_values: Some(vec!["urn:mace:incommon:iap:silver".to_string()]),
703            amr_values: Some(vec!["pwd".to_string(), "mfa".to_string()]),
704            max_age: Some(3600),
705            identity_assurance_level: None,
706        };
707
708        let error = manager
709            .create_unmet_auth_requirements_error(requirements, Some("state123".to_string()));
710
711        assert_eq!(error.error, OidcErrorCode::UnmetAuthenticationRequirements);
712        assert!(error.error_description.is_some());
713        assert_eq!(error.state.as_ref().unwrap(), "state123");
714        assert!(error.additional_details.contains_key("required_acr_values"));
715        assert!(error.additional_details.contains_key("required_amr_values"));
716    }
717
718    #[test]
719    fn test_custom_error_mappings() {
720        let mut manager = OidcErrorManager::default();
721
722        // Test adding custom error mapping
723        manager.add_custom_error_mapping(
724            "custom_validation_failed".to_string(),
725            OidcErrorCode::InvalidRequest,
726        );
727
728        // Test resolving custom error code
729        let resolved = manager.resolve_error_code("custom_validation_failed");
730        assert_eq!(resolved, Some(OidcErrorCode::InvalidRequest));
731
732        // Test resolving standard error code
733        let standard = manager.resolve_error_code("login_required");
734        assert_eq!(standard, Some(OidcErrorCode::LoginRequired));
735
736        // Test resolving unknown error code
737        let unknown = manager.resolve_error_code("nonexistent_error");
738        assert_eq!(unknown, None);
739
740        // Test has_custom_mapping
741        assert!(manager.has_custom_mapping("custom_validation_failed"));
742        assert!(!manager.has_custom_mapping("login_required"));
743
744        // Test creating error response from identifier
745        let error_response = manager
746            .create_error_response_from_identifier(
747                "custom_validation_failed",
748                Some("Custom validation error".to_string()),
749                Some("state123".to_string()),
750                HashMap::new(),
751            )
752            .unwrap();
753
754        assert_eq!(error_response.error, OidcErrorCode::InvalidRequest);
755        assert_eq!(error_response.state.as_ref().unwrap(), "state123");
756
757        // Test remove custom mapping
758        let removed = manager.remove_custom_error_mapping("custom_validation_failed");
759        assert_eq!(removed, Some(OidcErrorCode::InvalidRequest));
760        assert!(!manager.has_custom_mapping("custom_validation_failed"));
761
762        // Test clear all mappings
763        manager.add_custom_error_mapping("test1".to_string(), OidcErrorCode::InvalidScope);
764        manager.add_custom_error_mapping("test2".to_string(), OidcErrorCode::ConsentRequired);
765        assert_eq!(manager.get_custom_mappings().len(), 2);
766
767        manager.clear_custom_mappings();
768        assert_eq!(manager.get_custom_mappings().len(), 0);
769    }
770
771    #[test]
772    fn test_error_response_from_unknown_identifier() {
773        let manager = OidcErrorManager::default();
774
775        let result = manager.create_error_response_from_identifier(
776            "unknown_error_code",
777            None,
778            None,
779            HashMap::new(),
780        );
781
782        assert!(result.is_err());
783        assert!(
784            result
785                .unwrap_err()
786                .to_string()
787                .contains("Unknown error code identifier")
788        );
789    }
790
791    #[test]
792    fn test_custom_error_mappings_real_world_scenario() {
793        let mut manager = OidcErrorManager::default();
794
795        // Add domain-specific error mappings for a banking application
796        manager.add_custom_error_mapping(
797            "account_frozen".to_string(),
798            OidcErrorCode::AuthenticationMethodRequired,
799        );
800        manager.add_custom_error_mapping(
801            "kyc_verification_required".to_string(),
802            OidcErrorCode::InsufficientIdentityAssurance,
803        );
804        manager.add_custom_error_mapping(
805            "payment_limit_exceeded".to_string(),
806            OidcErrorCode::ConsentRequired,
807        );
808
809        // Demonstrate custom error response creation
810        let mut additional_details = HashMap::new();
811        additional_details.insert(
812            "account_id".to_string(),
813            serde_json::Value::String("acc-12345".to_string()),
814        );
815        additional_details.insert(
816            "freeze_reason".to_string(),
817            serde_json::Value::String("Suspicious activity detected".to_string()),
818        );
819
820        let error_response = manager
821            .create_error_response_from_identifier(
822                "account_frozen",
823                Some("Account authentication required due to security freeze".to_string()),
824                Some("banking-session-456".to_string()),
825                additional_details,
826            )
827            .unwrap();
828
829        assert_eq!(
830            error_response.error,
831            OidcErrorCode::AuthenticationMethodRequired
832        );
833        assert_eq!(
834            error_response.error_description.as_ref().unwrap(),
835            "Account authentication required due to security freeze"
836        );
837        assert_eq!(
838            error_response.state.as_ref().unwrap(),
839            "banking-session-456"
840        );
841        assert!(error_response.additional_details.contains_key("account_id"));
842        assert!(
843            error_response
844                .additional_details
845                .contains_key("freeze_reason")
846        );
847
848        // Verify custom mappings take precedence over standard ones
849        manager.add_custom_error_mapping(
850            "login_required".to_string(),
851            OidcErrorCode::RegistrationRequired, // Override standard behavior
852        );
853
854        let overridden_response = manager
855            .create_error_response_from_identifier(
856                "login_required",
857                Some("User registration required before login".to_string()),
858                None,
859                HashMap::new(),
860            )
861            .unwrap();
862
863        assert_eq!(
864            overridden_response.error,
865            OidcErrorCode::RegistrationRequired
866        );
867
868        // Verify management functions
869        assert_eq!(manager.get_custom_mappings().len(), 4);
870        assert!(manager.has_custom_mapping("account_frozen"));
871        assert!(!manager.has_custom_mapping("nonexistent_mapping"));
872
873        // Clean up specific mapping
874        let removed = manager.remove_custom_error_mapping("account_frozen");
875        assert_eq!(removed, Some(OidcErrorCode::AuthenticationMethodRequired));
876        assert!(!manager.has_custom_mapping("account_frozen"));
877
878        // Test clear all
879        manager.clear_custom_mappings();
880        assert_eq!(manager.get_custom_mappings().len(), 0);
881    }
882
883    #[test]
884    fn test_standard_error_code_resolution() {
885        let manager = OidcErrorManager::default();
886
887        // Test all standard error codes
888        assert_eq!(
889            manager.resolve_error_code("invalid_request"),
890            Some(OidcErrorCode::InvalidRequest)
891        );
892        assert_eq!(
893            manager.resolve_error_code("unmet_authentication_requirements"),
894            Some(OidcErrorCode::UnmetAuthenticationRequirements)
895        );
896        assert_eq!(
897            manager.resolve_error_code("session_selection_required"),
898            Some(OidcErrorCode::SessionSelectionRequired)
899        );
900
901        // Custom mappings take precedence over standard codes
902        let mut manager = OidcErrorManager::default();
903        manager.add_custom_error_mapping(
904            "login_required".to_string(),
905            OidcErrorCode::ConsentRequired, // Override standard mapping
906        );
907
908        assert_eq!(
909            manager.resolve_error_code("login_required"),
910            Some(OidcErrorCode::ConsentRequired) // Should return custom mapping
911        );
912    }
913}