1use crate::config::AuthConfig;
3use crate::error::{AuthError, AuthResult};
4use crate::hooks::{RequestHookContext, current_request_hook_context};
5use crate::types::CreateUser;
6use async_trait::async_trait;
7use chrono::Utc;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum UserValidationAction {
14 #[default]
15 CreateUser,
16 LinkAccount,
17 SignIn,
18}
19
20#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct UserValidationProvider {
24 pub provider_id: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
28 pub profile: Option<serde_json::Value>,
29}
30
31#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct UserValidationSource {
36 #[serde(default)]
37 pub method: String,
38 #[serde(default)]
39 pub action: UserValidationAction,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub oauth: Option<UserValidationProvider>,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub sso: Option<UserValidationProvider>,
44}
45
46impl UserValidationSource {
47 #[must_use]
48 pub fn creation(method: impl Into<String>) -> Self {
49 Self {
50 method: method.into(),
51 action: UserValidationAction::CreateUser,
52 oauth: None,
53 sso: None,
54 }
55 }
56
57 #[must_use]
58 pub fn oauth(
59 provider_id: impl Into<String>,
60 profile: &serde_json::Value,
61 action: UserValidationAction,
62 ) -> Self {
63 Self {
64 method: "oauth".into(),
65 action,
66 oauth: Some(UserValidationProvider {
67 provider_id: provider_id.into(),
68 profile: profile.is_object().then(|| profile.clone()),
69 }),
70 sso: None,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
78pub struct UserValidationData {
79 pub user: CreateUser,
80 pub source: UserValidationSource,
81}
82
83#[derive(Debug, Clone)]
86pub struct UserValidationRejection {
87 pub error: String,
88 pub error_description: Option<String>,
89}
90
91#[derive(Debug, Clone)]
94pub struct UserValidationContext {
95 pub request: Option<RequestHookContext>,
96 pub endpoint: Option<crate::endpoint::EndpointCall>,
97}
98
99#[async_trait]
100pub trait UserInfoValidator: Send + Sync {
101 async fn validate(
105 &self,
106 data: &mut UserValidationData,
107 context: &UserValidationContext,
108 ) -> AuthResult<Option<UserValidationRejection>>;
109}
110
111fn denial(code: &str, message: &str) -> AuthError {
112 AuthError::Api {
113 status: 403,
114 code: Some(code.into()),
115 message: message.into(),
116 }
117}
118
119fn assert_source(source: Option<&UserValidationSource>) -> AuthResult<()> {
120 let Some(source) = source.filter(|source| !source.method.is_empty()) else {
121 return Err(denial(
122 "validation_source_missing",
123 "User validation source is required",
124 ));
125 };
126 if source.method == "oauth"
127 && source
128 .oauth
129 .as_ref()
130 .is_none_or(|provider| provider.provider_id.is_empty())
131 {
132 return Err(denial(
133 "validation_source_missing",
134 "OAuth user validation source requires oauth.providerId",
135 ));
136 }
137 if matches!(source.method.as_str(), "sso-oidc" | "sso-saml")
138 && source
139 .sso
140 .as_ref()
141 .is_none_or(|provider| provider.provider_id.is_empty())
142 {
143 return Err(denial(
144 "validation_source_missing",
145 "SSO user validation source requires sso.providerId",
146 ));
147 }
148 Ok(())
149}
150
151pub async fn validate_user_info(
154 config: &AuthConfig,
155 data: &mut UserValidationData,
156) -> AuthResult<()> {
157 let Some(validator) = &config.user_validation else {
158 return Ok(());
159 };
160 assert_source(Some(&data.source))?;
161 let context = UserValidationContext {
162 request: current_request_hook_context(),
163 endpoint: crate::endpoint::current_endpoint_call_context(),
164 };
165 if context.request.is_none() && context.endpoint.is_none() {
166 return Err(denial(
167 "validation_context_missing",
168 "User validation requires an endpoint context",
169 ));
170 }
171 match validator.validate(data, &context).await {
172 Ok(Some(rejection)) if !rejection.error.is_empty() => {
173 let message = rejection
174 .error_description
175 .as_deref()
176 .filter(|value| !value.is_empty())
177 .unwrap_or(&rejection.error);
178 Err(denial(&rejection.error, message))
179 }
180 Ok(_) => Ok(()),
181 Err(_) => Err(denial("validation_failed", "User validation failed")),
182 }
183}
184
185pub struct PreparedUserCreation {
189 data: CreateUser,
190 defaults: crate::store::UserCreationDefaults,
191}
192
193impl PreparedUserCreation {
194 #[must_use]
195 pub fn into_data(self) -> CreateUser {
196 self.data
197 }
198
199 #[must_use]
202 pub fn into_parts(self) -> (CreateUser, crate::store::UserCreationDefaults) {
203 (self.data, self.defaults)
204 }
205
206 pub(crate) fn from_data(data: CreateUser) -> Self {
207 Self {
208 data,
209 defaults: crate::store::UserCreationDefaults::default(),
210 }
211 }
212
213 pub(crate) fn with_defaults(mut self, defaults: crate::store::UserCreationDefaults) -> Self {
214 self.defaults = defaults;
215 self
216 }
217}
218
219pub(crate) async fn prepare_creation(
220 config: &AuthConfig,
221 mut user: CreateUser,
222 source: Option<UserValidationSource>,
223) -> AuthResult<PreparedUserCreation> {
224 assert_source(source.as_ref())?;
225 let mut source = source.ok_or_else(|| {
226 denial(
227 "validation_source_missing",
228 "User validation source is required",
229 )
230 })?;
231 source.action = UserValidationAction::CreateUser;
232 let now = Utc::now();
233 _ = user.created_at.get_or_insert(now);
234 _ = user.updated_at.get_or_insert(now);
235 user.email = user
236 .email
237 .map(|email| crate::utils::normalize_user_email(&email));
238 let mut data = UserValidationData { user, source };
239 validate_user_info(config, &mut data).await?;
240 if data.user.created_at.is_none() || data.user.updated_at.is_none() {
241 return Err(AuthError::NotImplemented(
242 "Identity policy removal of creation timestamps is not supported".into(),
243 ));
244 }
245 Ok(PreparedUserCreation::from_data(data.user))
246}