better_auth_api/plugins/email_verification/
mod.rs1use async_trait::async_trait;
2use chrono::Duration;
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6
7use better_auth_core::AuthUser;
8use better_auth_core::wire::UserView;
9use better_auth_core::{AuthContext, AuthError, AuthResult};
10use better_auth_core::{AuthRequest, AuthResponse};
11
12use better_auth_core::utils::cookie_utils::create_session_cookie;
13
14use super::StatusResponse;
15
16pub(super) mod handlers;
17pub(crate) mod token;
18pub(super) mod types;
19
20#[cfg(test)]
21mod tests;
22
23use handlers::*;
24use types::*;
25
26#[async_trait]
31pub trait SendVerificationEmail: Send + Sync {
32 async fn send(&self, user: &UserView, url: &str, token: &str) -> AuthResult<()>;
33}
34
35pub type EmailVerificationHook =
39 Arc<dyn Fn(&UserView) -> Pin<Box<dyn Future<Output = AuthResult<()>> + Send>> + Send + Sync>;
40
41pub struct EmailVerificationPlugin {
43 config: EmailVerificationConfig,
44}
45
46#[derive(better_auth_core::PluginConfig)]
47#[plugin(name = "EmailVerificationPlugin")]
48pub struct EmailVerificationConfig {
49 #[config(default = Duration::hours(24))]
51 pub verification_token_expiry: Duration,
52 #[config(default = true)]
54 pub send_email_notifications: bool,
55 #[config(default = false)]
57 pub require_verification_for_signin: bool,
58 #[config(default = false)]
60 pub auto_verify_new_users: bool,
61 #[config(default = false)]
64 pub send_on_sign_in: bool,
65 #[config(default = false)]
68 pub auto_sign_in_after_verification: bool,
69 #[config(default = None, skip)]
72 pub send_verification_email: Option<Arc<dyn SendVerificationEmail>>,
73 #[config(default = None)]
75 pub before_email_verification: Option<EmailVerificationHook>,
76 #[config(default = None)]
78 pub after_email_verification: Option<EmailVerificationHook>,
79}
80
81impl EmailVerificationPlugin {
82 pub fn custom_send_verification_email(
83 mut self,
84 sender: Arc<dyn SendVerificationEmail>,
85 ) -> Self {
86 self.config.send_verification_email = Some(sender);
87 self
88 }
89}
90
91better_auth_core::impl_auth_plugin! {
92 EmailVerificationPlugin, "email-verification";
93 routes {
94 post "/send-verification-email" => handle_send_verification_email, "send_verification_email";
95 get "/verify-email" => handle_verify_email, "verify_email";
96 }
97 extra {
98 async fn on_user_created(&self, user: &S::User, ctx: &AuthContext<S>) -> AuthResult<()> {
99 if (self.config.send_email_notifications || self.config.send_verification_email.is_some())
102 && !user.email_verified()
103 && let Some(email) = user.email()
104 && let Err(e) = self
105 .send_verification_email_for_user(user, email, None, ctx)
106 .await
107 {
108 tracing::warn!(
109 email = %email,
110 error = %e,
111 "Failed to send verification email"
112 );
113 }
114 Ok(())
115 }
116 }
117}
118
119impl EmailVerificationPlugin {
124 async fn handle_send_verification_email(
125 &self,
126 req: &AuthRequest,
127 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
128 ) -> AuthResult<AuthResponse> {
129 let body: SendVerificationEmailRequest = match better_auth_core::validate_request_body(req)
130 {
131 Ok(v) => v,
132 Err(resp) => return Ok(resp),
133 };
134 let current_user = ctx.require_session(req).await.ok().map(|(user, _)| user);
135 let response =
136 send_verification_email_core(&body, current_user.as_ref(), &self.config, ctx).await?;
137 Ok(AuthResponse::json(200, &response)?)
138 }
139
140 async fn handle_verify_email(
141 &self,
142 req: &AuthRequest,
143 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
144 ) -> AuthResult<AuthResponse> {
145 let token = req
146 .query
147 .get("token")
148 .ok_or_else(|| AuthError::bad_request("Verification token is required"))?;
149 let callback_url = req.query.get("callbackURL").cloned();
150
151 if let Some(ref url) = callback_url
154 && !ctx.config.advanced.disable_origin_check
155 && !ctx.config.is_redirect_target_trusted(url)
156 {
157 return Ok(AuthError::forbidden("Invalid callbackURL").to_auth_response());
158 }
159
160 let query = VerifyEmailQuery {
161 token: token.clone(),
162 callback_url,
163 };
164
165 let ip_address = req.headers.get("x-forwarded-for").cloned();
166 let user_agent = req.headers.get("user-agent").cloned();
167 let current_session = ctx.require_session(req).await.ok();
168
169 match verify_email_core(
170 &query,
171 current_session,
172 &self.config,
173 ip_address,
174 user_agent,
175 ctx,
176 )
177 .await?
178 {
179 VerifyEmailResult::Redirect { url, session_token } => {
180 let mut headers = better_auth_core::Headers::new();
181 _ = headers.insert("Location".to_string(), url);
182 _ = headers.insert("content-type".to_string(), "application/json".to_string());
183 if let Some(token) = session_token {
184 let cookie = create_session_cookie(&token, &ctx.config);
185 headers.append("Set-Cookie".to_string(), cookie);
186 }
187 Ok(AuthResponse {
188 status: 302,
189 headers,
190 body: Vec::new(),
191 })
192 }
193 VerifyEmailResult::Json {
194 body,
195 session_token,
196 } => {
197 let mut response = AuthResponse::json(200, &body)?;
198 if let Some(token) = session_token {
199 let cookie = create_session_cookie(&token, &ctx.config);
200 response = response.with_header("Set-Cookie", cookie);
201 }
202 Ok(response)
203 }
204 }
205 }
206
207 async fn send_verification_email_for_user(
213 &self,
214 user: &impl AuthUser,
215 email: &str,
216 callback_url: Option<&str>,
217 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
218 ) -> AuthResult<()> {
219 let verification_token = token::create_email_verification_token(
220 &ctx.config.secret,
221 email,
222 None,
223 self.config.verification_token_expiry,
224 None,
225 )?;
226 let callback_url = callback_url.unwrap_or("/");
227 let verification_url = format!(
228 "{}/verify-email?token={}&callbackURL={}",
229 ctx.config.base_url,
230 verification_token,
231 urlencoding::encode(callback_url),
232 );
233
234 if let Some(ref custom_sender) = self.config.send_verification_email {
236 let user = UserView::from(user);
237 custom_sender
238 .send(&user, &verification_url, &verification_token)
239 .await?;
240 } else if self.config.send_email_notifications {
241 if ctx.email_provider.is_some() {
243 let subject = "Verify your email address";
244 let html = format!(
245 "<p>Click the link below to verify your email address:</p>\
246 <p><a href=\"{url}\">Verify Email</a></p>",
247 url = verification_url
248 );
249 let text = format!("Verify your email address: {}", verification_url);
250
251 ctx.email_provider()?
252 .send(email, subject, &html, &text)
253 .await?;
254 } else {
255 tracing::warn!(
256 email = %email,
257 "No email provider configured, skipping verification email"
258 );
259 }
260 }
261
262 Ok(())
263 }
264
265 pub async fn send_verification_on_sign_in(
271 &self,
272 user: &impl AuthUser,
273 callback_url: Option<&str>,
274 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
275 ) -> AuthResult<()> {
276 if !self.config.send_on_sign_in {
277 return Ok(());
278 }
279
280 if user.email_verified() {
281 return Ok(());
282 }
283
284 if let Some(email) = user.email() {
285 self.send_verification_email_for_user(user, email, callback_url, ctx)
286 .await?;
287 }
288
289 Ok(())
290 }
291
292 pub fn should_send_on_sign_in(&self) -> bool {
294 self.config.send_on_sign_in
295 }
296
297 pub fn is_verification_required(&self) -> bool {
299 self.config.require_verification_for_signin
300 }
301
302 pub fn is_user_verified_or_not_required(&self, user: &impl AuthUser) -> bool {
304 user.email_verified() || !self.config.require_verification_for_signin
305 }
306}
307
308