1use anyhow::Context;
8use axum::{
9 extract::{Form, Query, State},
10 response::{Html, IntoResponse, Response},
11};
12use hyper::StatusCode;
13use mas_axum_utils::{
14 FancyError, SessionInfoExt,
15 cookies::CookieJar,
16 csrf::{CsrfExt, ProtectedForm},
17};
18use mas_router::UrlBuilder;
19use mas_storage::{
20 BoxClock, BoxRepository, BoxRng,
21 user::{BrowserSessionRepository, UserPasswordRepository},
22};
23use mas_templates::{ReauthContext, TemplateContext, Templates};
24use serde::Deserialize;
25use zeroize::Zeroizing;
26
27use super::shared::OptionalPostAuthAction;
28use crate::{
29 BoundActivityTracker, PreferredLanguage, SiteConfig,
30 passwords::PasswordManager,
31 session::{SessionOrFallback, load_session_or_fallback},
32};
33
34#[derive(Deserialize, Debug)]
35pub(crate) struct ReauthForm {
36 password: String,
37}
38
39#[tracing::instrument(name = "handlers.views.reauth.get", skip_all, err)]
40pub(crate) async fn get(
41 mut rng: BoxRng,
42 clock: BoxClock,
43 PreferredLanguage(locale): PreferredLanguage,
44 State(templates): State<Templates>,
45 State(url_builder): State<UrlBuilder>,
46 State(site_config): State<SiteConfig>,
47 activity_tracker: BoundActivityTracker,
48 mut repo: BoxRepository,
49 Query(query): Query<OptionalPostAuthAction>,
50 cookie_jar: CookieJar,
51) -> Result<Response, FancyError> {
52 if !site_config.password_login_enabled {
53 return Ok(url_builder
55 .redirect(&mas_router::Account::default())
56 .into_response());
57 }
58
59 let (cookie_jar, maybe_session) = match load_session_or_fallback(
60 cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
61 )
62 .await?
63 {
64 SessionOrFallback::MaybeSession {
65 cookie_jar,
66 maybe_session,
67 ..
68 } => (cookie_jar, maybe_session),
69 SessionOrFallback::Fallback { response } => return Ok(response),
70 };
71
72 let Some(session) = maybe_session else {
73 let login = mas_router::Login::from(query.post_auth_action);
76 return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
77 };
78
79 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
80
81 activity_tracker
82 .record_browser_session(&clock, &session)
83 .await;
84
85 let ctx = ReauthContext::default();
86 let next = query.load_context(&mut repo).await?;
87 let ctx = if let Some(next) = next {
88 ctx.with_post_action(next)
89 } else {
90 ctx
91 };
92 let ctx = ctx
93 .with_session(session)
94 .with_csrf(csrf_token.form_value())
95 .with_language(locale);
96
97 let content = templates.render_reauth(&ctx)?;
98
99 Ok((cookie_jar, Html(content)).into_response())
100}
101
102#[tracing::instrument(name = "handlers.views.reauth.post", skip_all, err)]
103pub(crate) async fn post(
104 mut rng: BoxRng,
105 clock: BoxClock,
106 PreferredLanguage(locale): PreferredLanguage,
107 State(templates): State<Templates>,
108 State(password_manager): State<PasswordManager>,
109 State(url_builder): State<UrlBuilder>,
110 State(site_config): State<SiteConfig>,
111 mut repo: BoxRepository,
112 Query(query): Query<OptionalPostAuthAction>,
113 cookie_jar: CookieJar,
114 Form(form): Form<ProtectedForm<ReauthForm>>,
115) -> Result<Response, FancyError> {
116 if !site_config.password_login_enabled {
117 return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
119 }
120
121 let form = cookie_jar.verify_form(&clock, form)?;
122
123 let (cookie_jar, maybe_session) = match load_session_or_fallback(
124 cookie_jar, &clock, &mut rng, &templates, &locale, &mut repo,
125 )
126 .await?
127 {
128 SessionOrFallback::MaybeSession {
129 cookie_jar,
130 maybe_session,
131 ..
132 } => (cookie_jar, maybe_session),
133 SessionOrFallback::Fallback { response } => return Ok(response),
134 };
135
136 let Some(session) = maybe_session else {
137 let login = mas_router::Login::from(query.post_auth_action);
140 return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
141 };
142
143 let user_password = repo
145 .user_password()
146 .active(&session.user)
147 .await?
148 .context("User has no password")?;
149
150 let password = Zeroizing::new(form.password.as_bytes().to_vec());
151
152 let new_password_hash = password_manager
155 .verify_and_upgrade(
156 &mut rng,
157 user_password.version,
158 password,
159 user_password.hashed_password.clone(),
160 )
161 .await?;
162
163 let user_password = if let Some((version, new_password_hash)) = new_password_hash {
164 repo.user_password()
166 .add(
167 &mut rng,
168 &clock,
169 &session.user,
170 version,
171 new_password_hash,
172 Some(&user_password),
173 )
174 .await?
175 } else {
176 user_password
177 };
178
179 repo.browser_session()
181 .authenticate_with_password(&mut rng, &clock, &session, &user_password)
182 .await?;
183
184 let cookie_jar = cookie_jar.set_session(&session);
185 repo.save().await?;
186
187 let reply = query.go_next(&url_builder);
188 Ok((cookie_jar, reply).into_response())
189}