Skip to main content

alibi_plugins/sso/
provider_read.rs

1//! Authorized provider configuration without application credentials.
2use super::{
3    AuthContext, AuthError, AuthResult, AuthSchema, AuthUser, EndpointResponse, JsValue,
4    ServerEndpoint, SessionRequest, SsoPlugin, SsoProvider, Value, api, json,
5};
6impl SsoPlugin {
7    pub fn get_sso_provider_endpoint(provider_id: &str) -> AuthResult<ServerEndpoint<Value>> {
8        ServerEndpoint::new("sso", "getSSOProvider").with_query(&json!({"providerId":provider_id}))
9    }
10    pub(super) async fn read<S: AuthSchema>(
11        &self,
12        input: &JsValue,
13        request: &impl SessionRequest,
14        ctx: &AuthContext<S>,
15    ) -> AuthResult<EndpointResponse> {
16        let provider = self.authorized_provider(input, request, ctx).await?;
17        EndpointResponse::json(&sanitize(&provider, ctx)?)
18    }
19    pub(super) async fn authorized_provider<S: AuthSchema>(
20        &self,
21        input: &JsValue,
22        request: &impl SessionRequest,
23        ctx: &AuthContext<S>,
24    ) -> AuthResult<SsoProvider> {
25        let (user, _) = super::super::helpers::ordinary_session(request, ctx).await?;
26        let provider_id = input
27            .get("providerId")
28            .and_then(JsValue::as_str)
29            .ok_or_else(|| super::super::endpoint::validation("Invalid providerId"))?;
30        self.authorize_provider_id(provider_id, user.id().as_ref(), ctx)
31            .await
32    }
33    pub(super) async fn authorize_provider_id<S: AuthSchema>(
34        &self,
35        provider_id: &str,
36        user_id: &str,
37        ctx: &AuthContext<S>,
38    ) -> AuthResult<SsoProvider> {
39        let provider = ctx
40            .database
41            .get_sso_provider(provider_id)
42            .await?
43            .ok_or_else(|| api(404, "Provider not found"))?;
44        let allowed = if let Some(organization_id) = provider
45            .organization_id
46            .as_deref()
47            .filter(|id| !id.is_empty())
48        {
49            if ctx
50                .get_metadata(super::super::organization::METADATA_ENABLED)
51                .is_some()
52            {
53                ctx.database
54                    .get_member(organization_id, user_id)
55                    .await?
56                    .is_some_and(|member| {
57                        member
58                            .role
59                            .split(',')
60                            .any(|role| matches!(role.trim(), "owner" | "admin"))
61                    })
62            } else {
63                provider.user_id.as_deref() == Some(user_id)
64            }
65        } else {
66            provider.user_id.as_deref() == Some(user_id)
67        };
68        if !allowed {
69            return Err(api(403, "You don't have access to this provider"));
70        }
71        Ok(provider)
72    }
73}
74pub(super) fn sanitize<S: AuthSchema>(
75    provider: &SsoProvider,
76    ctx: &AuthContext<S>,
77) -> AuthResult<Value> {
78    let saml = provider
79        .saml_config
80        .as_deref()
81        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
82        .filter(|value| !value.is_null());
83    let base = format!(
84        "{}{}",
85        ctx.config.base_url.trim_end_matches('/'),
86        ctx.config.base_path.trim_end_matches('/')
87    );
88    let mut output = json!({"providerId":provider.provider_id,"type":if saml.is_some(){"saml"}else{"oidc"},"issuer":provider.issuer,"domain":provider.domain,"organizationId":provider.organization_id.as_deref().filter(|id|!id.is_empty()),"domainVerified":provider.domain_verified.unwrap_or(false),"spMetadataUrl":format!("{base}/sso/saml2/sp/metadata?providerId={}",component(&provider.provider_id))});
89    if let Some(config) = saml {
90        let mut safe = serde_json::Map::new();
91        for field in [
92            "entryPoint",
93            "callbackUrl",
94            "idpInitiatedCallbackUrl",
95            "audience",
96            "wantAssertionsSigned",
97            "authnRequestsSigned",
98            "identifierFormat",
99            "signatureAlgorithm",
100            "digestAlgorithm",
101        ] {
102            if let Some(value) = config.get(field) {
103                _ = safe.insert(field.into(), value.clone());
104            }
105        }
106        if let Some(certs) = config
107            .get("idpMetadata")
108            .and_then(|metadata| metadata.get("cert"))
109            .filter(|cert| !cert.is_null())
110            .or_else(|| config.get("cert"))
111        {
112            let certs = certs
113                .as_array()
114                .map_or_else(|| vec![certs], |certs| certs.iter().collect());
115            let details = certs
116                .into_iter()
117                .map(|cert| {
118                    cert.as_str().map_or_else(
119                        || json!({"error":"Failed to parse certificate"}),
120                        certificate_details,
121                    )
122                })
123                .collect::<Vec<_>>();
124            _ = safe.insert("certificate".into(), json!(details));
125        }
126        let object = output
127            .as_object_mut()
128            .ok_or_else(|| AuthError::internal("SSO provider serialization failed"))?;
129        _ = object.insert("samlConfig".into(), Value::Object(safe));
130    }
131    if let Some(config) = provider
132        .oidc_config
133        .as_deref()
134        .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
135        .filter(|value| !value.is_null())
136    {
137        let mut safe = serde_json::Map::new();
138        for field in [
139            "discoveryEndpoint",
140            "pkce",
141            "authorizationEndpoint",
142            "tokenEndpoint",
143            "userInfoEndpoint",
144            "jwksEndpoint",
145            "scopes",
146            "tokenEndpointAuthentication",
147        ] {
148            if let Some(value) = config.get(field) {
149                _ = safe.insert(field.into(), value.clone());
150            }
151        }
152        let client_id = config
153            .get("clientId")
154            .and_then(Value::as_str)
155            .ok_or_else(|| AuthError::internal("Stored OIDC configuration has no clientId"))?;
156        let units: Vec<u16> = client_id.encode_utf16().collect();
157        let last = units
158            .get(units.len().saturating_sub(4)..)
159            .unwrap_or_default();
160        let masked = if units.len() <= 4 {
161            "****".into()
162        } else {
163            format!("****{}", String::from_utf16_lossy(last))
164        };
165        _ = safe.insert("clientIdLastFour".into(), json!(masked));
166        let object = output
167            .as_object_mut()
168            .ok_or_else(|| AuthError::internal("SSO provider serialization failed"))?;
169        _ = object.insert("oidcConfig".into(), Value::Object(safe));
170    }
171    if let Some(object) = output.as_object_mut() {
172        for (name, field) in &ctx.config.advanced.database.sso_provider_fields.0 {
173            if field.returned
174                && let Some(value) = provider.additional_fields.get(name)
175            {
176                _ = object.insert(name.clone(), value.clone());
177            }
178        }
179    }
180    Ok(output)
181}
182fn component(value: &str) -> String {
183    urlencoding::encode(value)
184        .replace("%21", "!")
185        .replace("%27", "'")
186        .replace("%28", "(")
187        .replace("%29", ")")
188        .replace("%2A", "*")
189}
190pub(super) fn validate_query(value: Option<&JsValue>) -> AuthResult<JsValue> {
191    super::super::endpoint::validate_fields(
192        value,
193        "query",
194        &[super::super::authentication_helpers::JsonField::string(
195            "providerId",
196            true,
197        )],
198    )
199}
200
201fn certificate_details(pem: &str) -> Value {
202    use base64::Engine as _;
203    use sha2::Digest as _;
204    let parse = || -> Option<Value> {
205        let encoded = if pem.contains("-----BEGIN") {
206            pem.split_once("-----BEGIN CERTIFICATE-----")?
207                .1
208                .split_once("-----END CERTIFICATE-----")?
209                .0
210        } else {
211            pem
212        };
213        let encoded: String = encoded.chars().filter(|ch| !ch.is_whitespace()).collect();
214        let der = base64::engine::general_purpose::STANDARD
215            .decode(encoded)
216            .ok()?;
217        let (_, certificate) = x509_parser::parse_x509_certificate(&der).ok()?;
218        let fingerprint = sha2::Sha256::digest(&der)
219            .iter()
220            .map(|byte| format!("{byte:02X}"))
221            .collect::<Vec<_>>()
222            .join(":");
223        let date = |timestamp| {
224            chrono::DateTime::from_timestamp(timestamp, 0)
225                .map(|time| time.format("%b %e %H:%M:%S %Y GMT").to_string())
226        };
227        let algorithm = match certificate
228            .public_key()
229            .algorithm
230            .algorithm
231            .to_id_string()
232            .as_str()
233        {
234            "1.2.840.113549.1.1.1" => "RSA",
235            "1.2.840.113549.1.1.10" => "RSA-PSS",
236            "1.2.840.10045.2.1" => "EC",
237            "1.2.840.10040.4.1" => "DSA",
238            "1.3.101.112" => "ED25519",
239            "1.3.101.113" => "ED448",
240            "1.3.101.110" => "X25519",
241            "1.3.101.111" => "X448",
242            "1.2.840.113549.1.3.1" | "1.2.840.10046.2.1" => "DH",
243            _ => "UNKNOWN",
244        };
245        Some(json!({"fingerprintSha256":fingerprint,
246            "notBefore":date(certificate.validity().not_before.timestamp())?,
247            "notAfter":date(certificate.validity().not_after.timestamp())?,
248            "publicKeyAlgorithm":algorithm}))
249    };
250    parse().unwrap_or_else(|| json!({"error":"Failed to parse certificate"}))
251}