mas_handlers/views/
reauth.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use 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        // XXX: do something better here
54        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        // If there is no session, redirect to the login screen, keeping the
74        // PostAuthAction
75        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        // XXX: do something better here
118        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        // If there is no session, redirect to the login screen, keeping the
138        // PostAuthAction
139        let login = mas_router::Login::from(query.post_auth_action);
140        return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
141    };
142
143    // Load the user password
144    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    // TODO: recover from errors
153    // Verify the password, and upgrade it on-the-fly if needed
154    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        // Save the upgraded password
165        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    // Mark the session as authenticated by the password
180    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}