use actix::Addr; use actix_identity::Identity; use actix_remote_ip::RemoteIP; use actix_web::{web, HttpRequest, HttpResponse, Responder}; use askama::Template; use std::sync::Arc; use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::users_actor::{LoginResult, UsersActor}; use crate::actors::{bruteforce_actor, users_actor}; use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; use crate::controllers::base_controller::{ build_fatal_error_page, redirect_user, redirect_user_for_login, }; use crate::data::action_logger::{Action, ActionLogger}; use crate::data::login_redirect::LoginRedirect; use crate::data::provider::{Provider, ProvidersManager}; use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::user::User; use crate::data::webauthn_manager::WebAuthManagerReq; pub struct BaseLoginPage<'a> { pub danger: Option, pub success: Option, pub page_title: &'static str, pub app_name: &'static str, pub redirect_uri: &'a LoginRedirect, } #[derive(Template)] #[template(path = "login/login.html")] struct LoginTemplate<'a> { _p: BaseLoginPage<'a>, login: String, providers: Vec, } #[derive(Template)] #[template(path = "login/password_reset.html")] struct PasswordResetTemplate<'a> { _p: BaseLoginPage<'a>, min_pass_len: usize, } #[derive(Template)] #[template(path = "login/choose_second_factor.html")] struct ChooseSecondFactorTemplate<'a> { _p: BaseLoginPage<'a>, user: &'a User, } #[derive(Template)] #[template(path = "login/otp_input.html")] struct LoginWithOTPTemplate<'a> { _p: BaseLoginPage<'a>, } #[derive(Template)] #[template(path = "login/webauthn_input.html")] struct LoginWithWebauthnTemplate<'a> { _p: BaseLoginPage<'a>, opaque_state: String, challenge_json: String, } #[derive(serde::Deserialize)] pub struct LoginRequestBody { login: String, password: String, } #[derive(serde::Deserialize)] pub struct LoginRequestQuery { logout: Option, #[serde(default)] redirect: LoginRedirect, } /// Authenticate user #[allow(clippy::too_many_arguments)] pub async fn login_route( remote_ip: RemoteIP, providers: web::Data>, users: web::Data>, bruteforce: web::Data>, query: web::Query, req: Option>, id: Option, http_req: HttpRequest, logger: ActionLogger, ) -> impl Responder { let mut danger = None; let mut success = None; let mut login = String::new(); let failed_attempts = bruteforce .send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into(), }) .await .unwrap(); if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS { return HttpResponse::TooManyRequests().body(build_fatal_error_page( "Too many failed login attempts, please try again later!", )); } // Check if user session must be closed if let Some(true) = query.logout { if let Some(id) = id { logger.log(Action::Signout); id.logout(); } success = Some("Goodbye!".to_string()); } // Check if user is already authenticated else if SessionIdentity(id.as_ref()).is_authenticated() { return redirect_user(query.redirect.get()); } // Check if the password of the user has to be changed else if SessionIdentity(id.as_ref()).need_new_password() { return redirect_user(&format!( "/reset_password?redirect={}", query.redirect.get_encoded() )); } // Check if the user has to validate a second factor else if SessionIdentity(id.as_ref()).need_2fa_auth() { return redirect_user(&format!( "/2fa_auth?redirect={}", query.redirect.get_encoded() )); } // Try to authenticate user else if let Some(req) = &req { login = req.login.clone(); let response: LoginResult = users .send(users_actor::LocalLoginRequest { login: login.clone(), password: req.password.clone(), }) .await .unwrap(); match response { LoginResult::Success(user) => { let status = if user.need_reset_password { logger.log(Action::UserNeedNewPasswordOnLogin(&user)); SessionStatus::NeedNewPassword } else if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0) { logger.log(Action::UserNeed2FAOnLogin(&user)); SessionStatus::Need2FA } else { logger.log(Action::UserSuccessfullyAuthenticated(&user)); SessionStatus::SignedIn }; SessionIdentity(id.as_ref()).set_user(&http_req, &user, status); return redirect_user(query.redirect.get()); } LoginResult::AccountDisabled => { log::warn!("Failed login for username {} : account is disabled", &login); logger.log(Action::TryLoginWithDisabledAccount(&login)); danger = Some("Your account is disabled!".to_string()); } LoginResult::LocalAuthForbidden => { log::warn!("Failed login for username {} : attempted to use local auth, but it is forbidden", &login); logger.log(Action::TryLocalLoginFromUnauthorizedAccount(&login)); danger = Some("You cannot login from local auth with your account!".to_string()); } LoginResult::Error => { danger = Some("An unkown error occured while trying to sign you in!".to_string()); } c => { log::warn!( "Failed login for ip {:?} / username {}: {:?}", remote_ip, login, c ); logger.log(Action::FailedLoginWithBadCredentials(&login)); danger = Some("Login failed.".to_string()); bruteforce .send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into(), }) .await .unwrap(); } } } HttpResponse::Ok().content_type("text/html").body( LoginTemplate { _p: BaseLoginPage { page_title: "Login", danger, success, app_name: APP_NAME, redirect_uri: &query.redirect, }, login, providers: providers.cloned(), } .render() .unwrap(), ) } /// Sign out user pub async fn logout_route() -> impl Responder { redirect_user("/login?logout=true") } #[derive(serde::Deserialize)] pub struct ChangePasswordRequestBody { password: String, } #[derive(serde::Deserialize)] pub struct PasswordResetQuery { #[serde(default)] redirect: LoginRedirect, } /// Reset user password route pub async fn reset_password_route( id: Option, query: web::Query, req: Option>, users: web::Data>, http_req: HttpRequest, logger: ActionLogger, ) -> impl Responder { let mut danger = None; if !SessionIdentity(id.as_ref()).need_new_password() { return redirect_user_for_login(query.redirect.get()); } let user_id = SessionIdentity(id.as_ref()).user_id(); // Check if user is setting a new password if let Some(req) = &req { if req.password.len() < MIN_PASS_LEN { danger = Some("Password is too short!".to_string()); } else { let res: bool = users .send(users_actor::ChangePasswordRequest { user_id: user_id.clone(), new_password: req.password.clone(), temporary: false, }) .await .unwrap(); if !res { danger = Some("Failed to change password!".to_string()); } else { SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn); logger.log(Action::UserChangedPasswordOnLogin(&user_id)); return redirect_user(query.redirect.get()); } } } HttpResponse::Ok().content_type("text/html").body( PasswordResetTemplate { _p: BaseLoginPage { page_title: "Password reset", danger, success: None, app_name: APP_NAME, redirect_uri: &query.redirect, }, min_pass_len: MIN_PASS_LEN, } .render() .unwrap(), ) } #[derive(serde::Deserialize)] pub struct ChooseSecondFactorQuery { #[serde(default)] redirect: LoginRedirect, #[serde(default = "bool::default")] force_display: bool, } /// Let the user select the factor to use to authenticate pub async fn choose_2fa_method( id: Option, query: web::Query, users: web::Data>, ) -> impl Responder { if !SessionIdentity(id.as_ref()).need_2fa_auth() { log::trace!("User does not require 2fa auth, redirecting"); return redirect_user_for_login(query.redirect.get()); } let user: User = users .send(users_actor::GetUserRequest( SessionIdentity(id.as_ref()).user_id(), )) .await .unwrap() .0 .expect("Could not find user!"); // Automatically choose factor if there is only one factor if user.get_distinct_factors_types().len() == 1 && !query.force_display { log::trace!("User has only one factor, using it by default"); return redirect_user(&user.two_factor[0].login_url(&query.redirect)); } HttpResponse::Ok().content_type("text/html").body( ChooseSecondFactorTemplate { _p: BaseLoginPage { page_title: "Two factor authentication", danger: None, success: None, app_name: APP_NAME, redirect_uri: &query.redirect, }, user: &user, } .render() .unwrap(), ) } #[derive(serde::Deserialize)] pub struct LoginWithOTPQuery { #[serde(default)] redirect: LoginRedirect, } #[derive(serde::Deserialize)] pub struct LoginWithOTPForm { code: String, } /// Login with OTP pub async fn login_with_otp( id: Option, query: web::Query, form: Option>, users: web::Data>, http_req: HttpRequest, remote_ip: RemoteIP, logger: ActionLogger, ) -> impl Responder { let mut danger = None; if !SessionIdentity(id.as_ref()).need_2fa_auth() { return redirect_user_for_login(query.redirect.get()); } let user: User = users .send(users_actor::GetUserRequest( SessionIdentity(id.as_ref()).user_id(), )) .await .unwrap() .0 .expect("Could not find user!"); let keys = user.get_otp_factors(); if keys.is_empty() { return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!")); } if let Some(form) = form { if !keys .iter() .any(|k| k.check_code(&form.code).unwrap_or(false)) { logger.log(Action::OTPLoginAttempt { success: false, user: &user, }); danger = Some("Specified code is invalid!".to_string()); } else { users .send(users_actor::AddSuccessful2FALogin( user.uid.clone(), remote_ip.0, )) .await .unwrap(); SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn); logger.log(Action::OTPLoginAttempt { success: true, user: &user, }); return redirect_user(query.redirect.get()); } } HttpResponse::Ok().body( LoginWithOTPTemplate { _p: BaseLoginPage { danger, success: None, page_title: "Two-Factor Auth", app_name: APP_NAME, redirect_uri: &query.redirect, }, } .render() .unwrap(), ) } #[derive(serde::Deserialize)] pub struct LoginWithWebauthnQuery { #[serde(default)] redirect: LoginRedirect, } /// Login with Webauthn pub async fn login_with_webauthn( id: Option, query: web::Query, manager: WebAuthManagerReq, users: web::Data>, ) -> impl Responder { if !SessionIdentity(id.as_ref()).need_2fa_auth() { return redirect_user_for_login(query.redirect.get()); } let user: User = users .send(users_actor::GetUserRequest( SessionIdentity(id.as_ref()).user_id(), )) .await .unwrap() .0 .expect("Could not find user!"); let pub_keys = user.get_webauthn_pub_keys(); if pub_keys.is_empty() { return HttpResponse::Ok() .body(build_fatal_error_page("No Webauthn public key registered!")); } let challenge = match manager.start_authentication(&user.uid, &pub_keys) { Ok(c) => c, Err(e) => { log::error!("Failed to generate webauthn challenge! {:?}", e); return HttpResponse::InternalServerError().body(build_fatal_error_page( "Failed to generate webauthn challenge", )); } }; let challenge_json = match serde_json::to_string(&challenge.login_challenge) { Ok(r) => r, Err(e) => { log::error!("Failed to serialize challenge! {:?}", e); return HttpResponse::InternalServerError().body("Failed to serialize challenge!"); } }; HttpResponse::Ok().body( LoginWithWebauthnTemplate { _p: BaseLoginPage { danger: None, success: None, page_title: "Two-Factor Auth", app_name: APP_NAME, redirect_uri: &query.redirect, }, opaque_state: challenge.opaque_state, challenge_json: urlencoding::encode(&challenge_json).to_string(), } .render() .unwrap(), ) }