use anyhow::Context;
use axum::{
extract::{Form, Query, State},
response::{Html, IntoResponse, Response},
};
use hyper::StatusCode;
use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_router::UrlBuilder;
use mas_storage::{
user::{BrowserSessionRepository, UserPasswordRepository},
BoxClock, BoxRepository, BoxRng,
};
use mas_templates::{ReauthContext, TemplateContext, Templates};
use serde::Deserialize;
use zeroize::Zeroizing;
use super::shared::OptionalPostAuthAction;
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage, SiteConfig};
#[derive(Deserialize, Debug)]
pub(crate) struct ReauthForm {
password: String,
}
#[tracing::instrument(name = "handlers.views.reauth.get", skip_all, err)]
pub(crate) async fn get(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
State(site_config): State<SiteConfig>,
activity_tracker: BoundActivityTracker,
mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar,
) -> Result<Response, FancyError> {
if !site_config.password_login_enabled {
return Ok(url_builder
.redirect(&mas_router::Account::default())
.into_response());
}
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let Some(session) = maybe_session else {
let login = mas_router::Login::from(query.post_auth_action);
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
activity_tracker
.record_browser_session(&clock, &session)
.await;
let ctx = ReauthContext::default();
let next = query.load_context(&mut repo).await?;
let ctx = if let Some(next) = next {
ctx.with_post_action(next)
} else {
ctx
};
let ctx = ctx
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_reauth(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
}
#[tracing::instrument(name = "handlers.views.reauth.post", skip_all, err)]
pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
State(password_manager): State<PasswordManager>,
State(url_builder): State<UrlBuilder>,
State(site_config): State<SiteConfig>,
mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<ReauthForm>>,
) -> Result<Response, FancyError> {
if !site_config.password_login_enabled {
return Ok(StatusCode::METHOD_NOT_ALLOWED.into_response());
}
let form = cookie_jar.verify_form(&clock, form)?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let Some(session) = maybe_session else {
let login = mas_router::Login::from(query.post_auth_action);
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
let user_password = repo
.user_password()
.active(&session.user)
.await?
.context("User has no password")?;
let password = Zeroizing::new(form.password.as_bytes().to_vec());
let new_password_hash = password_manager
.verify_and_upgrade(
&mut rng,
user_password.version,
password,
user_password.hashed_password.clone(),
)
.await?;
let user_password = if let Some((version, new_password_hash)) = new_password_hash {
repo.user_password()
.add(
&mut rng,
&clock,
&session.user,
version,
new_password_hash,
Some(&user_password),
)
.await?
} else {
user_password
};
repo.browser_session()
.authenticate_with_password(&mut rng, &clock, &session, &user_password)
.await?;
let cookie_jar = cookie_jar.set_session(&session);
repo.save().await?;
let reply = query.go_next(&url_builder);
Ok((cookie_jar, reply).into_response())
}