1use crate::{AuthError, AuthResult};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
6#[serde(rename_all = "camelCase")]
7pub struct SsoProvider {
8 pub id: String,
9 pub issuer: String,
10 pub domain: String,
11 pub provider_id: String,
12 pub user_id: Option<String>,
13 pub organization_id: Option<String>,
14 pub oidc_config: Option<String>,
15 pub saml_config: Option<String>,
16 pub domain_verified: Option<bool>,
17 #[serde(flatten, default)]
18 pub additional_fields: serde_json::Map<String, serde_json::Value>,
19}
20pub struct SsoProviderUpdate {
22 pub provider: SsoProvider,
23 pub identity_changed: bool,
25 pub authentication_boundary_changed: bool,
27 pub additional_field_changes: serde_json::Map<String, serde_json::Value>,
29}
30pub type SsoProviderUpdateWork =
31 dyn Fn(&SsoProvider) -> AuthResult<SsoProviderUpdate> + Send + Sync;
32#[async_trait]
33pub trait SsoProviderStore: Send + Sync {
34 async fn mark_sso_domain_verified(&self, _id: &str, _domain: &str) -> AuthResult<bool> {
35 Err(AuthError::config(
36 "SSO persistence is not supported by this store",
37 ))
38 }
39 async fn update_sso_provider(
40 &self,
41 _id: &str,
42 _provider_id: &str,
43 _prepare: &SsoProviderUpdateWork,
44 ) -> AuthResult<Option<SsoProvider>> {
45 Err(AuthError::config(
46 "SSO persistence is not supported by this store",
47 ))
48 }
49 async fn delete_sso_provider_and_accounts(
51 &self,
52 _id: &str,
53 _provider_id: &str,
54 ) -> AuthResult<bool> {
55 Err(AuthError::config(
56 "SSO persistence is not supported by this store",
57 ))
58 }
59 async fn list_sso_providers(&self) -> AuthResult<Vec<SsoProvider>> {
60 Err(AuthError::config(
61 "SSO persistence is not supported by this store",
62 ))
63 }
64 async fn create_sso_provider(&self, _provider: SsoProvider) -> AuthResult<SsoProvider> {
65 Err(AuthError::config(
66 "SSO persistence is not supported by this store",
67 ))
68 }
69 async fn get_sso_provider(&self, _provider_id: &str) -> AuthResult<Option<SsoProvider>> {
70 Err(AuthError::config(
71 "SSO persistence is not supported by this store",
72 ))
73 }
74 async fn list_sso_providers_by_user(&self, _user_id: &str) -> AuthResult<Vec<SsoProvider>> {
75 Err(AuthError::config(
76 "SSO persistence is not supported by this store",
77 ))
78 }
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub enum SsoFieldType {
84 String,
85 Boolean,
86 Number,
87 Json,
88 Date,
89}
90impl SsoFieldType {
91 #[must_use]
92 pub fn from_config(field: &crate::field_policy::FieldConfig) -> Self {
93 match field.schema.get("type").and_then(serde_json::Value::as_str) {
94 Some("boolean") => Self::Boolean,
95 Some("number" | "integer") => Self::Number,
96 Some("object" | "array" | "json" | "string[]" | "number[]") => Self::Json,
97 Some("date") => Self::Date,
98 _ => Self::String,
99 }
100 }
101 #[must_use]
102 pub const fn sql_type(self, postgres: bool) -> &'static str {
103 match self {
104 Self::String => "TEXT",
105 Self::Boolean => {
106 if postgres {
107 "BOOLEAN"
108 } else {
109 "INTEGER"
110 }
111 }
112 Self::Number => "INTEGER",
113 Self::Json => {
114 if postgres {
115 "JSONB"
116 } else {
117 "TEXT"
118 }
119 }
120 Self::Date => {
121 if postgres {
122 "TIMESTAMPTZ"
123 } else {
124 "DATE"
125 }
126 }
127 }
128 }
129}
130#[must_use]
132pub fn sso_field_column<'a>(name: &'a str, field: &'a crate::field_policy::FieldConfig) -> &'a str {
133 field.field_name.as_deref().unwrap_or(name)
134}
135
136pub fn validate_sso_fields(config: &crate::config::AdvancedDatabaseConfig) -> AuthResult<()> {
138 const BUILT_IN: &[&str] = &[
139 "id",
140 "issuer",
141 "domain",
142 "providerId",
143 "userId",
144 "organizationId",
145 "oidcConfig",
146 "samlConfig",
147 "domainVerified",
148 ];
149 const RESPONSE: &[&str] = &[
150 "type",
151 "redirectURI",
152 "domainVerificationToken",
153 "spMetadataUrl",
154 ];
155 let mapped = config.sso_provider.as_ref();
156 for (name, field) in &config.sso_provider_fields.0 {
157 if BUILT_IN.contains(&name.as_str()) {
158 return Err(AuthError::config(format!(
159 "ssoProvider additional field \"{name}\" conflicts with a built-in field"
160 )));
161 }
162 if RESPONSE.contains(&name.as_str()) {
163 return Err(AuthError::config(format!(
164 "ssoProvider additional field \"{name}\" conflicts with a returned provider field"
165 )));
166 }
167 let physical = sso_field_column(name, field);
168 if physical.is_empty()
169 || BUILT_IN.iter().any(|canonical| {
170 let configured = mapped
171 .and_then(|mapping| {
172 mapping
173 .columns
174 .iter()
175 .find(|(key, _)| crate::config::sso_provider_column(key) == *canonical)
176 .map(|(_, value)| value.as_str())
177 })
178 .unwrap_or(canonical);
179 configured == physical
180 })
181 {
182 return Err(AuthError::config(format!(
183 "ssoProvider additional field \"{name}\" maps to built-in field \"{physical}\""
184 )));
185 }
186 }
187 Ok(())
188}
189pub fn sso_field_definition(
191 name: &str,
192 field: &crate::field_policy::FieldConfig,
193 postgres: bool,
194) -> AuthResult<String> {
195 use crate::field_policy::FieldDefault;
196 let quote = |value: &str| format!("\"{}\"", value.replace('"', "\"\""));
197 let mut definition = format!(
198 "{} {}",
199 quote(sso_field_column(name, field)),
200 SsoFieldType::from_config(field).sql_type(postgres)
201 );
202 if let Some(FieldDefault::Value(value)) = &field.default {
203 let default = match value {
204 crate::utils::json::JsValue::Null => "NULL".to_owned(),
205 crate::utils::json::JsValue::Bool(value) => {
206 if postgres {
207 value.to_string()
208 } else {
209 if *value { "1" } else { "0" }.to_owned()
210 }
211 }
212 crate::utils::json::JsValue::Number(value) if value.is_finite() => value.to_string(),
213 crate::utils::json::JsValue::String(value) => {
214 format!("'{}'", value.replace('\'', "''"))
215 }
216 value => format!("'{}'", serde_json::to_string(value)?.replace('\'', "''")),
217 };
218 definition.push_str(" DEFAULT ");
219 definition.push_str(&default);
220 }
221 if field.required {
222 definition.push_str(" NOT NULL");
223 }
224 Ok(definition)
225}
226
227pub async fn prepare_sso_fields(
229 fields: &crate::field_policy::SessionFields,
230 input: &serde_json::Map<String, serde_json::Value>,
231 create: bool,
232) -> AuthResult<serde_json::Map<String, serde_json::Value>> {
233 let policy = crate::field_policy::SessionAdapterFields(std::sync::Arc::new(fields.0.clone()));
234 let mut values: crate::field_policy::FieldValues = input
235 .iter()
236 .map(|(key, value)| {
237 (
238 key.clone(),
239 crate::utils::json::JsValue::from(value.clone()),
240 )
241 })
242 .collect();
243 policy.attach(&mut values, create);
244 values.apply_adapter_transforms_async().await?;
245 values
246 .into_iter()
247 .map(|(key, value)| Ok((key, value.to_json_value()?)))
248 .collect()
249}
250pub async fn project_sso_provider(
252 fields: &crate::field_policy::SessionFields,
253 mut provider: SsoProvider,
254) -> AuthResult<SsoProvider> {
255 let policy = crate::field_policy::SessionAdapterFields(std::sync::Arc::new(fields.0.clone()));
256 let output = policy
257 .output(std::mem::take(&mut provider.additional_fields))
258 .await?;
259 provider.additional_fields.clone_from(output.values());
260 for (name, field) in &fields.0 {
262 if SsoFieldType::from_config(field) == SsoFieldType::Date
263 && let Some(value) = provider.additional_fields.get_mut(name)
264 && let Some(text) = value.as_str()
265 {
266 *value = chrono::DateTime::parse_from_rfc3339(text).map_or(
267 serde_json::Value::Null,
268 |date| {
269 serde_json::Value::String(
270 date.with_timezone(&chrono::Utc)
271 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
272 )
273 },
274 );
275 }
276 }
277 Ok(provider)
278}