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.
This commit is contained in:
Valentin Tolmer 2021-11-21 18:09:29 +01:00 committed by nitnelave
parent 7b5ad47ee2
commit a13bfc3575
3 changed files with 124 additions and 11 deletions

View File

@ -15,6 +15,7 @@ use actix_web::{
error::{ErrorBadRequest, ErrorUnauthorized}, error::{ErrorBadRequest, ErrorUnauthorized},
web, HttpRequest, HttpResponse, web, HttpRequest, HttpResponse,
}; };
use actix_web_httpauth::extractors::bearer::BearerAuth;
use anyhow::Result; use anyhow::Result;
use chrono::prelude::*; use chrono::prelude::*;
use futures::future::{ok, Ready}; use futures::future::{ok, Ready};
@ -111,6 +112,64 @@ where
.unwrap_or_else(error_to_http_response) .unwrap_or_else(error_to_http_response)
} }
async fn get_password_reset_step1<Backend>(
data: web::Data<AppState<Backend>>,
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<Backend>(
data: web::Data<AppState<Backend>>,
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<Backend>( async fn get_logout<Backend>(
data: web::Data<AppState<Backend>>, data: web::Data<AppState<Backend>>,
request: HttpRequest, request: HttpRequest,
@ -254,14 +313,45 @@ where
} }
async fn opaque_register_start<Backend>( async fn opaque_register_start<Backend>(
request: actix_web::HttpRequest,
mut payload: actix_web::web::Payload,
data: web::Data<AppState<Backend>>, data: web::Data<AppState<Backend>>,
request: web::Json<registration::ClientRegistrationStartRequest>,
) -> ApiResult<registration::ServerRegistrationStartResponse> ) -> ApiResult<registration::ServerRegistrationStartResponse>
where where
Backend: OpaqueHandler + 'static, 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::<registration::ClientRegistrationStartRequest>::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 = &registration_start_request.username;
validation_result.can_access(user_id);
data.backend_handler data.backend_handler
.registration_start(request.into_inner()) .registration_start(registration_start_request)
.await .await
.map(|res| ApiResult::Left(web::Json(res))) .map(|res| ApiResult::Left(web::Json(res)))
.unwrap_or_else(error_to_api_response) .unwrap_or_else(error_to_api_response)
@ -402,14 +492,25 @@ where
web::resource("/opaque/login/finish") web::resource("/opaque/login/finish")
.route(web::post().to(opaque_login_finish::<Backend>)), .route(web::post().to(opaque_login_finish::<Backend>)),
) )
.service(
web::resource("/opaque/register/start")
.route(web::post().to(opaque_register_start::<Backend>)),
)
.service(
web::resource("/opaque/register/finish")
.route(web::post().to(opaque_register_finish::<Backend>)),
)
.service(web::resource("/refresh").route(web::get().to(get_refresh::<Backend>))) .service(web::resource("/refresh").route(web::get().to(get_refresh::<Backend>)))
.service(web::resource("/logout").route(web::get().to(get_logout::<Backend>))); .service(
web::resource("/reset/step1/{user_id}")
.route(web::get().to(get_password_reset_step1::<Backend>)),
)
.service(
web::resource("/reset/step2/{token}")
.route(web::get().to(get_password_reset_step2::<Backend>)),
)
.service(web::resource("/logout").route(web::get().to(get_logout::<Backend>)))
.service(
web::scope("/opaque/register")
.wrap(CookieToHeaderTranslatorFactory)
.service(
web::resource("/start").route(web::post().to(opaque_register_start::<Backend>)),
)
.service(
web::resource("/finish")
.route(web::post().to(opaque_register_finish::<Backend>)),
),
);
} }

View File

@ -151,4 +151,13 @@ impl TcpBackendHandler for SqlBackendHandler {
let (user_id,) = sqlx::query_as(&query).fetch_one(&self.sql_pool).await?; let (user_id,) = sqlx::query_as(&query).fetch_one(&self.sql_pool).await?;
Ok(user_id) 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(())
}
} }

View File

@ -17,6 +17,8 @@ pub trait TcpBackendHandler {
/// Get the user ID associated with a password reset token. /// Get the user ID associated with a password reset token.
async fn get_user_id_for_password_reset_token(&self, token: &str) -> Result<String>; async fn get_user_id_for_password_reset_token(&self, token: &str) -> Result<String>;
async fn delete_password_reset_token(&self, token: &str) -> Result<()>;
} }
#[cfg(test)] #[cfg(test)]
@ -56,5 +58,6 @@ mockall::mock! {
async fn delete_refresh_token(&self, refresh_token_hash: u64) -> Result<()>; async fn delete_refresh_token(&self, refresh_token_hash: u64) -> Result<()>;
async fn start_password_reset(&self, user: &str) -> Result<Option<String>>; async fn start_password_reset(&self, user: &str) -> Result<Option<String>>;
async fn get_user_id_for_password_reset_token(&self, token: &str) -> Result<String>; async fn get_user_id_for_password_reset_token(&self, token: &str) -> Result<String>;
async fn delete_password_reset_token(&self, token: &str) -> Result<()>;
} }
} }