From a13bfc3575cfa5b85a69f87e04b0c7014283cacf Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Sun, 21 Nov 2021 18:09:29 +0100 Subject: [PATCH] server: Implement password reset It's still missing the email. This also secures the password change method with a JWT token check: you have to be logged in to change the password. --- server/src/infra/auth_service.rs | 123 +++++++++++++++++++++--- server/src/infra/sql_backend_handler.rs | 9 ++ server/src/infra/tcp_backend_handler.rs | 3 + 3 files changed, 124 insertions(+), 11 deletions(-) diff --git a/server/src/infra/auth_service.rs b/server/src/infra/auth_service.rs index 6843c20..25e871d 100644 --- a/server/src/infra/auth_service.rs +++ b/server/src/infra/auth_service.rs @@ -15,6 +15,7 @@ use actix_web::{ error::{ErrorBadRequest, ErrorUnauthorized}, web, HttpRequest, HttpResponse, }; +use actix_web_httpauth::extractors::bearer::BearerAuth; use anyhow::Result; use chrono::prelude::*; use futures::future::{ok, Ready}; @@ -111,6 +112,64 @@ where .unwrap_or_else(error_to_http_response) } +async fn get_password_reset_step1( + data: web::Data>, + request: HttpRequest, +) -> HttpResponse +where + Backend: TcpBackendHandler + BackendHandler + 'static, +{ + let user_id = match request.match_info().get("user_id") { + None => return HttpResponse::BadRequest().body("Missing user ID"), + Some(id) => id, + }; + let _token = match data.backend_handler.start_password_reset(user_id).await { + Err(e) => return HttpResponse::InternalServerError().body(e.to_string()), + Ok(None) => return HttpResponse::Ok().finish(), + Ok(Some(token)) => token, + }; + // TODO: Send email. + HttpResponse::Ok().finish() +} + +async fn get_password_reset_step2( + data: web::Data>, + request: HttpRequest, +) -> HttpResponse +where + Backend: TcpBackendHandler + BackendHandler + 'static, +{ + let token = match request.match_info().get("token") { + None => return HttpResponse::BadRequest().body("Missing token"), + Some(token) => token, + }; + let user_id = match data + .backend_handler + .get_user_id_for_password_reset_token(token) + .await + { + Err(_) => return HttpResponse::Unauthorized().body("Invalid or expired token"), + Ok(user_id) => user_id, + }; + let _ = data + .backend_handler + .delete_password_reset_token(token) + .await; + let groups = HashSet::new(); + let token = create_jwt(&data.jwt_key, user_id.to_string(), groups); + HttpResponse::Ok() + .cookie( + Cookie::build("token", token.as_str()) + .max_age(5.minutes()) + // Cookie is only valid to reset the password. + .path("/auth") + .http_only(true) + .same_site(SameSite::Strict) + .finish(), + ) + .json(user_id) +} + async fn get_logout( data: web::Data>, request: HttpRequest, @@ -254,14 +313,45 @@ where } async fn opaque_register_start( + request: actix_web::HttpRequest, + mut payload: actix_web::web::Payload, data: web::Data>, - request: web::Json, ) -> ApiResult where Backend: OpaqueHandler + 'static, { + use actix_web::FromRequest; + let validation_result = match BearerAuth::from_request(&request, &mut payload.0) + .await + .ok() + .and_then(|bearer| check_if_token_is_valid(&data, bearer.token()).ok()) + { + Some(t) => t, + None => { + return ApiResult::Right( + HttpResponse::Unauthorized().body("Not authorized to change the user's password"), + ) + } + }; + let registration_start_request = + match web::Json::::from_request( + &request, + &mut payload.0, + ) + .await + { + Ok(r) => r, + Err(e) => { + return ApiResult::Right( + HttpResponse::BadRequest().body(format!("Bad request: {:#?}", e)), + ) + } + } + .into_inner(); + let user_id = ®istration_start_request.username; + validation_result.can_access(user_id); data.backend_handler - .registration_start(request.into_inner()) + .registration_start(registration_start_request) .await .map(|res| ApiResult::Left(web::Json(res))) .unwrap_or_else(error_to_api_response) @@ -402,14 +492,25 @@ where web::resource("/opaque/login/finish") .route(web::post().to(opaque_login_finish::)), ) - .service( - web::resource("/opaque/register/start") - .route(web::post().to(opaque_register_start::)), - ) - .service( - web::resource("/opaque/register/finish") - .route(web::post().to(opaque_register_finish::)), - ) .service(web::resource("/refresh").route(web::get().to(get_refresh::))) - .service(web::resource("/logout").route(web::get().to(get_logout::))); + .service( + web::resource("/reset/step1/{user_id}") + .route(web::get().to(get_password_reset_step1::)), + ) + .service( + web::resource("/reset/step2/{token}") + .route(web::get().to(get_password_reset_step2::)), + ) + .service(web::resource("/logout").route(web::get().to(get_logout::))) + .service( + web::scope("/opaque/register") + .wrap(CookieToHeaderTranslatorFactory) + .service( + web::resource("/start").route(web::post().to(opaque_register_start::)), + ) + .service( + web::resource("/finish") + .route(web::post().to(opaque_register_finish::)), + ), + ); } diff --git a/server/src/infra/sql_backend_handler.rs b/server/src/infra/sql_backend_handler.rs index 353924b..91f8fb1 100644 --- a/server/src/infra/sql_backend_handler.rs +++ b/server/src/infra/sql_backend_handler.rs @@ -151,4 +151,13 @@ impl TcpBackendHandler for SqlBackendHandler { let (user_id,) = sqlx::query_as(&query).fetch_one(&self.sql_pool).await?; Ok(user_id) } + + async fn delete_password_reset_token(&self, token: &str) -> Result<()> { + let query = Query::delete() + .from_table(PasswordResetTokens::Table) + .and_where(Expr::col(PasswordResetTokens::Token).eq(token)) + .to_string(DbQueryBuilder {}); + sqlx::query(&query).execute(&self.sql_pool).await?; + Ok(()) + } } diff --git a/server/src/infra/tcp_backend_handler.rs b/server/src/infra/tcp_backend_handler.rs index c5d3e02..e36ae50 100644 --- a/server/src/infra/tcp_backend_handler.rs +++ b/server/src/infra/tcp_backend_handler.rs @@ -17,6 +17,8 @@ pub trait TcpBackendHandler { /// Get the user ID associated with a password reset token. async fn get_user_id_for_password_reset_token(&self, token: &str) -> Result; + + async fn delete_password_reset_token(&self, token: &str) -> Result<()>; } #[cfg(test)] @@ -56,5 +58,6 @@ mockall::mock! { async fn delete_refresh_token(&self, refresh_token_hash: u64) -> Result<()>; async fn start_password_reset(&self, user: &str) -> Result>; async fn get_user_id_for_password_reset_token(&self, token: &str) -> Result; + async fn delete_password_reset_token(&self, token: &str) -> Result<()>; } }