use actix_web::{ cookie::{Cookie, SameSite}, dev::{Service, ServiceRequest, ServiceResponse, Transform}, error::{ErrorBadRequest, ErrorUnauthorized}, web, HttpRequest, HttpResponse, }; use actix_web_httpauth::extractors::bearer::BearerAuth; use anyhow::Result; use chrono::prelude::*; use futures::future::{ok, Ready}; use futures_util::FutureExt; use hmac::Hmac; use jwt::{SignWithKey, VerifyWithKey}; use sha2::Sha512; use std::{ collections::HashSet, hash::Hash, pin::Pin, task::{Context, Poll}, }; use time::ext::NumericalDuration; use tracing::{debug, info, instrument, warn}; use lldap_auth::{login, password_reset, registration, JWTClaims}; use crate::{ domain::{ error::DomainError, handler::{BackendHandler, BindRequest, LoginHandler, UserRequestFilter}, opaque_handler::OpaqueHandler, types::{GroupDetails, GroupName, UserColumn, UserId}, }, infra::{ access_control::{ReadonlyBackendHandler, UserReadableBackendHandler, ValidationResults}, tcp_backend_handler::*, tcp_server::{error_to_http_response, AppState, TcpError, TcpResult}, }, }; type Token = jwt::Token; type SignedToken = Token; fn default_hash(token: &T) -> u64 { use std::collections::hash_map::DefaultHasher; use std::hash::Hasher; let mut s = DefaultHasher::new(); token.hash(&mut s); s.finish() } async fn create_jwt( handler: &Handler, key: &Hmac, user: &UserId, groups: HashSet, ) -> SignedToken { let claims = JWTClaims { exp: Utc::now() + chrono::Duration::days(1), iat: Utc::now(), user: user.to_string(), groups: groups .into_iter() .map(|g| g.display_name.into_string()) .collect(), }; let expiry = claims.exp.naive_utc(); let header = jwt::Header { algorithm: jwt::AlgorithmType::Hs512, ..Default::default() }; let token = jwt::Token::new(header, claims).sign_with_key(key).unwrap(); handler .register_jwt(user, default_hash(token.as_str()), expiry) .await .unwrap(); token } fn parse_refresh_token(token: &str) -> TcpResult<(u64, UserId)> { match token.split_once('+') { None => Err(DomainError::AuthenticationError("Invalid refresh token".to_string()).into()), Some((token, u)) => Ok((default_hash(token), UserId::new(u))), } } fn get_refresh_token(request: HttpRequest) -> TcpResult<(u64, UserId)> { match ( request.cookie("refresh_token"), request.headers().get("refresh-token"), ) { (Some(c), _) => parse_refresh_token(c.value()), (_, Some(t)) => parse_refresh_token(t.to_str().unwrap()), (None, None) => { Err(DomainError::AuthenticationError("Missing refresh token".to_string()).into()) } } } #[instrument(skip_all, level = "debug")] async fn get_refresh( data: web::Data>, request: HttpRequest, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + 'static, { let jwt_key = &data.jwt_key; let (refresh_token_hash, user) = get_refresh_token(request)?; let found = data .get_tcp_handler() .check_token(refresh_token_hash, &user) .await?; if !found { return Err(TcpError::DomainError(DomainError::AuthenticationError( "Invalid refresh token".to_string(), ))); } let mut path = data.server_url.path().to_string(); if !path.ends_with('/') { path.push('/'); }; let groups = data.get_readonly_handler().get_user_groups(&user).await?; let token = create_jwt(data.get_tcp_handler(), jwt_key, &user, groups).await; Ok(HttpResponse::Ok() .cookie( Cookie::build("token", token.as_str()) .max_age(1.days()) .path(&path) .http_only(true) .same_site(SameSite::Strict) .finish(), ) .json(&login::ServerLoginResponse { token: token.as_str().to_owned(), refresh_token: None, })) } async fn get_refresh_handler( data: web::Data>, request: HttpRequest, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + 'static, { get_refresh(data, request) .await .unwrap_or_else(error_to_http_response) } #[instrument(skip_all, level = "debug")] async fn get_password_reset_step1( data: web::Data>, request: HttpRequest, ) -> TcpResult<()> where Backend: TcpBackendHandler + BackendHandler + 'static, { let user_string = request .match_info() .get("user_id") .ok_or_else(|| TcpError::BadRequest("Missing user ID".to_string()))?; let user_results = data .get_readonly_handler() .list_users( Some(UserRequestFilter::Or(vec![ UserRequestFilter::UserId(UserId::new(user_string)), UserRequestFilter::Equality(UserColumn::Email, user_string.to_owned()), ])), false, ) .await?; if user_results.is_empty() { return Ok(()); } else if user_results.len() > 1 { return Err(TcpError::InternalServerError( "Ambiguous user id or email".to_owned(), )); } let user = &user_results[0].user; let token = match data .get_tcp_handler() .start_password_reset(&user.user_id) .await? { None => return Ok(()), Some(token) => token, }; if let Err(e) = super::mail::send_password_reset_email( user.display_name .as_deref() .unwrap_or_else(|| user.user_id.as_str()), user.email.as_str(), &token, &data.server_url, &data.mail_options, ) .await { warn!("Error sending email: {:#?}", e); info!("Reset token: {}", token); return Err(TcpError::InternalServerError(format!( "Could not send email: {}", e ))); } Ok(()) } async fn get_password_reset_step1_handler( data: web::Data>, request: HttpRequest, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + 'static, { get_password_reset_step1(data, request) .await .map(|()| HttpResponse::Ok().finish()) .unwrap_or_else(error_to_http_response) } #[instrument(skip_all, level = "debug")] async fn get_password_reset_step2( data: web::Data>, request: HttpRequest, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + 'static, { let token = request .match_info() .get("token") .ok_or_else(|| TcpError::BadRequest("Missing reset token".to_owned()))?; let user_id = data .get_tcp_handler() .get_user_id_for_password_reset_token(token) .await .map_err(|e| { debug!("Reset token error: {e:#}"); TcpError::NotFoundError("Wrong or expired reset token".to_owned()) })?; let _ = data .get_tcp_handler() .delete_password_reset_token(token) .await; let groups = HashSet::new(); let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, &user_id, groups).await; let mut path = data.server_url.path().to_string(); if !path.ends_with('/') { path.push('/'); }; Ok(HttpResponse::Ok() .cookie( Cookie::build("token", token.as_str()) .max_age(5.minutes()) // Cookie is only valid to reset the password. .path(format!("{}auth", path)) .http_only(true) .same_site(SameSite::Strict) .finish(), ) .json(&password_reset::ServerPasswordResetResponse { user_id: user_id.to_string(), token: token.as_str().to_owned(), })) } async fn get_password_reset_step2_handler( data: web::Data>, request: HttpRequest, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + 'static, { get_password_reset_step2(data, request) .await .unwrap_or_else(error_to_http_response) } #[instrument(skip_all, level = "debug")] async fn get_logout( data: web::Data>, request: HttpRequest, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + 'static, { let (refresh_token_hash, user) = get_refresh_token(request)?; data.get_tcp_handler() .delete_refresh_token(refresh_token_hash) .await?; let new_blacklisted_jwt_hashes = data.get_tcp_handler().blacklist_jwts(&user).await?; let mut jwt_blacklist = data.jwt_blacklist.write().unwrap(); for jwt_hash in new_blacklisted_jwt_hashes { jwt_blacklist.insert(jwt_hash); } let mut path = data.server_url.path().to_string(); if !path.ends_with('/') { path.push('/'); }; Ok(HttpResponse::Ok() .cookie( Cookie::build("token", "") .max_age(0.days()) .path(&path) .http_only(true) .same_site(SameSite::Strict) .finish(), ) .cookie( Cookie::build("refresh_token", "") .max_age(0.days()) .path(format!("{}auth", path)) .http_only(true) .same_site(SameSite::Strict) .finish(), ) .finish()) } async fn get_logout_handler( data: web::Data>, request: HttpRequest, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + 'static, { get_logout(data, request) .await .unwrap_or_else(error_to_http_response) } pub(crate) fn error_to_api_response>(error: E) -> ApiResult { ApiResult::Right(error_to_http_response(error.into())) } pub type ApiResult = actix_web::Either, HttpResponse>; #[instrument(skip_all, level = "debug")] async fn opaque_login_start( data: web::Data>, request: web::Json, ) -> ApiResult where Backend: OpaqueHandler + 'static, { data.get_opaque_handler() .login_start(request.into_inner()) .await .map(|res| ApiResult::Left(web::Json(res))) .unwrap_or_else(error_to_api_response) } #[instrument(skip_all, level = "debug")] async fn get_login_successful_response( data: &web::Data>, name: &UserId, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler, { // The authentication was successful, we need to fetch the groups to create the JWT // token. let groups = data.get_readonly_handler().get_user_groups(name).await?; let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?; let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, name, groups).await; let refresh_token_plus_name = refresh_token + "+" + name.as_str(); let mut path = data.server_url.path().to_string(); if !path.ends_with('/') { path.push('/'); }; Ok(HttpResponse::Ok() .cookie( Cookie::build("token", token.as_str()) .max_age(1.days()) .path(&path) .http_only(true) .same_site(SameSite::Strict) .finish(), ) .cookie( Cookie::build("refresh_token", refresh_token_plus_name.clone()) .max_age(max_age.num_days().days()) .path(format!("{}auth", path)) .http_only(true) .same_site(SameSite::Strict) .finish(), ) .json(&login::ServerLoginResponse { token: token.as_str().to_owned(), refresh_token: Some(refresh_token_plus_name), })) } #[instrument(skip_all, level = "debug")] async fn opaque_login_finish( data: web::Data>, request: web::Json, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, { let name = data .get_opaque_handler() .login_finish(request.into_inner()) .await?; get_login_successful_response(&data, &name).await } async fn opaque_login_finish_handler( data: web::Data>, request: web::Json, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, { opaque_login_finish(data, request) .await .unwrap_or_else(error_to_http_response) } #[instrument(skip_all, level = "debug")] async fn simple_login( data: web::Data>, request: web::Json, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static, { let login::ClientSimpleLoginRequest { username, password } = request.into_inner(); let bind_request = BindRequest { name: username.clone(), password, }; data.get_login_handler().bind(bind_request).await?; get_login_successful_response(&data, &username).await } async fn simple_login_handler( data: web::Data>, request: web::Json, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + LoginHandler + 'static, { simple_login(data, request) .await .unwrap_or_else(error_to_http_response) } #[instrument(skip_all, level = "debug", fields(name = %request.name))] async fn post_authorize( data: web::Data>, request: web::Json, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + LoginHandler + 'static, { let name = request.name.clone(); data.get_login_handler().bind(request.into_inner()).await?; get_login_successful_response(&data, &name).await } async fn post_authorize_handler( data: web::Data>, request: web::Json, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + LoginHandler + 'static, { post_authorize(data, request) .await .unwrap_or_else(error_to_http_response) } #[instrument(skip_all, level = "debug")] async fn opaque_register_start( request: actix_web::HttpRequest, payload: actix_web::web::Payload, data: web::Data>, ) -> TcpResult where Backend: BackendHandler + OpaqueHandler + 'static, { use actix_web::FromRequest; let inner_payload = &mut payload.into_inner(); let validation_result = BearerAuth::from_request(&request, inner_payload) .await .ok() .and_then(|bearer| check_if_token_is_valid(&data, bearer.token()).ok()) .ok_or_else(|| { TcpError::UnauthorizedError("Not authorized to change the user's password".to_string()) })?; let registration_start_request = web::Json::::from_request( &request, inner_payload, ) .await .map_err(|e| TcpError::BadRequest(format!("{:#?}", e)))? .into_inner(); let user_id = ®istration_start_request.username; let user_is_admin = data .get_readonly_handler() .get_user_groups(user_id) .await? .iter() .any(|g| g.display_name == "lldap_admin".into()); if !validation_result.can_change_password(user_id, user_is_admin) { return Err(TcpError::UnauthorizedError( "Not authorized to change the user's password".to_string(), )); } Ok(data .get_opaque_handler() .registration_start(registration_start_request) .await?) } async fn opaque_register_start_handler( request: actix_web::HttpRequest, payload: actix_web::web::Payload, data: web::Data>, ) -> ApiResult where Backend: BackendHandler + OpaqueHandler + 'static, { opaque_register_start(request, payload, data) .await .map(|res| ApiResult::Left(web::Json(res))) .unwrap_or_else(error_to_api_response) } #[instrument(skip_all, level = "debug")] async fn opaque_register_finish( data: web::Data>, request: web::Json, ) -> TcpResult where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, { data.get_opaque_handler() .registration_finish(request.into_inner()) .await?; Ok(HttpResponse::Ok().finish()) } async fn opaque_register_finish_handler( data: web::Data>, request: web::Json, ) -> HttpResponse where Backend: TcpBackendHandler + BackendHandler + OpaqueHandler + 'static, { opaque_register_finish(data, request) .await .unwrap_or_else(error_to_http_response) } pub struct CookieToHeaderTranslatorFactory; impl Transform for CookieToHeaderTranslatorFactory where S: Service, S::Future: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type InitError = (); type Transform = CookieToHeaderTranslator; type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ok(CookieToHeaderTranslator { service }) } } pub struct CookieToHeaderTranslator { service: S, } impl Service for CookieToHeaderTranslator where S: Service, S::Future: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; #[allow(clippy::type_complexity)] type Future = Pin>>>; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { self.service.poll_ready(cx) } fn call(&self, mut req: ServiceRequest) -> Self::Future { if let Some(token_cookie) = req.cookie("token") { if let Ok(header_value) = actix_http::header::HeaderValue::from_str(&format!( "Bearer {}", token_cookie.value() )) { req.headers_mut() .insert(actix_http::header::AUTHORIZATION, header_value); } else { return async move { Ok(req.error_response(ErrorBadRequest("Invalid token cookie"))) } .boxed_local(); } }; Box::pin(self.service.call(req)) } } #[instrument(skip_all, level = "debug", err, ret)] pub(crate) fn check_if_token_is_valid( state: &AppState, token_str: &str, ) -> Result { let token: Token<_> = VerifyWithKey::verify_with_key(token_str, &state.jwt_key) .map_err(|_| ErrorUnauthorized("Invalid JWT"))?; if token.claims().exp.lt(&Utc::now()) { return Err(ErrorUnauthorized("Expired JWT")); } if token.header().algorithm != jwt::AlgorithmType::Hs512 { return Err(ErrorUnauthorized(format!( "Unsupported JWT algorithm: '{:?}'. Supported ones are: ['HS512']", token.header().algorithm ))); } let jwt_hash = default_hash(token_str); if state.jwt_blacklist.read().unwrap().contains(&jwt_hash) { return Err(ErrorUnauthorized("JWT was logged out")); } Ok(state.backend_handler.get_permissions_from_groups( UserId::new(&token.claims().user), token .claims() .groups .iter() .map(|s| GroupName::from(s.as_str())), )) } pub fn configure_server(cfg: &mut web::ServiceConfig, enable_password_reset: bool) where Backend: TcpBackendHandler + LoginHandler + OpaqueHandler + BackendHandler + 'static, { cfg.service(web::resource("").route(web::post().to(post_authorize_handler::))) .service( web::resource("/opaque/login/start") .route(web::post().to(opaque_login_start::)), ) .service( web::resource("/opaque/login/finish") .route(web::post().to(opaque_login_finish_handler::)), ) .service( web::resource("/simple/login").route(web::post().to(simple_login_handler::)), ) .service(web::resource("/refresh").route(web::get().to(get_refresh_handler::))) .service(web::resource("/logout").route(web::get().to(get_logout_handler::))) .service( web::scope("/opaque/register") .wrap(CookieToHeaderTranslatorFactory) .service( web::resource("/start") .route(web::post().to(opaque_register_start_handler::)), ) .service( web::resource("/finish") .route(web::post().to(opaque_register_finish_handler::)), ), ); if enable_password_reset { cfg.service( web::resource("/reset/step1/{user_id}") .route(web::get().to(get_password_reset_step1_handler::)), ) .service( web::resource("/reset/step2/{token}") .route(web::get().to(get_password_reset_step2_handler::)), ); } }