1use 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}