1use 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#[derive(Debug, Clone)]
24pub struct OidcConfig {
25 pub issuer: String,
27
28 pub oauth2_config: crate::oauth2_server::OAuth2Config,
30
31 pub jwks_uri: String,
33
34 pub userinfo_endpoint: String,
36
37 pub response_types_supported: Vec<String>,
39
40 pub subject_types_supported: Vec<SubjectType>,
42
43 pub id_token_signing_alg_values_supported: Vec<Algorithm>,
45
46 pub scopes_supported: Vec<String>,
48
49 pub claims_supported: Vec<String>,
51
52 pub claims_parameter_supported: bool,
54
55 pub request_parameter_supported: bool,
57
58 pub request_uri_parameter_supported: bool,
60
61 pub id_token_expiry: Duration,
63
64 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), max_age_supported: Some(Duration::from_secs(86400)), }
125 }
126}
127
128impl OidcConfig {
129 pub fn builder() -> OidcConfigBuilder {
143 OidcConfigBuilder::default()
144 }
145}
146
147#[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 pub fn issuer(mut self, issuer: impl Into<String>) -> Self {
174 self.config.issuer = issuer.into();
175 self
176 }
177
178 pub fn oauth2_config(mut self, config: crate::oauth2_server::OAuth2Config) -> Self {
187 self.config.oauth2_config = config;
188 self
189 }
190
191 pub fn jwks_uri(mut self, uri: impl Into<String>) -> Self {
200 self.config.jwks_uri = uri.into();
201 self
202 }
203
204 pub fn userinfo_endpoint(mut self, uri: impl Into<String>) -> Self {
213 self.config.userinfo_endpoint = uri.into();
214 self
215 }
216
217 pub fn response_types_supported(mut self, types: Vec<String>) -> Self {
226 self.config.response_types_supported = types;
227 self
228 }
229
230 pub fn subject_types_supported(mut self, types: Vec<SubjectType>) -> Self {
239 self.config.subject_types_supported = types;
240 self
241 }
242
243 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 pub fn scopes_supported(mut self, scopes: Vec<String>) -> Self {
265 self.config.scopes_supported = scopes;
266 self
267 }
268
269 pub fn claims_supported(mut self, claims: Vec<String>) -> Self {
278 self.config.claims_supported = claims;
279 self
280 }
281
282 pub fn claims_parameter_supported(mut self, supported: bool) -> Self {
291 self.config.claims_parameter_supported = supported;
292 self
293 }
294
295 pub fn request_parameter_supported(mut self, supported: bool) -> Self {
304 self.config.request_parameter_supported = supported;
305 self
306 }
307
308 pub fn request_uri_parameter_supported(mut self, supported: bool) -> Self {
317 self.config.request_uri_parameter_supported = supported;
318 self
319 }
320
321 pub fn id_token_expiry(mut self, expiry: Duration) -> Self {
331 self.config.id_token_expiry = expiry;
332 self
333 }
334
335 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 pub fn build(self) -> OidcConfig {
360 self.config
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(rename_all = "lowercase")]
367pub enum SubjectType {
368 Public,
370 Pairwise,
372}
373
374pub 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 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 pub fn oauth2_server(&self) -> &OAuth2Server {
416 &self.oauth2_server
417 }
418
419 pub fn set_client_registry(&mut self, client_registry: Arc<ClientRegistry>) {
421 self.client_registry = Some(client_registry);
422 }
423
424 pub fn config(&self) -> &OidcConfig {
426 &self.config
427 }
428
429 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#[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 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 pub fn with_nonce(mut self, nonce: &'a str) -> Self {
513 self.nonce = Some(nonce);
514 self
515 }
516
517 pub fn with_auth_time(mut self, auth_time: SystemTime) -> Self {
519 self.auth_time = Some(auth_time);
520 self
521 }
522
523 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 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 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 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 pub async fn validate_authorization_request(
584 &self,
585 request: &OidcAuthorizationRequest,
586 ) -> Result<AuthorizationValidationResult> {
587 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 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 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 {
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 if request.client_id.is_empty() {
640 return Err(AuthError::auth_method("oidc", "Missing client_id"));
641 }
642
643 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 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 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 return Err(AuthError::auth_method(
681 "oidc",
682 "Client registry is required for authorization requests",
683 ));
684 }
685
686 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 pub async fn get_userinfo(&self, access_token: &str) -> Result<UserInfo> {
710 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 let subject = &token_claims.sub;
718
719 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 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 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 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 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 if let Some(post_logout_uri) = post_logout_redirect_uri {
910 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 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 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 async fn is_post_logout_uri_registered(&self, client_id: &str, uri: &str) -> Result<bool> {
947 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 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 tracing::error!(
979 "Rejected redirect URI — client lookup failed for {}: {}",
980 client_id,
981 uri
982 );
983 Ok(false)
984 }
985 }
986 }
987
988 async fn get_client_registered_post_logout_uris(&self, client_id: &str) -> Result<Vec<String>> {
990 if let Some(client_registry) = &self.client_registry {
992 if let Some(client) = client_registry.get_client(client_id).await? {
993 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 Ok(Vec::new())
1007 }
1008
1009 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#[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 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
1063pub 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 pub fn response_type(mut self, rt: impl Into<String>) -> Self {
1108 self.req.response_type = rt.into();
1109 self
1110 }
1111
1112 pub fn scope(mut self, scope: impl Into<String>) -> Self {
1114 self.req.scope = scope.into();
1115 self
1116 }
1117
1118 pub fn state(mut self, state: impl Into<String>) -> Self {
1120 self.req.state = Some(state.into());
1121 self
1122 }
1123
1124 pub fn nonce(mut self, nonce: impl Into<String>) -> Self {
1126 self.req.nonce = Some(nonce.into());
1127 self
1128 }
1129
1130 pub fn max_age(mut self, seconds: u64) -> Self {
1132 self.req.max_age = Some(seconds);
1133 self
1134 }
1135
1136 pub fn login_hint(mut self, hint: impl Into<String>) -> Self {
1138 self.req.login_hint = Some(hint.into());
1139 self
1140 }
1141
1142 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 pub fn acr_values(mut self, values: impl Into<String>) -> Self {
1150 self.req.acr_values = Some(values.into());
1151 self
1152 }
1153
1154 pub fn build(self) -> OidcAuthorizationRequest {
1156 self.req
1157 }
1158}
1159
1160#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1175pub struct IdTokenClaims {
1176 pub iss: String,
1178 pub sub: String,
1180 pub aud: Vec<String>,
1182 pub exp: u64,
1184 pub iat: u64,
1186 #[serde(skip_serializing_if = "Option::is_none")]
1188 pub auth_time: Option<u64>,
1189 #[serde(skip_serializing_if = "Option::is_none")]
1191 pub nonce: Option<String>,
1192 #[serde(flatten)]
1194 pub additional_claims: HashMap<String, Value>,
1195}
1196
1197#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
1296pub struct JwkSet {
1297 pub keys: Vec<Jwk>,
1298}
1299
1300#[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#[derive(Debug, Clone)]
1320pub struct LogoutResponse {
1321 pub post_logout_redirect_uri: Option<String>,
1322 pub state: Option<String>,
1323}
1324
1325fn 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 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(), 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 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}