use actix::Addr; use actix_identity::Identity; use actix_web::{HttpResponse, Responder, web}; use askama::Template; use crate::actors::{bruteforce_actor, users_actor}; use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor}; 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::login_redirect::LoginRedirect; use crate::data::remote_ip::RemoteIP; use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; use crate::data::webauthn_manager::WebAuthManagerReq; struct BaseLoginPage<'a> { danger: Option, success: Option, page_title: &'static str, app_name: &'static str, redirect_uri: &'a LoginRedirect, } #[derive(Template)] #[template(path = "login/login.html")] struct LoginTemplate<'a> { _p: BaseLoginPage<'a>, login: String, } #[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>, factors: &'a [TwoFactor], } #[derive(Template)] #[template(path = "login/opt_input.html")] struct LoginWithOTPTemplate<'a> { _p: BaseLoginPage<'a>, factor: &'a TwoFactor, } #[derive(Template)] #[template(path = "login/webauthn_input.html")] struct LoginWithWebauthnTemplate<'a> { _p: BaseLoginPage<'a>, factor: &'a TwoFactor, 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 pub async fn login_route( remote_ip: RemoteIP, users: web::Data>, bruteforce: web::Data>, query: web::Query, req: Option>, id: Identity, ) -> 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 { id.forget(); success = Some("Goodbye!".to_string()); } // Check if user is already authenticated if SessionIdentity(&id).is_authenticated() { return redirect_user(query.redirect.get()); } // Check if the password of the user has to be changed if SessionIdentity(&id).need_new_password() { return redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded())); } // Check if the user has to valide a second factor if SessionIdentity(&id).need_2fa_auth() { return redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded())); } // Try to authenticate user if let Some(req) = &req { login = req.login.clone(); let response: LoginResult = users .send(users_actor::LoginRequest { login: login.clone(), password: req.password.clone(), }) .await .unwrap(); match response { LoginResult::Success(user) => { SessionIdentity(&id).set_user(&user); return if user.need_reset_password { SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded())) } else if user.has_two_factor() { SessionIdentity(&id).set_status(SessionStatus::Need2FA); redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded())) } else { redirect_user(query.redirect.get()) }; } LoginResult::AccountDisabled => { log::warn!("Failed login for username {} : account is disabled", login); danger = Some("Your account is disabled!".to_string()); } c => { log::warn!("Failed login for ip {:?} / username {}: {:?}", remote_ip, login, c); 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, } .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: Identity, query: web::Query, req: Option>, users: web::Data>) -> impl Responder { let mut danger = None; if !SessionIdentity(&id).need_new_password() { return redirect_user_for_login(query.redirect.get()); } // 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: ChangePasswordResult = users .send(users_actor::ChangePasswordRequest { user_id: SessionIdentity(&id).user_id(), new_password: req.password.clone(), temporary: false, }) .await .unwrap(); if !res.0 { danger = Some("Failed to change password!".to_string()); } else { SessionIdentity(&id).set_status(SessionStatus::SignedIn); 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: Identity, query: web::Query, users: web::Data>) -> impl Responder { if !SessionIdentity(&id).need_2fa_auth() { return redirect_user_for_login(query.redirect.get()); } let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) .await.unwrap().0.expect("Could not find user!"); // Automatically choose factor if there is only one factor if user.two_factor.len() == 1 && !query.force_display { 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, }, factors: &user.two_factor, } .render() .unwrap(), ) } #[derive(serde::Deserialize)] pub struct LoginWithOTPQuery { #[serde(default)] redirect: LoginRedirect, id: FactorID, } #[derive(serde::Deserialize)] pub struct LoginWithOTPForm { code: String, } /// Login with OTP pub async fn login_with_otp(id: Identity, query: web::Query, form: Option>, users: web::Data>) -> impl Responder { let mut danger = None; if !SessionIdentity(&id).need_2fa_auth() { return redirect_user_for_login(query.redirect.get()); } let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) .await.unwrap().0.expect("Could not find user!"); let factor = match user.find_factor(&query.id) { Some(f) => f, None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!")) }; let key = match &factor.kind { TwoFactorType::TOTP(key) => key, _ => { return HttpResponse::Ok().body(build_fatal_error_page("Factor is not a TOTP key!")); } }; if let Some(form) = form { if !key.check_code(&form.code).unwrap_or(false) { danger = Some("Specified code is invalid!".to_string()); } else { SessionIdentity(&id).set_status(SessionStatus::SignedIn); 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, }, factor, }.render().unwrap()) } #[derive(serde::Deserialize)] pub struct LoginWithWebauthnQuery { #[serde(default)] redirect: LoginRedirect, id: FactorID, } /// Login with Webauthn pub async fn login_with_webauthn(id: Identity, query: web::Query, manager: WebAuthManagerReq, users: web::Data>) -> impl Responder { if !SessionIdentity(&id).need_2fa_auth() { return redirect_user_for_login(query.redirect.get()); } let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) .await.unwrap().0.expect("Could not find user!"); let factor = match user.find_factor(&query.id) { Some(f) => f, None => return HttpResponse::Ok().body(build_fatal_error_page("Factor not found!")) }; let key = match &factor.kind { TwoFactorType::WEBAUTHN(key) => key, _ => { return HttpResponse::Ok() .body(build_fatal_error_page("Factor is not a Webauthn key!")); } }; let challenge = match manager.start_authentication(&user.uid, key) { 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, }, factor, opaque_state: challenge.opaque_state, challenge_json: urlencoding::encode(&challenge_json).to_string(), }.render().unwrap()) }