Skip to main content

auth_framework/server/oidc/
core.rs

1//! OpenID Connect Provider Implementation (OIDC 1.0)
2//!
3//! This module implements a complete OpenID Connect Provider based on:
4//! - OpenID Connect Core 1.0 specification
5//! - OpenID Connect Discovery 1.0
6//! - OpenID Connect Dynamic Client Registration 1.0
7//! - JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants
8
9use crate::errors::{AuthError, Result};
10use crate::oauth2_server::OAuth2Server;
11use crate::server::core::client_registry::ClientRegistry;
12use crate::storage::AuthStorage;
13use crate::tokens::TokenManager;
14use jsonwebtoken::{Algorithm, Header};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::HashMap;
18use std::fmt;
19use std::sync::Arc;
20use std::time::{Duration, SystemTime, UNIX_EPOCH};
21
22/// OpenID Connect Provider configuration
23#[derive(Debug, Clone)]
24pub struct OidcConfig {
25    /// Issuer identifier (must be HTTPS URL)
26    pub issuer: String,
27
28    /// OAuth 2.0 base configuration
29    pub oauth2_config: crate::oauth2_server::OAuth2Config,
30
31    /// JWK Set URI
32    pub jwks_uri: String,
33
34    /// UserInfo endpoint URI
35    pub userinfo_endpoint: String,
36
37    /// Supported response types
38    pub response_types_supported: Vec<String>,
39
40    /// Supported subject identifier types
41    pub subject_types_supported: Vec<SubjectType>,
42
43    /// Supported ID token signing algorithms
44    pub id_token_signing_alg_values_supported: Vec<Algorithm>,
45
46    /// Supported scopes
47    pub scopes_supported: Vec<String>,
48
49    /// Supported claims
50    pub claims_supported: Vec<String>,
51
52    /// Whether claims parameter is supported
53    pub claims_parameter_supported: bool,
54
55    /// Whether request parameter is supported
56    pub request_parameter_supported: bool,
57
58    /// Whether request_uri parameter is supported
59    pub request_uri_parameter_supported: bool,
60
61    /// ID token expiration time
62    pub id_token_expiry: Duration,
63
64    /// Maximum age for authentication
65    pub max_age_supported: Option<Duration>,
66}
67
68impl Default for OidcConfig {
69    fn default() -> Self {
70        Self {
71            issuer: "https://auth.example.com".to_string(),
72            oauth2_config: crate::oauth2_server::OAuth2Config::default(),
73            jwks_uri: "https://auth.example.com/.well-known/jwks.json".to_string(),
74            userinfo_endpoint: "https://auth.example.com/oidc/userinfo".to_string(),
75            response_types_supported: vec![
76                "code".to_string(),
77                "id_token".to_string(),
78                "id_token token".to_string(),
79                "code id_token".to_string(),
80                "code token".to_string(),
81                "code id_token token".to_string(),
82            ],
83            subject_types_supported: vec![SubjectType::Public],
84            id_token_signing_alg_values_supported: vec![
85                Algorithm::RS256,
86                Algorithm::ES256,
87                Algorithm::HS256,
88            ],
89            scopes_supported: vec![
90                "openid".to_string(),
91                "profile".to_string(),
92                "email".to_string(),
93                "address".to_string(),
94                "phone".to_string(),
95                "offline_access".to_string(),
96            ],
97            claims_supported: vec![
98                "sub".to_string(),
99                "name".to_string(),
100                "given_name".to_string(),
101                "family_name".to_string(),
102                "middle_name".to_string(),
103                "nickname".to_string(),
104                "preferred_username".to_string(),
105                "profile".to_string(),
106                "picture".to_string(),
107                "website".to_string(),
108                "email".to_string(),
109                "email_verified".to_string(),
110                "gender".to_string(),
111                "birthdate".to_string(),
112                "zoneinfo".to_string(),
113                "locale".to_string(),
114                "phone_number".to_string(),
115                "phone_number_verified".to_string(),
116                "address".to_string(),
117                "updated_at".to_string(),
118            ],
119            claims_parameter_supported: true,
120            request_parameter_supported: true,
121            request_uri_parameter_supported: true,
122            id_token_expiry: Duration::from_secs(3600), // 1 hour
123            max_age_supported: Some(Duration::from_secs(86400)), // 24 hours
124        }
125    }
126}
127
128impl OidcConfig {
129    /// Create a new builder for `OidcConfig`, starting from defaults.
130    ///
131    /// # Example
132    ///
133    /// ```rust,no_run
134    /// use auth_framework::server::oidc::core::OidcConfig;
135    /// use std::time::Duration;
136    ///
137    /// let config = OidcConfig::builder()
138    ///     .issuer("https://id.example.com")
139    ///     .id_token_expiry(Duration::from_secs(1800))
140    ///     .build();
141    /// ```
142    pub fn builder() -> OidcConfigBuilder {
143        OidcConfigBuilder::default()
144    }
145}
146
147/// A builder for [`OidcConfig`].
148///
149/// Obtain via [`OidcConfig::builder()`]. All fields start with sensible
150/// defaults; override only what you need.
151#[derive(Debug, Clone)]
152pub struct OidcConfigBuilder {
153    config: OidcConfig,
154}
155
156impl Default for OidcConfigBuilder {
157    fn default() -> Self {
158        Self {
159            config: OidcConfig::default(),
160        }
161    }
162}
163
164impl OidcConfigBuilder {
165    /// Set the issuer identifier (must be an HTTPS URL in production).
166    ///
167    /// # Example
168    /// ```rust,ignore
169    /// let config = OidcConfigBuilder::default()
170    ///     .issuer("https://auth.example.com")
171    ///     .build();
172    /// ```
173    pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
174        self.config.issuer = issuer.into();
175        self
176    }
177
178    /// Set the underlying OAuth 2.0 configuration.
179    ///
180    /// # Example
181    /// ```rust,ignore
182    /// let config = OidcConfigBuilder::default()
183    ///     .oauth2_config(OAuth2Config::default())
184    ///     .build();
185    /// ```
186    pub fn oauth2_config(mut self, config: crate::oauth2_server::OAuth2Config) -> Self {
187        self.config.oauth2_config = config;
188        self
189    }
190
191    /// Set the JWK Set URI.
192    ///
193    /// # Example
194    /// ```rust,ignore
195    /// let config = OidcConfigBuilder::default()
196    ///     .jwks_uri("https://auth.example.com/.well-known/jwks.json")
197    ///     .build();
198    /// ```
199    pub fn jwks_uri(mut self, uri: impl Into<String>) -> Self {
200        self.config.jwks_uri = uri.into();
201        self
202    }
203
204    /// Set the UserInfo endpoint URI.
205    ///
206    /// # Example
207    /// ```rust,ignore
208    /// let config = OidcConfigBuilder::default()
209    ///     .userinfo_endpoint("https://auth.example.com/userinfo")
210    ///     .build();
211    /// ```
212    pub fn userinfo_endpoint(mut self, uri: impl Into<String>) -> Self {
213        self.config.userinfo_endpoint = uri.into();
214        self
215    }
216
217    /// Set the supported response types.
218    ///
219    /// # Example
220    /// ```rust,ignore
221    /// let config = OidcConfigBuilder::default()
222    ///     .response_types_supported(vec!["code".into(), "id_token".into()])
223    ///     .build();
224    /// ```
225    pub fn response_types_supported(mut self, types: Vec<String>) -> Self {
226        self.config.response_types_supported = types;
227        self
228    }
229
230    /// Set the supported subject identifier types.
231    ///
232    /// # Example
233    /// ```rust,ignore
234    /// let config = OidcConfigBuilder::default()
235    ///     .subject_types_supported(vec![SubjectType::Public])
236    ///     .build();
237    /// ```
238    pub fn subject_types_supported(mut self, types: Vec<SubjectType>) -> Self {
239        self.config.subject_types_supported = types;
240        self
241    }
242
243    /// Set the supported ID token signing algorithms.
244    ///
245    /// # Example
246    /// ```rust,ignore
247    /// let config = OidcConfigBuilder::default()
248    ///     .id_token_signing_alg_values_supported(vec![Algorithm::RS256])
249    ///     .build();
250    /// ```
251    pub fn id_token_signing_alg_values_supported(mut self, algs: Vec<Algorithm>) -> Self {
252        self.config.id_token_signing_alg_values_supported = algs;
253        self
254    }
255
256    /// Set the supported scopes.
257    ///
258    /// # Example
259    /// ```rust,ignore
260    /// let config = OidcConfigBuilder::default()
261    ///     .scopes_supported(vec!["openid".into(), "profile".into(), "email".into()])
262    ///     .build();
263    /// ```
264    pub fn scopes_supported(mut self, scopes: Vec<String>) -> Self {
265        self.config.scopes_supported = scopes;
266        self
267    }
268
269    /// Set the supported claims.
270    ///
271    /// # Example
272    /// ```rust,ignore
273    /// let config = OidcConfigBuilder::default()
274    ///     .claims_supported(vec!["sub".into(), "name".into(), "email".into()])
275    ///     .build();
276    /// ```
277    pub fn claims_supported(mut self, claims: Vec<String>) -> Self {
278        self.config.claims_supported = claims;
279        self
280    }
281
282    /// Set whether the claims parameter is supported.
283    ///
284    /// # Example
285    /// ```rust,ignore
286    /// let config = OidcConfigBuilder::default()
287    ///     .claims_parameter_supported(true)
288    ///     .build();
289    /// ```
290    pub fn claims_parameter_supported(mut self, supported: bool) -> Self {
291        self.config.claims_parameter_supported = supported;
292        self
293    }
294
295    /// Set whether the request parameter is supported.
296    ///
297    /// # Example
298    /// ```rust,ignore
299    /// let config = OidcConfigBuilder::default()
300    ///     .request_parameter_supported(true)
301    ///     .build();
302    /// ```
303    pub fn request_parameter_supported(mut self, supported: bool) -> Self {
304        self.config.request_parameter_supported = supported;
305        self
306    }
307
308    /// Set whether the request_uri parameter is supported.
309    ///
310    /// # Example
311    /// ```rust,ignore
312    /// let config = OidcConfigBuilder::default()
313    ///     .request_uri_parameter_supported(false)
314    ///     .build();
315    /// ```
316    pub fn request_uri_parameter_supported(mut self, supported: bool) -> Self {
317        self.config.request_uri_parameter_supported = supported;
318        self
319    }
320
321    /// Set the ID token expiration duration.
322    ///
323    /// # Example
324    /// ```rust,ignore
325    /// use std::time::Duration;
326    /// let config = OidcConfigBuilder::default()
327    ///     .id_token_expiry(Duration::from_secs(3600))
328    ///     .build();
329    /// ```
330    pub fn id_token_expiry(mut self, expiry: Duration) -> Self {
331        self.config.id_token_expiry = expiry;
332        self
333    }
334
335    /// Set the maximum age for authentication.
336    ///
337    /// # Example
338    /// ```rust,ignore
339    /// use std::time::Duration;
340    /// let config = OidcConfigBuilder::default()
341    ///     .max_age_supported(Duration::from_secs(86400))
342    ///     .build();
343    /// ```
344    pub fn max_age_supported(mut self, max_age: Duration) -> Self {
345        self.config.max_age_supported = Some(max_age);
346        self
347    }
348
349    /// Build the [`OidcConfig`].
350    ///
351    /// # Example
352    /// ```rust,ignore
353    /// let config = OidcConfigBuilder::default()
354    ///     .issuer("https://auth.example.com")
355    ///     .scopes_supported(vec!["openid".into(), "profile".into()])
356    ///     .id_token_expiry(Duration::from_secs(3600))
357    ///     .build();
358    /// ```
359    pub fn build(self) -> OidcConfig {
360        self.config
361    }
362}
363
364/// Subject identifier types
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "lowercase")]
367pub enum SubjectType {
368    /// Public subject identifier
369    Public,
370    /// Pairwise subject identifier
371    Pairwise,
372}
373
374/// OpenID Connect Provider
375pub struct OidcProvider<S: AuthStorage + ?Sized> {
376    config: OidcConfig,
377    oauth2_server: OAuth2Server,
378    token_manager: Arc<TokenManager>,
379    storage: Arc<S>,
380    client_registry: Option<Arc<ClientRegistry>>,
381}
382
383impl<S: AuthStorage + ?Sized> fmt::Debug for OidcProvider<S> {
384    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385        f.debug_struct("OidcProvider")
386            .field("config", &self.config)
387            .field("oauth2_server", &"<OAuth2Server>")
388            .field("token_manager", &"<TokenManager>")
389            .field("storage", &"<AuthStorage>")
390            .field("client_registry", &self.client_registry.is_some())
391            .finish()
392    }
393}
394
395impl<S: ?Sized + AuthStorage> OidcProvider<S> {
396    /// Create a new OIDC Provider
397    pub async fn new(
398        config: OidcConfig,
399        token_manager: Arc<TokenManager>,
400        storage: Arc<S>,
401    ) -> Result<Self> {
402        let oauth2_server =
403            OAuth2Server::new(config.oauth2_config.clone(), token_manager.clone()).await?;
404
405        Ok(Self {
406            config,
407            oauth2_server,
408            token_manager,
409            storage,
410            client_registry: None,
411        })
412    }
413
414    /// Get the underlying OAuth 2.0 server
415    pub fn oauth2_server(&self) -> &OAuth2Server {
416        &self.oauth2_server
417    }
418
419    /// Set the client registry for validation
420    pub fn set_client_registry(&mut self, client_registry: Arc<ClientRegistry>) {
421        self.client_registry = Some(client_registry);
422    }
423
424    /// Get OIDC configuration
425    pub fn config(&self) -> &OidcConfig {
426        &self.config
427    }
428
429    /// Generate OpenID Connect Discovery document
430    pub fn discovery_document(&self) -> Result<OidcDiscoveryDocument> {
431        Ok(OidcDiscoveryDocument {
432            issuer: self.config.issuer.clone(),
433            authorization_endpoint: format!("{}/oidc/authorize", self.config.issuer),
434            token_endpoint: format!("{}/oidc/token", self.config.issuer),
435            userinfo_endpoint: self.config.userinfo_endpoint.clone(),
436            jwks_uri: self.config.jwks_uri.clone(),
437            registration_endpoint: Some(format!("{}/oidc/register", self.config.issuer)),
438            scopes_supported: self.config.scopes_supported.clone(),
439            response_types_supported: self.config.response_types_supported.clone(),
440            response_modes_supported: Some(vec![
441                "query".to_string(),
442                "fragment".to_string(),
443                "form_post".to_string(),
444            ]),
445            grant_types_supported: Some(vec![
446                "authorization_code".to_string(),
447                "refresh_token".to_string(),
448                "client_credentials".to_string(),
449            ]),
450            subject_types_supported: self.config.subject_types_supported.clone(),
451            id_token_signing_alg_values_supported: self
452                .config
453                .id_token_signing_alg_values_supported
454                .iter()
455                .map(algorithm_to_string)
456                .collect(),
457            userinfo_signing_alg_values_supported: Some(vec![
458                "RS256".to_string(),
459                "ES256".to_string(),
460                "HS256".to_string(),
461            ]),
462            token_endpoint_auth_methods_supported: Some(vec![
463                "client_secret_basic".to_string(),
464                "client_secret_post".to_string(),
465                "client_secret_jwt".to_string(),
466                "private_key_jwt".to_string(),
467            ]),
468            claims_supported: Some(self.config.claims_supported.clone()),
469            claims_parameter_supported: Some(self.config.claims_parameter_supported),
470            request_parameter_supported: Some(self.config.request_parameter_supported),
471            request_uri_parameter_supported: Some(self.config.request_uri_parameter_supported),
472            code_challenge_methods_supported: Some(vec!["S256".to_string()]),
473        })
474    }
475}
476
477/// Request parameters for creating an ID token.
478///
479/// Use [`IdTokenRequest::new`] to construct with the required `subject` and
480/// `client_id`, then chain optional setters for `nonce`, `auth_time`, and
481/// extra `claims`.
482///
483/// # Example
484///
485/// ```rust,no_run
486/// # use auth_framework::server::oidc::core::IdTokenRequest;
487/// let request = IdTokenRequest::new("user123", "client456")
488///     .with_nonce("abc");
489/// ```
490#[derive(Debug, Clone, Default)]
491pub struct IdTokenRequest<'a> {
492    pub subject: &'a str,
493    pub client_id: &'a str,
494    pub nonce: Option<&'a str>,
495    pub auth_time: Option<SystemTime>,
496    pub claims: Option<&'a HashMap<String, Value>>,
497}
498
499impl<'a> IdTokenRequest<'a> {
500    /// Create a new ID token request with required subject and client_id.
501    pub fn new(subject: &'a str, client_id: &'a str) -> Self {
502        Self {
503            subject,
504            client_id,
505            nonce: None,
506            auth_time: None,
507            claims: None,
508        }
509    }
510
511    /// Set the nonce value for replay protection.
512    pub fn with_nonce(mut self, nonce: &'a str) -> Self {
513        self.nonce = Some(nonce);
514        self
515    }
516
517    /// Set the authentication time.
518    pub fn with_auth_time(mut self, auth_time: SystemTime) -> Self {
519        self.auth_time = Some(auth_time);
520        self
521    }
522
523    /// Set additional claims to include in the ID token.
524    pub fn with_claims(mut self, claims: &'a HashMap<String, Value>) -> Self {
525        self.claims = Some(claims);
526        self
527    }
528}
529
530impl<S: AuthStorage + ?Sized> OidcProvider<S> {
531    /// Create an ID token from a request.
532    pub async fn create_id_token(&self, request: IdTokenRequest<'_>) -> Result<String> {
533        let subject = request.subject;
534        let client_id = request.client_id;
535        let nonce = request.nonce;
536        let auth_time = request.auth_time;
537        let claims = request.claims;
538        let now = SystemTime::now()
539            .duration_since(UNIX_EPOCH)
540            .map_err(|e| AuthError::auth_method("oidc", format!("Time error: {}", e)))?
541            .as_secs();
542
543        let exp = now + self.config.id_token_expiry.as_secs();
544
545        let mut id_token_claims = IdTokenClaims {
546            iss: self.config.issuer.clone(),
547            sub: subject.to_string(),
548            aud: vec![client_id.to_string()],
549            exp,
550            iat: now,
551            auth_time: auth_time
552                .and_then(|t| t.duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs())),
553            nonce: nonce.map(|n| n.to_string()),
554            additional_claims: claims.cloned().unwrap_or_default(),
555        };
556
557        // Add standard claims if provided
558        if let Some(claims) = claims {
559            for (key, value) in claims {
560                if self.config.claims_supported.contains(key) {
561                    id_token_claims
562                        .additional_claims
563                        .insert(key.clone(), value.clone());
564                }
565            }
566        }
567
568        // Create JWT
569        let _header = Header::new(Algorithm::RS256);
570        let token = self
571            .token_manager
572            .create_jwt_token(
573                subject,
574                vec!["openid".to_string()],
575                Some(Duration::from_secs(3600)),
576            )
577            .map_err(|e| AuthError::auth_method("oidc", format!("JWT creation failed: {}", e)))?;
578
579        Ok(token)
580    }
581
582    /// Validate an authorization request for OIDC
583    pub async fn validate_authorization_request(
584        &self,
585        request: &OidcAuthorizationRequest,
586    ) -> Result<AuthorizationValidationResult> {
587        // Check if 'openid' scope is present
588        if !request.scope.split_whitespace().any(|s| s == "openid") {
589            return Err(AuthError::auth_method(
590                "oidc",
591                "Missing required 'openid' scope",
592            ));
593        }
594
595        // SEC-H2: Require state parameter (CSRF protection per OAuth 2.0 §10.12)
596        match &request.state {
597            None => {
598                return Err(AuthError::auth_method(
599                    "oidc",
600                    "Missing required 'state' parameter",
601                ));
602            }
603            Some(s) if s.is_empty() => {
604                return Err(AuthError::auth_method(
605                    "oidc",
606                    "The 'state' parameter must not be empty",
607                ));
608            }
609            _ => {}
610        }
611
612        // Validate response_type
613        if !self
614            .config
615            .response_types_supported
616            .contains(&request.response_type)
617        {
618            return Err(AuthError::auth_method(
619                "oidc",
620                format!("Unsupported response_type: {}", request.response_type),
621            ));
622        }
623
624        // SEC-M8: Block implicit grant response types (OAuth 2.1 compliance).
625        // Response types containing "token" without "code" are implicit grants.
626        {
627            let rt = &request.response_type;
628            if rt.split_whitespace().any(|part| part == "token")
629                && !rt.split_whitespace().any(|part| part == "code")
630            {
631                return Err(AuthError::auth_method(
632                    "oidc",
633                    "Implicit grant (response_type containing 'token' without 'code') is not permitted",
634                ));
635            }
636        }
637
638        // Validate client_id
639        if request.client_id.is_empty() {
640            return Err(AuthError::auth_method("oidc", "Missing client_id"));
641        }
642
643        // Check client exists in registry and validate redirect_uri + scopes
644        if let Some(client_registry) = &self.client_registry {
645            if client_registry
646                .get_client(&request.client_id)
647                .await?
648                .is_none()
649            {
650                return Err(AuthError::auth_method("oidc", "Invalid client_id"));
651            }
652
653            // Validate redirect_uri against registered URIs
654            if !client_registry
655                .validate_redirect_uri(&request.client_id, &request.redirect_uri)
656                .await?
657            {
658                return Err(AuthError::auth_method(
659                    "oidc",
660                    "Invalid redirect_uri for client",
661                ));
662            }
663
664            // SEC-M4: Validate requested scopes against client's authorized scopes
665            for scope in request.scope.split_whitespace() {
666                if !client_registry
667                    .validate_scope(&request.client_id, scope)
668                    .await?
669                {
670                    return Err(AuthError::auth_method(
671                        "oidc",
672                        format!("Client is not authorized for scope '{}'", scope),
673                    ));
674                }
675            }
676        } else {
677            // SEC-H3: Without a client registry, we cannot verify the redirect_uri
678            // belongs to a legitimate client. Reject all requests to prevent open
679            // redirect attacks.
680            return Err(AuthError::auth_method(
681                "oidc",
682                "Client registry is required for authorization requests",
683            ));
684        }
685
686        // SEC-M4: Validate all requested scopes against server's supported scopes
687        for scope in request.scope.split_whitespace() {
688            if !self.config.scopes_supported.contains(&scope.to_string()) {
689                return Err(AuthError::auth_method(
690                    "oidc",
691                    format!("Unsupported scope '{}'", scope),
692                ));
693            }
694        }
695
696        Ok(AuthorizationValidationResult {
697            valid: true,
698            client_id: request.client_id.clone(),
699            redirect_uri: request.redirect_uri.clone(),
700            scope: request.scope.clone(),
701            state: request.state.clone(),
702            nonce: request.nonce.clone(),
703            max_age: request.max_age,
704            response_type: request.response_type.clone(),
705        })
706    }
707
708    /// Get user information for the UserInfo endpoint
709    pub async fn get_userinfo(&self, access_token: &str) -> Result<UserInfo> {
710        // Validate access token
711        let token_claims = self
712            .token_manager
713            .validate_jwt_token(access_token)
714            .map_err(|e| AuthError::auth_method("oidc", format!("Invalid access token: {}", e)))?;
715
716        // Extract subject from token
717        let subject = &token_claims.sub;
718
719        // Get user information from storage
720        let user_key = format!("user:{}", subject);
721        if let Some(user_data) = self.storage.get_kv(&user_key).await? {
722            let user_str = std::str::from_utf8(&user_data).unwrap_or("{}");
723            let user_profile: HashMap<String, Value> =
724                serde_json::from_str(user_str).unwrap_or_default();
725
726            Ok(UserInfo {
727                sub: subject.clone(),
728                name: user_profile
729                    .get("name")
730                    .and_then(|v| v.as_str())
731                    .map(|s| s.to_string()),
732                given_name: user_profile
733                    .get("given_name")
734                    .and_then(|v| v.as_str())
735                    .map(|s| s.to_string()),
736                family_name: user_profile
737                    .get("family_name")
738                    .and_then(|v| v.as_str())
739                    .map(|s| s.to_string()),
740                middle_name: user_profile
741                    .get("middle_name")
742                    .and_then(|v| v.as_str())
743                    .map(|s| s.to_string()),
744                nickname: user_profile
745                    .get("nickname")
746                    .and_then(|v| v.as_str())
747                    .map(|s| s.to_string()),
748                preferred_username: user_profile
749                    .get("preferred_username")
750                    .and_then(|v| v.as_str())
751                    .map(|s| s.to_string()),
752                profile: user_profile
753                    .get("profile")
754                    .and_then(|v| v.as_str())
755                    .map(|s| s.to_string()),
756                picture: user_profile
757                    .get("picture")
758                    .and_then(|v| v.as_str())
759                    .map(|s| s.to_string()),
760                website: user_profile
761                    .get("website")
762                    .and_then(|v| v.as_str())
763                    .map(|s| s.to_string()),
764                email: user_profile
765                    .get("email")
766                    .and_then(|v| v.as_str())
767                    .map(|s| s.to_string()),
768                email_verified: user_profile.get("email_verified").and_then(|v| v.as_bool()),
769                gender: user_profile
770                    .get("gender")
771                    .and_then(|v| v.as_str())
772                    .map(|s| s.to_string()),
773                birthdate: user_profile
774                    .get("birthdate")
775                    .and_then(|v| v.as_str())
776                    .map(|s| s.to_string()),
777                zoneinfo: user_profile
778                    .get("zoneinfo")
779                    .and_then(|v| v.as_str())
780                    .map(|s| s.to_string()),
781                locale: user_profile
782                    .get("locale")
783                    .and_then(|v| v.as_str())
784                    .map(|s| s.to_string()),
785                phone_number: user_profile
786                    .get("phone_number")
787                    .and_then(|v| v.as_str())
788                    .map(|s| s.to_string()),
789                phone_number_verified: user_profile
790                    .get("phone_number_verified")
791                    .and_then(|v| v.as_bool()),
792                address: user_profile
793                    .get("address")
794                    .and_then(|addr| addr.as_object())
795                    .map(|addr_obj| Address {
796                        formatted: addr_obj
797                            .get("formatted")
798                            .and_then(|v| v.as_str())
799                            .map(|s| s.to_string()),
800                        street_address: addr_obj
801                            .get("street_address")
802                            .and_then(|v| v.as_str())
803                            .map(|s| s.to_string()),
804                        locality: addr_obj
805                            .get("locality")
806                            .and_then(|v| v.as_str())
807                            .map(|s| s.to_string()),
808                        region: addr_obj
809                            .get("region")
810                            .and_then(|v| v.as_str())
811                            .map(|s| s.to_string()),
812                        postal_code: addr_obj
813                            .get("postal_code")
814                            .and_then(|v| v.as_str())
815                            .map(|s| s.to_string()),
816                        country: addr_obj
817                            .get("country")
818                            .and_then(|v| v.as_str())
819                            .map(|s| s.to_string()),
820                    }),
821                updated_at: user_profile.get("updated_at").and_then(|v| v.as_u64()),
822                additional_claims: user_profile
823                    .into_iter()
824                    .filter(|(k, _)| {
825                        ![
826                            "sub",
827                            "name",
828                            "given_name",
829                            "family_name",
830                            "middle_name",
831                            "nickname",
832                            "preferred_username",
833                            "profile",
834                            "picture",
835                            "website",
836                            "email",
837                            "email_verified",
838                            "gender",
839                            "birthdate",
840                            "zoneinfo",
841                            "locale",
842                            "phone_number",
843                            "phone_number_verified",
844                            "address",
845                            "updated_at",
846                        ]
847                        .contains(&k.as_str())
848                    })
849                    .collect(),
850            })
851        } else {
852            // Return minimal user info with only the subject claim when no stored profile exists
853            Ok(UserInfo {
854                sub: subject.clone(),
855                name: None,
856                given_name: None,
857                family_name: None,
858                middle_name: None,
859                nickname: None,
860                preferred_username: Some(subject.clone()),
861                profile: None,
862                picture: None,
863                website: None,
864                email: None,
865                email_verified: None,
866                gender: None,
867                birthdate: None,
868                zoneinfo: None,
869                locale: None,
870                phone_number: None,
871                phone_number_verified: None,
872                address: None,
873                updated_at: None,
874                additional_claims: HashMap::new(),
875            })
876        }
877    }
878
879    /// Handle logout request
880    pub async fn handle_logout(
881        &self,
882        id_token_hint: Option<&str>,
883        post_logout_redirect_uri: Option<&str>,
884        state: Option<&str>,
885    ) -> Result<LogoutResponse> {
886        // Validate ID token hint if provided
887        if let Some(id_token) = id_token_hint {
888            let claims = self
889                .token_manager
890                .validate_jwt_token(id_token)
891                .map_err(|e| AuthError::auth_method("oidc", format!("Invalid ID token: {}", e)))?;
892
893            // Invalidate all sessions for the user identified in the token
894            let user_sessions = self
895                .storage
896                .list_user_sessions(&claims.sub)
897                .await
898                .map_err(|e| AuthError::internal(format!("Failed to list user sessions: {}", e)))?;
899
900            for session in user_sessions {
901                self.storage
902                    .delete_session(&session.session_id)
903                    .await
904                    .map_err(|e| AuthError::internal(format!("Failed to delete session: {}", e)))?;
905            }
906        }
907
908        // Validate post_logout_redirect_uri against registered URIs
909        if let Some(post_logout_uri) = post_logout_redirect_uri {
910            // Extract client_id from the ID token if available
911            if let Some(id_token) = id_token_hint {
912                let claims = self
913                    .token_manager
914                    .validate_jwt_token(id_token)
915                    .map_err(|e| {
916                        AuthError::auth_method("oidc", format!("Invalid ID token: {}", e))
917                    })?;
918
919                if let Some(aud) = claims.aud.split_whitespace().next() {
920                    // Validate that the post-logout redirect URI is registered for this client
921                    if !self
922                        .is_post_logout_uri_registered(aud, post_logout_uri)
923                        .await?
924                    {
925                        return Err(AuthError::validation(
926                            "post_logout_redirect_uri not registered for client",
927                        ));
928                    }
929                }
930            } else {
931                // If no ID token provided, we cannot validate the client association
932                // In a production system, you might want to require ID token for validation
933                return Err(AuthError::validation(
934                    "id_token_hint required for post_logout_redirect_uri validation",
935                ));
936            }
937        }
938
939        Ok(LogoutResponse {
940            post_logout_redirect_uri: post_logout_redirect_uri.map(|uri| uri.to_string()),
941            state: state.map(|s| s.to_string()),
942        })
943    }
944
945    /// Check if a post-logout redirect URI is registered for a client
946    async fn is_post_logout_uri_registered(&self, client_id: &str, uri: &str) -> Result<bool> {
947        // SECURITY CRITICAL: Validate redirect URI against registered URIs
948
949        // Basic security: only allow https URIs (except localhost for development)
950        if !uri.starts_with("https://")
951            && !uri.starts_with("http://localhost")
952            && !uri.starts_with("http://127.0.0.1")
953        {
954            tracing::warn!(
955                "Rejected post-logout redirect URI with invalid scheme: {}",
956                uri
957            );
958            return Ok(false);
959        }
960
961        // Enhanced security: Validate against registered URIs
962        match self.get_client_registered_post_logout_uris(client_id).await {
963            Ok(registered_uris) => {
964                let is_registered = registered_uris.contains(&uri.to_string());
965                if !is_registered {
966                    tracing::warn!(
967                        "Rejected unregistered post-logout redirect URI for client {}: {}",
968                        client_id,
969                        uri
970                    );
971                }
972                Ok(is_registered)
973            }
974            Err(_) => {
975                // When client lookup fails, reject all redirect URIs to prevent
976                // open-redirect attacks. Even loopback URIs are rejected here because
977                // `http://localhost.evil.com` would match a naive prefix check.
978                tracing::error!(
979                    "Rejected redirect URI — client lookup failed for {}: {}",
980                    client_id,
981                    uri
982                );
983                Ok(false)
984            }
985        }
986    }
987
988    /// Get registered post-logout redirect URIs for a client
989    async fn get_client_registered_post_logout_uris(&self, client_id: &str) -> Result<Vec<String>> {
990        // Look up post-logout redirect URIs from client registry storage
991        if let Some(client_registry) = &self.client_registry {
992            if let Some(client) = client_registry.get_client(client_id).await? {
993                // Post-logout redirect URIs are stored in client metadata
994                if let Some(uris) = client.metadata.get("post_logout_redirect_uris") {
995                    if let Some(arr) = uris.as_array() {
996                        return Ok(arr
997                            .iter()
998                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
999                            .collect());
1000                    }
1001                }
1002            }
1003        }
1004        // Unknown client or no registry — return empty list so all redirect URIs
1005        // are rejected, preventing open-redirect attacks.
1006        Ok(Vec::new())
1007    }
1008
1009    /// Generate JWK Set for the .well-known/jwks.json endpoint
1010    pub fn generate_jwks(&self) -> Result<JwkSet> {
1011        let keys = self
1012            .token_manager
1013            .export_public_jwks()?
1014            .into_iter()
1015            .map(|key| Jwk {
1016                kty: "RSA".to_string(),
1017                use_: Some("sig".to_string()),
1018                key_ops: Some(vec!["verify".to_string()]),
1019                alg: Some(algorithm_to_string(&key.algorithm)),
1020                kid: Some(key.kid),
1021                n: key.n,
1022                e: key.e,
1023                additional_params: HashMap::new(),
1024            })
1025            .collect();
1026
1027        Ok(JwkSet { keys })
1028    }
1029}
1030
1031/// OIDC Authorization Request
1032#[derive(Debug, Clone, Serialize, Deserialize)]
1033pub struct OidcAuthorizationRequest {
1034    pub response_type: String,
1035    pub client_id: String,
1036    pub redirect_uri: String,
1037    pub scope: String,
1038    pub state: Option<String>,
1039    pub nonce: Option<String>,
1040    pub max_age: Option<u64>,
1041    pub ui_locales: Option<String>,
1042    pub claims_locales: Option<String>,
1043    pub id_token_hint: Option<String>,
1044    pub login_hint: Option<String>,
1045    pub acr_values: Option<String>,
1046    pub claims: Option<String>,
1047    pub request: Option<String>,
1048    pub request_uri: Option<String>,
1049}
1050
1051impl OidcAuthorizationRequest {
1052    /// Create a builder with the required `client_id` and `redirect_uri`.
1053    ///
1054    /// Defaults: `response_type = "code"`, `scope = "openid"`.
1055    pub fn builder(
1056        client_id: impl Into<String>,
1057        redirect_uri: impl Into<String>,
1058    ) -> OidcAuthorizationRequestBuilder {
1059        OidcAuthorizationRequestBuilder::new(client_id, redirect_uri)
1060    }
1061}
1062
1063/// Fluent builder for [`OidcAuthorizationRequest`].
1064///
1065/// # Example
1066///
1067/// ```rust
1068/// use auth_framework::server::oidc::core::OidcAuthorizationRequest;
1069///
1070/// let req = OidcAuthorizationRequest::builder("my-client", "https://app.example/callback")
1071///     .scope("openid profile email")
1072///     .state("random_state")
1073///     .nonce("random_nonce")
1074///     .build();
1075///
1076/// assert_eq!(req.client_id, "my-client");
1077/// assert_eq!(req.scope, "openid profile email");
1078/// ```
1079pub struct OidcAuthorizationRequestBuilder {
1080    req: OidcAuthorizationRequest,
1081}
1082
1083impl OidcAuthorizationRequestBuilder {
1084    fn new(client_id: impl Into<String>, redirect_uri: impl Into<String>) -> Self {
1085        Self {
1086            req: OidcAuthorizationRequest {
1087                response_type: "code".to_string(),
1088                client_id: client_id.into(),
1089                redirect_uri: redirect_uri.into(),
1090                scope: "openid".to_string(),
1091                state: None,
1092                nonce: None,
1093                max_age: None,
1094                ui_locales: None,
1095                claims_locales: None,
1096                id_token_hint: None,
1097                login_hint: None,
1098                acr_values: None,
1099                claims: None,
1100                request: None,
1101                request_uri: None,
1102            },
1103        }
1104    }
1105
1106    /// Set the OAuth2 response type (default: `"code"`).
1107    pub fn response_type(mut self, rt: impl Into<String>) -> Self {
1108        self.req.response_type = rt.into();
1109        self
1110    }
1111
1112    /// Set the requested scopes (default: `"openid"`).
1113    pub fn scope(mut self, scope: impl Into<String>) -> Self {
1114        self.req.scope = scope.into();
1115        self
1116    }
1117
1118    /// Set the state parameter for CSRF protection.
1119    pub fn state(mut self, state: impl Into<String>) -> Self {
1120        self.req.state = Some(state.into());
1121        self
1122    }
1123
1124    /// Set the nonce for ID token replay protection.
1125    pub fn nonce(mut self, nonce: impl Into<String>) -> Self {
1126        self.req.nonce = Some(nonce.into());
1127        self
1128    }
1129
1130    /// Set the maximum authentication age in seconds.
1131    pub fn max_age(mut self, seconds: u64) -> Self {
1132        self.req.max_age = Some(seconds);
1133        self
1134    }
1135
1136    /// Provide a login hint (e.g. email or username).
1137    pub fn login_hint(mut self, hint: impl Into<String>) -> Self {
1138        self.req.login_hint = Some(hint.into());
1139        self
1140    }
1141
1142    /// Set the id_token_hint for session management.
1143    pub fn id_token_hint(mut self, hint: impl Into<String>) -> Self {
1144        self.req.id_token_hint = Some(hint.into());
1145        self
1146    }
1147
1148    /// Set the ACR values.
1149    pub fn acr_values(mut self, values: impl Into<String>) -> Self {
1150        self.req.acr_values = Some(values.into());
1151        self
1152    }
1153
1154    /// Consume the builder and produce the request.
1155    pub fn build(self) -> OidcAuthorizationRequest {
1156        self.req
1157    }
1158}
1159
1160/// Authorization validation result
1161#[derive(Debug, Clone)]
1162pub struct AuthorizationValidationResult {
1163    pub valid: bool,
1164    pub client_id: String,
1165    pub redirect_uri: String,
1166    pub scope: String,
1167    pub state: Option<String>,
1168    pub nonce: Option<String>,
1169    pub max_age: Option<u64>,
1170    pub response_type: String,
1171}
1172
1173/// ID Token Claims
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1175pub struct IdTokenClaims {
1176    /// Issuer
1177    pub iss: String,
1178    /// Subject
1179    pub sub: String,
1180    /// Audience
1181    pub aud: Vec<String>,
1182    /// Expiration time
1183    pub exp: u64,
1184    /// Issued at
1185    pub iat: u64,
1186    /// Authentication time
1187    #[serde(skip_serializing_if = "Option::is_none")]
1188    pub auth_time: Option<u64>,
1189    /// Nonce
1190    #[serde(skip_serializing_if = "Option::is_none")]
1191    pub nonce: Option<String>,
1192    /// Additional claims
1193    #[serde(flatten)]
1194    pub additional_claims: HashMap<String, Value>,
1195}
1196
1197/// UserInfo response
1198#[derive(Debug, Clone, Serialize, Deserialize)]
1199pub struct UserInfo {
1200    pub sub: String,
1201    #[serde(skip_serializing_if = "Option::is_none")]
1202    pub name: Option<String>,
1203    #[serde(skip_serializing_if = "Option::is_none")]
1204    pub given_name: Option<String>,
1205    #[serde(skip_serializing_if = "Option::is_none")]
1206    pub family_name: Option<String>,
1207    #[serde(skip_serializing_if = "Option::is_none")]
1208    pub middle_name: Option<String>,
1209    #[serde(skip_serializing_if = "Option::is_none")]
1210    pub nickname: Option<String>,
1211    #[serde(skip_serializing_if = "Option::is_none")]
1212    pub preferred_username: Option<String>,
1213    #[serde(skip_serializing_if = "Option::is_none")]
1214    pub profile: Option<String>,
1215    #[serde(skip_serializing_if = "Option::is_none")]
1216    pub picture: Option<String>,
1217    #[serde(skip_serializing_if = "Option::is_none")]
1218    pub website: Option<String>,
1219    #[serde(skip_serializing_if = "Option::is_none")]
1220    pub email: Option<String>,
1221    #[serde(skip_serializing_if = "Option::is_none")]
1222    pub email_verified: Option<bool>,
1223    #[serde(skip_serializing_if = "Option::is_none")]
1224    pub gender: Option<String>,
1225    #[serde(skip_serializing_if = "Option::is_none")]
1226    pub birthdate: Option<String>,
1227    #[serde(skip_serializing_if = "Option::is_none")]
1228    pub zoneinfo: Option<String>,
1229    #[serde(skip_serializing_if = "Option::is_none")]
1230    pub locale: Option<String>,
1231    #[serde(skip_serializing_if = "Option::is_none")]
1232    pub phone_number: Option<String>,
1233    #[serde(skip_serializing_if = "Option::is_none")]
1234    pub phone_number_verified: Option<bool>,
1235    #[serde(skip_serializing_if = "Option::is_none")]
1236    pub address: Option<Address>,
1237    #[serde(skip_serializing_if = "Option::is_none")]
1238    pub updated_at: Option<u64>,
1239    #[serde(flatten)]
1240    pub additional_claims: HashMap<String, Value>,
1241}
1242
1243/// Address claim
1244#[derive(Debug, Clone, Serialize, Deserialize)]
1245pub struct Address {
1246    #[serde(skip_serializing_if = "Option::is_none")]
1247    pub formatted: Option<String>,
1248    #[serde(skip_serializing_if = "Option::is_none")]
1249    pub street_address: Option<String>,
1250    #[serde(skip_serializing_if = "Option::is_none")]
1251    pub locality: Option<String>,
1252    #[serde(skip_serializing_if = "Option::is_none")]
1253    pub region: Option<String>,
1254    #[serde(skip_serializing_if = "Option::is_none")]
1255    pub postal_code: Option<String>,
1256    #[serde(skip_serializing_if = "Option::is_none")]
1257    pub country: Option<String>,
1258}
1259
1260/// OIDC Discovery Document
1261#[derive(Debug, Clone, Serialize, Deserialize)]
1262pub struct OidcDiscoveryDocument {
1263    pub issuer: String,
1264    pub authorization_endpoint: String,
1265    pub token_endpoint: String,
1266    pub userinfo_endpoint: String,
1267    pub jwks_uri: String,
1268    #[serde(skip_serializing_if = "Option::is_none")]
1269    pub registration_endpoint: Option<String>,
1270    pub scopes_supported: Vec<String>,
1271    pub response_types_supported: Vec<String>,
1272    #[serde(skip_serializing_if = "Option::is_none")]
1273    pub response_modes_supported: Option<Vec<String>>,
1274    #[serde(skip_serializing_if = "Option::is_none")]
1275    pub grant_types_supported: Option<Vec<String>>,
1276    pub subject_types_supported: Vec<SubjectType>,
1277    pub id_token_signing_alg_values_supported: Vec<String>,
1278    #[serde(skip_serializing_if = "Option::is_none")]
1279    pub userinfo_signing_alg_values_supported: Option<Vec<String>>,
1280    #[serde(skip_serializing_if = "Option::is_none")]
1281    pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
1282    #[serde(skip_serializing_if = "Option::is_none")]
1283    pub claims_supported: Option<Vec<String>>,
1284    #[serde(skip_serializing_if = "Option::is_none")]
1285    pub claims_parameter_supported: Option<bool>,
1286    #[serde(skip_serializing_if = "Option::is_none")]
1287    pub request_parameter_supported: Option<bool>,
1288    #[serde(skip_serializing_if = "Option::is_none")]
1289    pub request_uri_parameter_supported: Option<bool>,
1290    #[serde(skip_serializing_if = "Option::is_none")]
1291    pub code_challenge_methods_supported: Option<Vec<String>>,
1292}
1293
1294/// JWK Set
1295#[derive(Debug, Clone, Serialize, Deserialize)]
1296pub struct JwkSet {
1297    pub keys: Vec<Jwk>,
1298}
1299
1300/// JSON Web Key
1301#[derive(Debug, Clone, Serialize, Deserialize)]
1302pub struct Jwk {
1303    pub kty: String,
1304    #[serde(rename = "use", skip_serializing_if = "Option::is_none")]
1305    pub use_: Option<String>,
1306    #[serde(skip_serializing_if = "Option::is_none")]
1307    pub key_ops: Option<Vec<String>>,
1308    #[serde(skip_serializing_if = "Option::is_none")]
1309    pub alg: Option<String>,
1310    #[serde(skip_serializing_if = "Option::is_none")]
1311    pub kid: Option<String>,
1312    pub n: String,
1313    pub e: String,
1314    #[serde(flatten)]
1315    pub additional_params: HashMap<String, Value>,
1316}
1317
1318/// Logout response
1319#[derive(Debug, Clone)]
1320pub struct LogoutResponse {
1321    pub post_logout_redirect_uri: Option<String>,
1322    pub state: Option<String>,
1323}
1324
1325/// Helper function to convert Algorithm to string
1326fn algorithm_to_string(alg: &Algorithm) -> String {
1327    match alg {
1328        Algorithm::HS256 => "HS256".to_string(),
1329        Algorithm::HS384 => "HS384".to_string(),
1330        Algorithm::HS512 => "HS512".to_string(),
1331        Algorithm::ES256 => "ES256".to_string(),
1332        Algorithm::ES384 => "ES384".to_string(),
1333        Algorithm::RS256 => "RS256".to_string(),
1334        Algorithm::RS384 => "RS384".to_string(),
1335        Algorithm::RS512 => "RS512".to_string(),
1336        Algorithm::PS256 => "PS256".to_string(),
1337        Algorithm::PS384 => "PS384".to_string(),
1338        Algorithm::PS512 => "PS512".to_string(),
1339        Algorithm::EdDSA => "EdDSA".to_string(),
1340    }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345    use super::*;
1346    use crate::storage::MemoryStorage;
1347
1348    async fn create_test_oidc_provider() -> OidcProvider<MemoryStorage> {
1349        let config = OidcConfig::default();
1350        let token_manager = Arc::new(TokenManager::new_hmac(
1351            b"test_secret_key_32_bytes_long!!!!",
1352            "test_issuer",
1353            "test_audience",
1354        ));
1355        let storage = Arc::new(MemoryStorage::new());
1356
1357        // Register a test client so that the fail-closed client registry check passes
1358        let client_registry = Arc::new(
1359            crate::server::core::client_registry::ClientRegistry::new(
1360                storage.clone() as Arc<dyn crate::storage::AuthStorage>
1361            )
1362            .await
1363            .unwrap(),
1364        );
1365        let test_client = crate::client::ClientConfig {
1366            client_id: "test_client".to_string(),
1367            client_secret: Some("test_secret".to_string()),
1368            client_type: crate::client::ClientType::Confidential,
1369            redirect_uris: vec!["https://client.example.com/callback".to_string()].into(),
1370            authorized_scopes: vec![
1371                "openid".to_string(),
1372                "profile".to_string(),
1373                "email".to_string(),
1374            ]
1375            .into(),
1376            authorized_grant_types: vec!["authorization_code".to_string()].into(),
1377            authorized_response_types: vec!["code".to_string()].into(),
1378            client_name: Some("Test Client".to_string()),
1379            client_description: None,
1380            metadata: std::collections::HashMap::new(),
1381        };
1382        client_registry.register_client(test_client).await.unwrap();
1383
1384        let mut provider = OidcProvider::new(config, token_manager, storage)
1385            .await
1386            .unwrap();
1387        provider.set_client_registry(client_registry);
1388        provider
1389    }
1390
1391    #[tokio::test]
1392    async fn test_oidc_provider_creation() {
1393        let provider = create_test_oidc_provider().await;
1394        assert_eq!(provider.config.issuer, "https://auth.example.com");
1395        assert!(
1396            provider
1397                .config
1398                .scopes_supported
1399                .contains(&"openid".to_string())
1400        );
1401    }
1402
1403    #[tokio::test]
1404    async fn test_discovery_document() {
1405        let provider = create_test_oidc_provider().await;
1406        let discovery = provider.discovery_document().unwrap();
1407
1408        assert_eq!(discovery.issuer, "https://auth.example.com");
1409        assert_eq!(
1410            discovery.authorization_endpoint,
1411            "https://auth.example.com/oidc/authorize"
1412        );
1413        assert!(discovery.scopes_supported.contains(&"openid".to_string()));
1414        assert!(
1415            discovery
1416                .response_types_supported
1417                .contains(&"code".to_string())
1418        );
1419    }
1420
1421    #[tokio::test]
1422    async fn test_authorization_request_validation() {
1423        let provider = create_test_oidc_provider().await;
1424
1425        let valid_request = OidcAuthorizationRequest {
1426            response_type: "code".to_string(),
1427            client_id: "test_client".to_string(),
1428            redirect_uri: "https://client.example.com/callback".to_string(),
1429            scope: "openid profile email".to_string(),
1430            state: Some("abc123".to_string()),
1431            nonce: Some("xyz789".to_string()),
1432            max_age: None,
1433            ui_locales: None,
1434            claims_locales: None,
1435            id_token_hint: None,
1436            login_hint: None,
1437            acr_values: None,
1438            claims: None,
1439            request: None,
1440            request_uri: None,
1441        };
1442
1443        let result = provider
1444            .validate_authorization_request(&valid_request)
1445            .await
1446            .unwrap();
1447        assert!(result.valid);
1448        assert_eq!(result.client_id, "test_client");
1449        assert_eq!(result.scope, "openid profile email");
1450    }
1451
1452    #[tokio::test]
1453    async fn test_authorization_request_missing_openid_scope() {
1454        let provider = create_test_oidc_provider().await;
1455
1456        let invalid_request = OidcAuthorizationRequest {
1457            response_type: "code".to_string(),
1458            client_id: "test_client".to_string(),
1459            redirect_uri: "https://client.example.com/callback".to_string(),
1460            scope: "profile email".to_string(), // Missing 'openid'
1461            state: Some("abc123".to_string()),
1462            nonce: Some("xyz789".to_string()),
1463            max_age: None,
1464            ui_locales: None,
1465            claims_locales: None,
1466            id_token_hint: None,
1467            login_hint: None,
1468            acr_values: None,
1469            claims: None,
1470            request: None,
1471            request_uri: None,
1472        };
1473
1474        let result = provider
1475            .validate_authorization_request(&invalid_request)
1476            .await;
1477        assert!(result.is_err());
1478    }
1479
1480    #[tokio::test]
1481    async fn test_id_token_creation() {
1482        let provider = create_test_oidc_provider().await;
1483
1484        let auth_time = SystemTime::now();
1485        let mut claims = HashMap::new();
1486        claims.insert("name".to_string(), Value::String("John Doe".to_string()));
1487        claims.insert(
1488            "email".to_string(),
1489            Value::String("john@example.com".to_string()),
1490        );
1491
1492        use crate::server::oidc::core::IdTokenRequest;
1493        let id_token = provider
1494            .create_id_token(
1495                IdTokenRequest::new("user123", "client456")
1496                    .with_nonce("nonce789")
1497                    .with_auth_time(auth_time)
1498                    .with_claims(&claims),
1499            )
1500            .await
1501            .unwrap();
1502
1503        assert!(!id_token.is_empty());
1504        assert!(id_token.contains('.'));
1505    }
1506
1507    #[tokio::test]
1508    async fn test_jwks_generation() {
1509        let provider = create_test_oidc_provider().await;
1510        let jwks = provider.generate_jwks().unwrap();
1511
1512        assert!(jwks.keys.is_empty());
1513    }
1514
1515    #[tokio::test]
1516    async fn test_logout_handling() {
1517        let provider = create_test_oidc_provider().await;
1518
1519        // Test logout without post_logout_redirect_uri (should work without id_token_hint)
1520        let logout_response = provider
1521            .handle_logout(None, None, Some("state123"))
1522            .await
1523            .unwrap();
1524
1525        assert_eq!(logout_response.post_logout_redirect_uri, None);
1526        assert_eq!(logout_response.state, Some("state123".to_string()));
1527    }
1528
1529    #[test]
1530    fn test_subject_type_serialization() {
1531        let public = SubjectType::Public;
1532        let pairwise = SubjectType::Pairwise;
1533
1534        let public_json = serde_json::to_string(&public).unwrap();
1535        let pairwise_json = serde_json::to_string(&pairwise).unwrap();
1536
1537        assert_eq!(public_json, "\"public\"");
1538        assert_eq!(pairwise_json, "\"pairwise\"");
1539    }
1540
1541    #[test]
1542    fn test_algorithm_to_string_conversion() {
1543        assert_eq!(algorithm_to_string(&Algorithm::RS256), "RS256");
1544        assert_eq!(algorithm_to_string(&Algorithm::ES256), "ES256");
1545        assert_eq!(algorithm_to_string(&Algorithm::HS256), "HS256");
1546        assert_eq!(algorithm_to_string(&Algorithm::EdDSA), "EdDSA");
1547    }
1548
1549    #[test]
1550    fn test_oidc_config_default() {
1551        let config = OidcConfig::default();
1552        assert_eq!(config.issuer, "https://auth.example.com");
1553        assert!(config.scopes_supported.contains(&"openid".to_string()));
1554        assert!(config.claims_supported.contains(&"sub".to_string()));
1555        assert_eq!(config.subject_types_supported, vec![SubjectType::Public]);
1556    }
1557}