Skip to main content

helios_auth/
discovery.rs

1use serde::Serialize;
2use serde_json::Value;
3
4use crate::config::AuthConfig;
5
6/// SMART on FHIR configuration document served at
7/// `/.well-known/smart-configuration`.
8#[derive(Debug, Serialize)]
9pub struct SmartConfiguration {
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub issuer: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub jwks_uri: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub authorization_endpoint: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub token_endpoint: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub introspection_endpoint: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub management_endpoint: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub registration_endpoint: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub revocation_endpoint: Option<String>,
26    pub scopes_supported: Vec<String>,
27    pub response_types_supported: Vec<String>,
28    pub grant_types_supported: Vec<String>,
29    pub token_endpoint_auth_methods_supported: Vec<String>,
30    pub code_challenge_methods_supported: Vec<String>,
31    pub token_endpoint_auth_signing_alg_values_supported: Vec<String>,
32    pub capabilities: Vec<String>,
33}
34
35impl SmartConfiguration {
36    /// Build the SMART configuration document from `AuthConfig`.
37    pub fn from_config(config: &AuthConfig) -> Self {
38        let mut response_types_supported = vec!["token".to_string()];
39        let mut grant_types_supported = vec!["client_credentials".to_string()];
40
41        if config.smart_authorize_endpoint.is_some() {
42            response_types_supported.push("code".to_string());
43            grant_types_supported.push("authorization_code".to_string());
44        }
45
46        Self {
47            issuer: config.expected_issuer.clone(),
48            jwks_uri: config
49                .smart_jwks_url
50                .clone()
51                .or_else(|| config.jwks_url.clone()),
52            authorization_endpoint: config.smart_authorize_endpoint.clone(),
53            token_endpoint: config.smart_token_endpoint.clone(),
54            introspection_endpoint: config.smart_introspection_endpoint.clone(),
55            management_endpoint: config.smart_management_endpoint.clone(),
56            registration_endpoint: config.smart_registration_endpoint.clone(),
57            revocation_endpoint: config.smart_revocation_endpoint.clone(),
58            scopes_supported: vec![
59                "system/*.cruds".to_string(),
60                "system/*.rs".to_string(),
61                "system/*.r".to_string(),
62            ],
63            response_types_supported,
64            grant_types_supported,
65            token_endpoint_auth_methods_supported: vec!["private_key_jwt".to_string()],
66            code_challenge_methods_supported: vec!["S256".to_string()],
67            token_endpoint_auth_signing_alg_values_supported: vec![
68                "RS384".to_string(),
69                "ES384".to_string(),
70            ],
71            capabilities: vec![
72                "permission-v2".to_string(),
73                "client-confidential-asymmetric".to_string(),
74            ],
75        }
76    }
77
78    /// Serialize to a JSON `Value`.
79    pub fn to_json(&self) -> Value {
80        serde_json::to_value(self).expect("SmartConfiguration serialization cannot fail")
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_smart_config_from_default() {
90        let config = AuthConfig::default();
91        let smart = SmartConfiguration::from_config(&config);
92
93        assert!(smart.issuer.is_none());
94        assert!(smart.token_endpoint.is_none());
95        assert!(smart.capabilities.contains(&"permission-v2".to_string()));
96        assert_eq!(smart.code_challenge_methods_supported, vec!["S256"]);
97    }
98
99    #[test]
100    fn test_smart_config_with_values() {
101        let config = AuthConfig {
102            expected_issuer: Some("https://idp.example.com".to_string()),
103            smart_token_endpoint: Some("https://idp.example.com/token".to_string()),
104            smart_authorize_endpoint: Some("https://idp.example.com/authorize".to_string()),
105            smart_jwks_url: Some("https://idp.example.com/.well-known/jwks.json".to_string()),
106            ..AuthConfig::default()
107        };
108        let smart = SmartConfiguration::from_config(&config);
109
110        assert_eq!(smart.issuer.as_deref(), Some("https://idp.example.com"));
111        assert_eq!(
112            smart.token_endpoint.as_deref(),
113            Some("https://idp.example.com/token")
114        );
115        assert!(
116            smart
117                .grant_types_supported
118                .contains(&"authorization_code".to_string())
119        );
120        assert!(smart.response_types_supported.contains(&"code".to_string()));
121        assert_eq!(
122            smart.jwks_uri.as_deref(),
123            Some("https://idp.example.com/.well-known/jwks.json")
124        );
125    }
126
127    #[test]
128    fn test_smart_config_json_serialization() {
129        let config = AuthConfig {
130            expected_issuer: Some("https://idp.example.com".to_string()),
131            ..AuthConfig::default()
132        };
133        let smart = SmartConfiguration::from_config(&config);
134        let json = smart.to_json();
135
136        assert!(json["capabilities"].is_array());
137        assert!(json["scopes_supported"].is_array());
138        assert_eq!(
139            json["code_challenge_methods_supported"],
140            serde_json::json!(["S256"])
141        );
142        // Fields that are None should be omitted
143        assert!(json.get("authorization_endpoint").is_none());
144    }
145}