Skip to main content

better_auth_api/plugins/email_password/
signup.rs

1use crate::plugins::{
2    email_verification::EmailVerificationPlugin,
3    helpers::{apply_default_role, apply_user_create_fields},
4};
5use async_trait::async_trait;
6use better_auth_core::utils::password as password_utils;
7use better_auth_core::{
8    AuthContext, AuthError, AuthRequest, AuthResult, AuthSchema, AuthSession, AuthUser,
9    CreateAccount, CreateSession, CreateUser, FieldMap, RequestMeta, SchemaField, wire::UserView,
10};
11use serde_json::Value;
12
13use super::{EmailPasswordConfig, SignUpRequest, SignUpResponse};
14
15/// Notification for a duplicate signup protected against account enumeration.
16#[async_trait]
17pub trait OnExistingUserSignUp: Send + Sync {
18    /// Inspect the existing account and the signup request without returning that account to the client.
19    async fn on_existing_user_sign_up(
20        &self,
21        user: &UserView,
22        request: Option<&AuthRequest>,
23    ) -> AuthResult<()>;
24}
25
26/// Input to the synchronous enumeration-safe user factory.
27pub struct SyntheticUserInput {
28    /// Submitted core fields and fresh timestamps. The account identifier is provided separately.
29    pub core_fields: FieldMap,
30    /// Parsed fields from the application's user schema, excluding plugin-owned fields.
31    pub additional_fields: FieldMap,
32    /// Fresh identifier that is never persisted.
33    pub id: String,
34}
35
36/// Customize a synthetic signup user before the public user schema filters the response.
37pub type CustomSyntheticUser = dyn Fn(SyntheticUserInput) -> AuthResult<FieldMap> + Send + Sync;
38
39type SignUpResult = SignUpResponse;
40
41pub(super) fn synthetic_response<S: AuthSchema>(
42    body: &SignUpRequest,
43    create: &CreateUser,
44    config: &EmailPasswordConfig,
45    ctx: &AuthContext<S>,
46) -> AuthResult<SignUpResult> {
47    let now = better_auth_core::FieldDate::from(chrono::Utc::now());
48    let mut core = FieldMap::from_iter([
49        ("name".into(), body.name.clone().into()),
50        ("email".into(), body.email.to_lowercase().into()),
51        ("emailVerified".into(), false.into()),
52        ("image".into(), body.image.clone().into_field()),
53        ("createdAt".into(), now.clone().into()),
54        ("updatedAt".into(), now.into()),
55    ]);
56    let id = ctx
57        .config
58        .advanced
59        .generate_id("user", None)?
60        .filter(|id| !id.is_empty())
61        .unwrap_or_else(|| better_auth_core::id::random_id(None));
62    let data = if let Some(factory) = &config.custom_synthetic_user {
63        factory(SyntheticUserInput {
64            core_fields: core,
65            additional_fields: create.additional_fields.clone(),
66            id,
67        })?
68    } else {
69        core.extend(create.additional_fields.clone());
70        for (name, value) in [
71            ("username", create.username.as_ref()),
72            ("displayUsername", create.display_username.as_ref()),
73        ] {
74            if let Some(value) = value {
75                let _ = core.insert(name.into(), value.clone().into_field());
76            }
77        }
78        if let Some(phone) = &create.phone_number {
79            let _ = core.insert("phoneNumber".into(), phone.clone().into());
80        }
81        let _ = core.insert("id".into(), id.into());
82        core
83    };
84    Ok(SignUpResponse {
85        token: None,
86        user: UserView::synthetic_output(data, &ctx.config.user, &ctx.metadata)?,
87    })
88}
89
90/// Core sign-up logic.
91///
92/// Issue the session before transaction after hooks run, as in the upstream signup endpoint.
93pub(super) async fn sign_up_core<S: AuthSchema>(
94    body: &SignUpRequest,
95    endpoint_body: Value,
96    config: &EmailPasswordConfig,
97    req: &AuthRequest,
98    ctx: &AuthContext<S>,
99) -> AuthResult<SignUpResult> {
100    if !config.enable_signup {
101        return Err(AuthError::forbidden("User registration is not enabled"));
102    }
103
104    password_utils::validate_password(
105        &body.password,
106        ctx.password_policy.min_length,
107        ctx.password_policy.max_length,
108        ctx,
109    )?;
110
111    let mut create_user = CreateUser::new()
112        .with_email(body.email.to_lowercase())
113        .with_name(&body.name);
114    apply_user_create_fields(ctx, &body.additional_fields, &mut create_user)?;
115    create_user.image = body
116        .image
117        .clone()
118        .map(|value| Some(value).into())
119        .unwrap_or_default();
120    let protect_enumeration = config.require_email_verification || !config.auto_sign_in();
121    if let Some(user) = ctx
122        .database
123        .get_user_by_email(&body.email.to_lowercase())
124        .await?
125    {
126        if !protect_enumeration {
127            return Err(AuthError::UnprocessableEntity(
128                "User already exists. Use another email.".into(),
129            ));
130        }
131        let body = body.clone();
132        let config = config.clone();
133        let request = req.clone();
134        let auth = ctx.clone();
135        return better_auth_core::store::transaction(ctx.database.as_ref(), move |tx| {
136            Box::pin(async move {
137                let _ = password_utils::hash_password(
138                    auth.password_policy.hasher.as_ref(),
139                    &body.password,
140                )
141                .await?;
142                let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
143                    Some(&request),
144                    better_auth_core::FieldValue::from_json(endpoint_body)?,
145                    &auth,
146                );
147                endpoint.transaction = Some(tx);
148                let task = super::callbacks::delivery(&config, &user, &endpoint)?;
149                better_auth_core::background::run_or_await(
150                    task,
151                    auth.config.advanced.background_tasks.as_ref(),
152                    &auth.config.logger,
153                )
154                .await;
155                synthetic_response(&body, &create_user, &config, &auth)
156            })
157        })
158        .await;
159    }
160    let password_hash =
161        password_utils::hash_password(ctx.password_policy.hasher.as_ref(), &body.password).await?;
162    let synthetic_create = create_user.clone();
163    apply_default_role(ctx, &mut create_user);
164    let auto_sign_in = config.auto_sign_in() && !config.require_email_verification;
165    let meta = RequestMeta::from_request_with_config(req, &ctx.config.advanced.ip_address);
166    let expires_in = if body.remember_me == Some(false) {
167        chrono::Duration::days(1)
168    } else {
169        ctx.config.session.expires_in()
170    };
171    let ip_address = meta.ip_address.clone();
172    let user_agent = meta.user_agent.clone();
173    let database = ctx.database.clone();
174    let transaction_database = database.clone();
175    let user_config = ctx.config.user.clone();
176    let adapter_user_config = ctx.adapter_user_fields().clone();
177    let user_metadata = ctx.metadata.clone();
178    let supports_native_json = database.supports_native_json();
179
180    let signup_callback_url = body.callback_url.clone();
181    let require_email_verification = config.require_email_verification;
182    let dont_remember = body.remember_me == Some(false);
183    let admission_context = ctx.clone();
184    let admission_request = req.clone();
185    let mut admission_input = synthetic_create.clone();
186    admission_input.email_verified = Some(false);
187    let result: Option<SignUpResult> =
188        better_auth_core::store::transaction(database.as_ref(), move |tx| {
189            let _database = transaction_database.clone();
190            Box::pin(async move {
191                let mut endpoint = crate::plugins::endpoint_context::EndpointContext::new(
192                    Some(&admission_request),
193                    better_auth_core::FieldValue::from_json(endpoint_body)?,
194                    &admission_context,
195                );
196                endpoint.transaction = Some(tx);
197                let admitted = crate::plugins::user_admission::validate_create(
198                    &admission_input,
199                    crate::plugins::user_admission::UserValidationSource::new(
200                        "email-password",
201                        crate::plugins::user_admission::UserValidationAction::CreateUser,
202                    ),
203                    &endpoint,
204                )
205                .await;
206                if let Err(error) = admitted {
207                    if protect_enumeration {
208                        return Ok(None);
209                    }
210                    return Err(error.into_auth_error());
211                }
212                let user = match tx.create_user(create_user).await {
213                    Ok(user) => user,
214                    Err(error) if protect_enumeration && error.status_code() == 403 => {
215                        return Ok(None);
216                    }
217                    Err(AuthError::Database(_)) => {
218                        return Err(AuthError::UnprocessableEntity(
219                            "Failed to create user".to_string(),
220                        ));
221                    }
222                    Err(error) => return Err(error),
223                };
224
225                let _ = tx
226                    .create_account(CreateAccount {
227                        user_id: user.id().into_owned(),
228                        account_id: user.id().into_owned(),
229                        provider_id: ("credential".to_string()).into(),
230                        access_token: Default::default(),
231                        refresh_token: Default::default(),
232                        id_token: Default::default(),
233                        access_token_expires_at: Default::default(),
234                        refresh_token_expires_at: Default::default(),
235                        scope: Default::default(),
236                        password: (Some(password_hash.clone()))
237                            .map(|value| better_auth_core::SchemaValue::Typed(Some(value)))
238                            .unwrap_or_default(),
239                        ..Default::default()
240                    })
241                    .await?;
242
243                if let Some(verification) =
244                    EmailVerificationPlugin::from_context(&admission_context)
245                {
246                    verification
247                        .send_verification_on_sign_up(
248                            &user,
249                            require_email_verification,
250                            signup_callback_url.as_deref(),
251                            &endpoint,
252                        )
253                        .await?;
254                }
255
256                if auto_sign_in {
257                    let session = tx
258                        .create_session(CreateSession {
259                            additional_fields: Default::default(),
260                            user_id: user.id().into_owned(),
261                            expires_at: (chrono::Utc::now() + expires_in).into(),
262                            ip_address,
263                            user_agent,
264                            impersonated_by: None,
265                            active_organization_id: None,
266                        })
267                        .await?;
268                    let token = session.token().to_string();
269
270                    let manager = admission_context.session_manager();
271                    let data = manager.internal_data(&user, &session).await?;
272                    manager
273                        .set_session_cookie_in_transaction(
274                            &admission_request,
275                            data,
276                            Some(dont_remember),
277                            tx,
278                        )
279                        .await?;
280                    Ok(Some(SignUpResponse {
281                        token: Some(token),
282                        user: UserView::with_field_policies(
283                            &user,
284                            &adapter_user_config,
285                            &user_config,
286                            &user_metadata,
287                            supports_native_json,
288                        )
289                        .await?
290                        .into(),
291                    }))
292                } else {
293                    Ok(Some(SignUpResponse {
294                        token: None,
295                        user: UserView::with_field_policies(
296                            &user,
297                            &adapter_user_config,
298                            &user_config,
299                            &user_metadata,
300                            supports_native_json,
301                        )
302                        .await?
303                        .into(),
304                    }))
305                }
306            })
307        })
308        .await?;
309    let Some(response) = result else {
310        return synthetic_response(body, &synthetic_create, config, ctx);
311    };
312    Ok(response)
313}