1#[cfg(test)]
54mod tests;
55mod pages;
56mod store;
57
58pub use store::{AuthUser, MemoryUserStore, UserStore};
59#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
60pub use store::DbUserStore;
61
62use std::collections::HashMap;
63use std::sync::{Arc, OnceLock};
64use std::time::{SystemTime, UNIX_EPOCH};
65
66use sha2::{Digest, Sha256};
67
68use crate::application::Application;
69use crate::cookie::{signed_cookie, verify_signed_cookie, CookieJar, SetCookie};
70use crate::error::IntoResponse;
71use crate::extract::FromRequest;
72use crate::flash::Redirect;
73use crate::header::Header;
74use crate::mailer::{Deliver, Email, Mail};
75use crate::middleware::Middleware;
76use crate::rate_limit::RateLimiter;
77use crate::request::Request;
78use crate::response::{Response, StatusCodeReasonPhrase, STATUS_CODE_REASON_PHRASE};
79use crate::server::ConnectionInfo;
80
81const USER_ID_HEADER: &str = "X-Rws-Auth-User-Id";
82const USER_EMAIL_HEADER: &str = "X-Rws-Auth-User-Email";
83const USER_CONFIRMED_HEADER: &str = "X-Rws-Auth-User-Confirmed";
84const MARK_HEADER: &str = "X-Rws-Auth-Mark";
87
88const RESET_TTL: u64 = 3600;
89const CONFIRM_TTL: u64 = 3 * 86_400;
90
91fn mark() -> &'static str {
92 static MARK: OnceLock<String> = OnceLock::new();
93 MARK.get_or_init(|| crate::crypto::generate_token(16))
94}
95
96fn now() -> u64 {
97 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
98}
99
100fn hex(bytes: &[u8]) -> String {
101 bytes.iter().map(|b| format!("{b:02x}")).collect()
102}
103
104fn unhex(text: &str) -> Option<Vec<u8>> {
105 if text.len() % 2 != 0 {
106 return None;
107 }
108 (0..text.len()).step_by(2).map(|i| u8::from_str_radix(text.get(i..i + 2)?, 16).ok()).collect()
109}
110
111fn fingerprint(value: &str) -> String {
114 hex(&Sha256::digest(value.as_bytes())[..8])
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct CurrentUser {
121 pub id: String,
122 pub email: String,
123 pub confirmed: bool,
124}
125
126impl CurrentUser {
127 pub fn of(request: &Request) -> Option<CurrentUser> {
129 if request.header(MARK_HEADER).map(|h| h.value.as_ref()) != Some(mark()) {
130 return None;
131 }
132 Some(CurrentUser {
133 id: request.header(USER_ID_HEADER)?.value.to_string(),
134 email: request.header(USER_EMAIL_HEADER)?.value.to_string(),
135 confirmed: request.header(USER_CONFIRMED_HEADER).map(|h| h.value == "true").unwrap_or(false),
136 })
137 }
138}
139
140impl FromRequest for CurrentUser {
141 fn from_request(request: &Request) -> Result<Self, Response> {
142 CurrentUser::of(request).ok_or_else(|| crate::error::AppError::Unauthorized.into_response())
143 }
144}
145
146pub fn signed_in<S>(request: &Request, _connection: &ConnectionInfo, _state: &S) -> Result<(), Response> {
148 CurrentUser::from_request(request).map(|_| ())
149}
150
151pub fn signed_in_or_redirect<S>(login: &str) -> impl Fn(&Request, &ConnectionInfo, &S) -> Result<(), Response> + Send + Sync + 'static {
154 let login = login.to_string();
155 move |request, _, _| match CurrentUser::of(request) {
156 Some(_) => Ok(()),
157 None => {
158 let back = crate::router::build_path("/", &[("return_to", &request.request_uri)]).unwrap_or_default();
159 Err(Redirect::to(&format!("{login}{}", back.trim_start_matches('/'))).alert("Please sign in.").into_response())
160 }
161 }
162}
163
164struct Inner {
165 secret: Vec<u8>,
166 base_url: String,
167 prefix: String,
168 store: Arc<dyn UserStore>,
169 mailer: Arc<dyn Deliver>,
170 app_name: String,
171 require_confirmation: bool,
172 min_password: usize,
173 session_ttl: u64,
174 cookie_name: String,
175 secure: bool,
176 after_login: String,
177 after_logout: String,
178 limiter: Option<Arc<RateLimiter>>,
179 on_signup: Vec<Arc<dyn Fn(&AuthUser) + Send + Sync>>,
180 #[cfg(feature = "tera")]
181 templates: Option<Arc<crate::template::TeraEngine>>,
182}
183
184#[derive(Clone)]
187pub struct AuthFlows {
188 inner: Arc<Inner>,
189}
190
191impl AuthFlows {
192 pub fn new(secret: &[u8], base_url: &str, store: impl UserStore + 'static, mailer: impl Deliver + 'static) -> Self {
200 Self::with_arcs(secret, base_url, Arc::new(store), Arc::new(mailer))
201 }
202
203 pub fn with_arcs(secret: &[u8], base_url: &str, store: Arc<dyn UserStore>, mailer: Arc<dyn Deliver>) -> Self {
205 assert!(secret.len() >= 32, "AuthFlows: the secret must be at least 32 bytes");
206 let base_url = base_url.trim_end_matches('/').to_string();
207 AuthFlows {
208 inner: Arc::new(Inner {
209 secret: secret.to_vec(),
210 secure: base_url.starts_with("https://"),
211 base_url,
212 prefix: "/auth".into(),
213 store,
214 mailer,
215 app_name: "the app".into(),
216 require_confirmation: false,
217 min_password: 8,
218 session_ttl: 30 * 86_400,
219 cookie_name: "rws_auth".into(),
220 after_login: "/".into(),
221 after_logout: "/".into(),
222 limiter: Some(Arc::new(RateLimiter::new(10, 60))),
223 on_signup: Vec::new(),
224 #[cfg(feature = "tera")]
225 templates: None,
226 }),
227 }
228 }
229
230 fn edit(mut self, f: impl FnOnce(&mut Inner)) -> Self {
231 f(Arc::get_mut(&mut self.inner).expect("configure AuthFlows before cloning it"));
232 self
233 }
234
235 pub fn prefix(self, prefix: &str) -> Self {
237 let prefix = format!("/{}", prefix.trim_matches('/'));
238 self.edit(|i| i.prefix = prefix)
239 }
240
241 pub fn app_name(self, name: &str) -> Self {
243 self.edit(|i| i.app_name = name.to_string())
244 }
245
246 pub fn require_confirmation(self, require: bool) -> Self {
249 self.edit(|i| i.require_confirmation = require)
250 }
251
252 pub fn min_password_length(self, n: usize) -> Self {
254 self.edit(|i| i.min_password = n.max(1))
255 }
256
257 pub fn session_ttl(self, ttl: std::time::Duration) -> Self {
259 self.edit(|i| i.session_ttl = ttl.as_secs().max(60))
260 }
261
262 pub fn cookie_name(self, name: &str) -> Self {
264 self.edit(|i| i.cookie_name = name.to_string())
265 }
266
267 pub fn secure_cookie(self, secure: bool) -> Self {
269 self.edit(|i| i.secure = secure)
270 }
271
272 pub fn after_login(self, path: &str) -> Self {
274 self.edit(|i| i.after_login = path.to_string())
275 }
276
277 pub fn after_logout(self, path: &str) -> Self {
279 self.edit(|i| i.after_logout = path.to_string())
280 }
281
282 pub fn rate_limit(self, max: u32, window_secs: u64) -> Self {
285 let limiter = (max > 0).then(|| Arc::new(RateLimiter::new(max, window_secs)));
286 self.edit(|i| i.limiter = limiter)
287 }
288
289 pub fn on_signup(self, hook: impl Fn(&AuthUser) + Send + Sync + 'static) -> Self {
293 self.edit(|i| i.on_signup.push(Arc::new(hook)))
294 }
295
296 #[cfg(feature = "tera")]
301 pub fn templates(self, engine: Arc<crate::template::TeraEngine>) -> Self {
302 self.edit(|i| i.templates = Some(engine))
303 }
304
305 pub fn session_cookie(&self, user: &AuthUser) -> String {
308 let i = &self.inner;
309 let value = format!("s.{}.{}.{}", hex(user.id.as_bytes()), now() + i.session_ttl, fingerprint(&user.password_hash));
310 let mut cookie = SetCookie::new(&i.cookie_name, signed_cookie(&value, &i.secret)).path("/").http_only().same_site("Lax").max_age(i.session_ttl as i64);
311 if i.secure {
312 cookie = cookie.secure();
313 }
314 cookie.build()
315 }
316
317 fn clear_cookie(&self) -> String {
318 let mut cookie = SetCookie::new(&self.inner.cookie_name, "").path("/").http_only().same_site("Lax").max_age(0);
319 if self.inner.secure {
320 cookie = cookie.secure();
321 }
322 cookie.build()
323 }
324
325 pub fn session_user(&self, request: &Request) -> Option<AuthUser> {
328 let header = request.header("cookie")?;
329 let jar = CookieJar::parse(&header.value);
330 let value = verify_signed_cookie(&jar.get(&self.inner.cookie_name)?.value, &self.inner.secret)?;
331 let mut parts = value.split('.');
332 let (Some("s"), Some(id), Some(expires), Some(fp), None) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next()) else {
333 return None;
334 };
335 if expires.parse::<u64>().ok()? < now() {
336 return None;
337 }
338 let id = String::from_utf8(unhex(id)?).ok()?;
339 let user = self.inner.store.find_by_id(&id).ok()??;
340 (fingerprint(&user.password_hash) == fp).then_some(user)
341 }
342
343 fn token(&self, purpose: &str, user: &AuthUser, ttl: u64) -> String {
345 let bound = if purpose == "reset" { &user.password_hash } else { &user.email };
346 let value = format!("{purpose}.{}.{}.{}", hex(user.id.as_bytes()), now() + ttl, fingerprint(bound));
347 signed_cookie(&hex(value.as_bytes()), &self.inner.secret)
348 }
349
350 fn check_token(&self, purpose: &str, token: &str) -> Option<AuthUser> {
352 let value = verify_signed_cookie(token.trim(), &self.inner.secret)?;
353 let value = String::from_utf8(unhex(&value)?).ok()?;
354 let mut parts = value.split('.');
355 let (Some(p), Some(id), Some(expires), Some(fp), None) = (parts.next(), parts.next(), parts.next(), parts.next(), parts.next()) else {
356 return None;
357 };
358 if p != purpose || expires.parse::<u64>().ok()? < now() {
359 return None;
360 }
361 let user = self.inner.store.find_by_id(&String::from_utf8(unhex(id)?).ok()?).ok()??;
362 let bound = if purpose == "reset" { &user.password_hash } else { &user.email };
363 (fingerprint(bound) == fp).then_some(user)
364 }
365
366 fn link(&self, path: &str, purpose: &str, user: &AuthUser, ttl: u64) -> String {
368 let token = self.token(purpose, user, ttl);
369 format!("{}{}{}?token={token}", self.inner.base_url, self.inner.prefix, path)
370 }
371
372 fn send(&self, user: &AuthUser, kind: &str, link: &str) {
373 let i = &self.inner;
374 let (subject, text) = match kind {
375 "confirm" => (
376 format!("Confirm your email for {}", i.app_name),
377 format!("Confirm your email address for {}:\n\n{link}\n\nThe link works for 3 days.", i.app_name),
378 ),
379 _ => (
380 format!("Reset your password for {}", i.app_name),
381 format!("Choose a new password for {}:\n\n{link}\n\nThe link works for an hour. If you didn't ask for it, ignore this email.", i.app_name),
382 ),
383 };
384 let builder = Email::builder().to(&user.email).subject(&subject);
385 #[cfg(feature = "tera")]
386 let builder = match &i.templates {
387 Some(engine) if engine.has_template(&format!("auth/mail/{kind}.txt")) || engine.has_template(&format!("auth/mail/{kind}.html")) => {
388 let mut ctx = crate::template::Context::new();
389 ctx.insert("link", link);
390 ctx.insert("app", &i.app_name);
391 ctx.insert("email", &user.email);
392 match builder.render_with(engine, &format!("auth/mail/{kind}"), &ctx) {
393 Ok(b) => b,
394 Err(e) => {
395 eprintln!("[auth] {kind} email template: {e}");
396 return;
397 }
398 }
399 }
400 _ => builder.text(&text),
401 };
402 #[cfg(not(feature = "tera"))]
403 let builder = builder.text(&text);
404 let result = builder.build().and_then(|email| i.mailer.deliver(&Mail::new(email)));
405 if let Err(e) = result {
406 eprintln!("[auth] the {kind} email to {} failed: {e}", user.email);
407 }
408 }
409
410 fn limited(&self, request: &Request, connection: &ConnectionInfo, action: &str) -> bool {
414 match &self.inner.limiter {
415 Some(limiter) => !limiter.check(&format!("{}|{action}", crate::ip_filter::client_ip(request, &connection.client.ip))),
416 None => false,
417 }
418 }
419
420 fn validate_email(email: &str) -> Result<String, String> {
421 let email = email.trim().to_lowercase();
422 let ok = email.len() <= 254
423 && !email.chars().any(|c| c.is_whitespace() || c.is_control() || matches!(c, '<' | '>' | ',' | ';' | '"'))
424 && email.split_once('@').is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.') && !domain.starts_with('.') && !domain.ends_with('.'));
425 if ok { Ok(email) } else { Err("that doesn't look like an email address".into()) }
426 }
427
428 fn validate_password(&self, password: &str) -> Result<(), String> {
429 let n = password.chars().count();
430 if n < self.inner.min_password {
431 Err(format!("the password needs at least {} characters", self.inner.min_password))
432 } else if password.len() > 1024 {
433 Err("the password is too long".into())
434 } else {
435 Ok(())
436 }
437 }
438
439 fn with_identity(&self, request: &Request) -> Request {
442 let mut request = request.clone();
443 request.headers.retain(|h| !h.name.to_ascii_lowercase().starts_with("x-rws-auth-"));
444 if let Some(user) = self.session_user(&request) {
445 request.headers.push(Header::new(USER_ID_HEADER, user.id));
446 request.headers.push(Header::new(USER_EMAIL_HEADER, user.email));
447 request.headers.push(Header::new(USER_CONFIRMED_HEADER, if user.confirmed { "true" } else { "false" }));
448 request.headers.push(Header::new(MARK_HEADER, mark()));
449 }
450 request
451 }
452
453 fn route(&self, request: &Request, connection: &ConnectionInfo) -> Option<Response> {
454 let path = request.request_uri.split('?').next().unwrap_or("");
455 let rest = path.strip_prefix(&self.inner.prefix)?;
456 let rest = rest.trim_end_matches('/');
457 let get = request.method == "GET" || request.method == "HEAD";
458 let post = request.method == "POST";
459 let input = Input::read(request);
460 let ctx = Ctx { flows: self, request, json: wants_json(request), input: &input };
461 Some(match (rest, get, post) {
462 ("/signup", _, true) => ctx.signup(connection),
463 ("/login", _, true) => ctx.login(connection),
464 ("/logout", _, true) => ctx.logout(),
465 ("/me", true, _) => ctx.me(),
466 ("/password/forgot", _, true) => ctx.forgot(connection),
467 ("/password/reset", _, true) => ctx.reset(),
468 ("/confirm", true, _) | ("/confirm", _, true) => ctx.confirm(),
469 ("/confirm/resend", _, true) => ctx.resend(connection),
470 ("/login", true, _) | ("/signup", true, _) | ("/password/forgot", true, _) | ("/password/reset", true, _) => pages::page(self, request, rest),
471 _ => return None,
472 })
473 }
474}
475
476struct Input(HashMap<String, String>);
478
479impl Input {
480 fn read(request: &Request) -> Input {
481 let mut fields = HashMap::new();
482 if let Some((_, query)) = request.request_uri.split_once('?') {
483 fields.extend(parse_form(query));
484 }
485 let content_type = request.header("content-type").map(|h| h.value.to_ascii_lowercase()).unwrap_or_default();
486 if content_type.starts_with("application/json") {
487 if let Ok(serde_json::Value::Object(map)) = serde_json::from_slice::<serde_json::Value>(&request.body) {
488 for (k, v) in map {
489 if let serde_json::Value::String(s) = v {
490 fields.insert(k, s);
491 }
492 }
493 }
494 } else if content_type.starts_with("application/x-www-form-urlencoded") {
495 fields.extend(parse_form(&String::from_utf8_lossy(&request.body)));
496 }
497 Input(fields)
498 }
499
500 fn get(&self, name: &str) -> &str {
501 self.0.get(name).map(String::as_str).unwrap_or("")
502 }
503}
504
505fn parse_form(text: &str) -> Vec<(String, String)> {
506 text.split('&')
507 .filter_map(|pair| {
508 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
509 Some((form_decode(k)?, form_decode(v)?))
510 })
511 .collect()
512}
513
514fn form_decode(text: &str) -> Option<String> {
516 let bytes = text.as_bytes();
517 let mut out = Vec::with_capacity(bytes.len());
518 let mut i = 0;
519 while i < bytes.len() {
520 match bytes[i] {
521 b'+' => {
522 out.push(b' ');
523 i += 1;
524 }
525 b'%' => {
526 out.push(u8::from_str_radix(text.get(i + 1..i + 3)?, 16).ok()?);
527 i += 3;
528 }
529 b => {
530 out.push(b);
531 i += 1;
532 }
533 }
534 }
535 String::from_utf8(out).ok()
536}
537
538fn wants_json(request: &Request) -> bool {
539 let content_type = request.header("content-type").map(|h| h.value.to_ascii_lowercase()).unwrap_or_default();
540 if content_type.starts_with("application/json") {
541 return true;
542 }
543 if content_type.starts_with("application/x-www-form-urlencoded") || content_type.starts_with("multipart/form-data") {
544 return false;
545 }
546 let accept = request.header("accept").map(|h| h.value.to_ascii_lowercase()).unwrap_or_default();
547 accept.contains("application/json") && !accept.contains("text/html")
548}
549
550fn user_json(user: &AuthUser) -> serde_json::Value {
551 serde_json::json!({ "id": user.id, "email": user.email, "confirmed": user.confirmed })
552}
553
554fn json(status: &StatusCodeReasonPhrase, value: serde_json::Value) -> Response {
555 Response::json(status, value.to_string().into_bytes())
556}
557
558fn problem(status: &StatusCodeReasonPhrase, detail: &str) -> Response {
559 crate::error::problem::problem(status, detail)
560}
561
562fn local_path(path: &str) -> Option<&str> {
564 (path.starts_with('/') && !path.starts_with("//") && !path.contains(['\\', '\r', '\n'])).then_some(path)
565}
566
567struct Ctx<'a> {
569 flows: &'a AuthFlows,
570 request: &'a Request,
571 json: bool,
572 input: &'a Input,
573}
574
575impl Ctx<'_> {
576 fn i(&self) -> &Inner {
577 &self.flows.inner
578 }
579
580 fn page(&self, page: &str) -> String {
581 format!("{}{page}", self.i().prefix)
582 }
583
584 fn fail(&self, status: &StatusCodeReasonPhrase, message: &str, back: &str) -> Response {
587 if self.json {
588 problem(status, message)
589 } else {
590 let mut location = self.page(back);
591 let return_to = self.input.get("return_to");
592 if let Some(path) = local_path(return_to) {
593 location = crate::router::build_path(&location, &[("return_to", path)]).unwrap_or(location);
594 }
595 Redirect::to(&location).alert(message).into_response()
596 }
597 }
598
599 fn internal(&self, e: String) -> Response {
600 eprintln!("[auth] {e}");
601 problem(STATUS_CODE_REASON_PHRASE.n500_internal_server_error, "something went wrong")
602 }
603
604 fn signed_in(&self, user: &AuthUser, status: &StatusCodeReasonPhrase, notice: &str) -> Response {
605 let mut response = if self.json {
606 json(status, serde_json::json!({ "user": user_json(user) }))
607 } else {
608 let target = local_path(self.input.get("return_to")).unwrap_or(&self.i().after_login);
609 Redirect::to(target).notice(notice).into_response()
610 };
611 response.headers.push(Header::new(Header::_SET_COOKIE, self.flows.session_cookie(user)));
612 response
613 }
614
615 fn too_many(&self) -> Response {
616 let mut response = if self.json {
617 problem(STATUS_CODE_REASON_PHRASE.n429_too_many_requests, "too many attempts; try again in a minute")
618 } else {
619 Response::text(STATUS_CODE_REASON_PHRASE.n429_too_many_requests, "Too many attempts; try again in a minute.")
620 };
621 response.headers.push(Header::new("Retry-After", "60"));
622 response
623 }
624
625 fn signup(&self, connection: &ConnectionInfo) -> Response {
626 if self.flows.limited(self.request, connection, "signup") {
627 return self.too_many();
628 }
629 let email = match AuthFlows::validate_email(self.input.get("email")) {
630 Ok(email) => email,
631 Err(e) => return self.fail(STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity, &e, "/signup"),
632 };
633 let password = self.input.get("password");
634 if let Err(e) = self.flows.validate_password(password) {
635 return self.fail(STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity, &e, "/signup");
636 }
637 let hash = match crate::crypto::hash_password(password) {
638 Ok(hash) => hash,
639 Err(e) => return self.internal(e.to_string()),
640 };
641 let user = match self.i().store.create(&email, &hash) {
642 Ok(Some(user)) => user,
643 Ok(None) => return self.fail(STATUS_CODE_REASON_PHRASE.n409_conflict, "an account with that email already exists", "/signup"),
644 Err(e) => return self.internal(e),
645 };
646 self.flows.send(&user, "confirm", &self.flows.link("/confirm", "confirm", &user, CONFIRM_TTL));
647 for hook in &self.i().on_signup {
648 hook(&user);
649 }
650 if self.i().require_confirmation {
651 if self.json {
652 json(STATUS_CODE_REASON_PHRASE.n201_created, serde_json::json!({ "user": user_json(&user), "confirmation_required": true }))
653 } else {
654 Redirect::to(&self.page("/login")).notice("Check your email to confirm your account.").into_response()
655 }
656 } else {
657 self.signed_in(&user, STATUS_CODE_REASON_PHRASE.n201_created, "Welcome! We sent you an email to confirm your address.")
658 }
659 }
660
661 fn login(&self, connection: &ConnectionInfo) -> Response {
662 if self.flows.limited(self.request, connection, "login") {
663 return self.too_many();
664 }
665 let email = self.input.get("email").trim().to_lowercase();
666 let password = self.input.get("password");
667 let user = match self.i().store.find_by_email(&email) {
668 Ok(user) => user,
669 Err(e) => return self.internal(e),
670 };
671 let valid = match &user {
672 Some(user) => crate::crypto::verify_password(password, &user.password_hash).unwrap_or(false),
673 None => {
674 let _ = crate::crypto::verify_password(password, dummy_hash());
677 false
678 }
679 };
680 let Some(user) = user.filter(|_| valid) else {
681 return self.fail(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "wrong email or password", "/login");
682 };
683 if self.i().require_confirmation && !user.confirmed {
684 return self.fail(STATUS_CODE_REASON_PHRASE.n403_forbidden, "please confirm your email first (we can send the link again)", "/login");
685 }
686 self.signed_in(&user, STATUS_CODE_REASON_PHRASE.n200_ok, "Signed in.")
687 }
688
689 fn logout(&self) -> Response {
690 let mut response = if self.json {
691 Response::get_response(STATUS_CODE_REASON_PHRASE.n204_no_content, None, None)
692 } else {
693 Redirect::to(&self.i().after_logout).notice("Signed out.").into_response()
694 };
695 response.headers.push(Header::new(Header::_SET_COOKIE, self.flows.clear_cookie()));
696 response
697 }
698
699 fn me(&self) -> Response {
700 match self.flows.session_user(self.request) {
701 Some(user) => json(STATUS_CODE_REASON_PHRASE.n200_ok, serde_json::json!({ "user": user_json(&user) })),
702 None => problem(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "not signed in"),
703 }
704 }
705
706 fn accepted(&self, notice: &str, back: &str) -> Response {
707 if self.json {
708 json(STATUS_CODE_REASON_PHRASE.n202_accepted, serde_json::json!({ "status": "accepted" }))
709 } else {
710 Redirect::to(&self.page(back)).notice(notice).into_response()
711 }
712 }
713
714 fn forgot(&self, connection: &ConnectionInfo) -> Response {
715 if self.flows.limited(self.request, connection, "email") {
716 return self.too_many();
717 }
718 let email = self.input.get("email").trim().to_lowercase();
719 match self.i().store.find_by_email(&email) {
720 Ok(Some(user)) => self.flows.send(&user, "reset", &self.flows.link("/password/reset", "reset", &user, RESET_TTL)),
721 Ok(None) => {}
722 Err(e) => return self.internal(e),
723 }
724 self.accepted("If that email has an account, we sent it a link to reset the password.", "/login")
726 }
727
728 fn reset(&self) -> Response {
729 let Some(user) = self.flows.check_token("reset", self.input.get("token")) else {
730 return self.fail(STATUS_CODE_REASON_PHRASE.n400_bad_request, "that link has expired or was already used; ask for a new one", "/password/forgot");
731 };
732 let password = self.input.get("password");
733 if let Err(e) = self.flows.validate_password(password) {
734 if self.json {
735 return problem(STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity, &e);
736 }
737 let back = format!("{}?token={}", self.page("/password/reset"), self.input.get("token"));
738 return Redirect::to(&back).alert(&e).into_response();
739 }
740 let hash = match crate::crypto::hash_password(password) {
741 Ok(hash) => hash,
742 Err(e) => return self.internal(e.to_string()),
743 };
744 if let Err(e) = self.i().store.set_password_hash(&user.id, &hash) {
745 return self.internal(e);
746 }
747 if !user.confirmed {
749 let _ = self.i().store.confirm(&user.id);
750 }
751 let user = AuthUser { password_hash: hash, confirmed: true, ..user };
752 self.signed_in(&user, STATUS_CODE_REASON_PHRASE.n200_ok, "Your password was changed.")
753 }
754
755 fn confirm(&self) -> Response {
756 let Some(user) = self.flows.check_token("confirm", self.input.get("token")) else {
757 return self.fail(STATUS_CODE_REASON_PHRASE.n400_bad_request, "that confirmation link has expired; ask for a new one", "/login");
758 };
759 if let Err(e) = self.i().store.confirm(&user.id) {
760 return self.internal(e);
761 }
762 let user = AuthUser { confirmed: true, ..user };
763 if self.json {
764 json(STATUS_CODE_REASON_PHRASE.n200_ok, serde_json::json!({ "user": user_json(&user) }))
765 } else {
766 let target = if self.flows.session_user(self.request).is_some() { self.i().after_login.clone() } else { self.page("/login") };
767 Redirect::to(&target).notice("Your email is confirmed.").into_response()
768 }
769 }
770
771 fn resend(&self, connection: &ConnectionInfo) -> Response {
772 if self.flows.limited(self.request, connection, "email") {
773 return self.too_many();
774 }
775 let email = self.input.get("email").trim().to_lowercase();
776 match self.i().store.find_by_email(&email) {
777 Ok(Some(user)) if !user.confirmed => self.flows.send(&user, "confirm", &self.flows.link("/confirm", "confirm", &user, CONFIRM_TTL)),
778 Ok(_) => {}
779 Err(e) => return self.internal(e),
780 }
781 self.accepted("If that email has an unconfirmed account, we sent it a new link.", "/login")
782 }
783}
784
785fn dummy_hash() -> &'static str {
787 static HASH: OnceLock<String> = OnceLock::new();
788 HASH.get_or_init(|| crate::crypto::hash_password(&crate::crypto::generate_token(16)).unwrap_or_default())
789}
790
791impl Middleware for AuthFlows {
792 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
793 if let Some(response) = self.route(request, connection) {
794 return Ok(response);
795 }
796 next.execute(&self.with_identity(request), connection)
797 }
798
799 fn is_passthrough(&self, request: &Request, _connection: &ConnectionInfo) -> bool {
802 let ours = request.request_uri.starts_with(&self.inner.prefix);
803 let spoofed = request.headers.iter().any(|h| h.name.to_ascii_lowercase().starts_with("x-rws-auth-"));
804 let cookie = request.header("cookie").is_some_and(|h| CookieJar::parse(&h.value).get(&self.inner.cookie_name).is_some());
805 !(ours || spoofed || cookie)
806 }
807
808 fn layer_name(&self) -> &'static str {
809 "AuthFlows"
810 }
811}