diff --git a/src/constants.rs b/src/constants.rs index 6603348..a8fc7ef 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -19,6 +19,9 @@ pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30; /// Maximum session duration (6 hours) pub const MAX_SESSION_DURATION: u64 = 3600 * 6; +/// Maximum length of a second factor name +pub const MAX_SECOND_FACTOR_NAME_LEN: usize = 25; + /// When the user successfully authenticate using 2FA, period of time during which the user is /// exempted from this IP address to use 2FA pub const SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN: u64 = 7 * 24 * 3600; diff --git a/src/controllers/admin_api.rs b/src/controllers/admin_api.rs index 5499cf8..684665e 100644 --- a/src/controllers/admin_api.rs +++ b/src/controllers/admin_api.rs @@ -1,7 +1,9 @@ +use crate::actors::users_actor; use actix::Addr; use actix_web::{web, HttpResponse, Responder}; use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor}; +use crate::data::action_logger::{Action, ActionLogger}; use crate::data::current_user::CurrentUser; use crate::data::user::UserID; @@ -37,13 +39,25 @@ pub async fn delete_user( user: CurrentUser, req: web::Form, users: web::Data>, + action_logger: ActionLogger, ) -> impl Responder { if user.uid == req.user_id { return HttpResponse::BadRequest().body("You can not remove your own account!"); } + let user = match users + .send(users_actor::GetUserRequest(req.user_id.clone())) + .await + .unwrap() + .0 + { + None => return HttpResponse::NotFound().body("Could not find a user to remove!"), + Some(u) => u, + }; + let res = users.send(DeleteUserRequest(req.0.user_id)).await.unwrap(); if res.0 { + action_logger.log(Action::AdminDeleteUser(&user)); HttpResponse::Ok().finish() } else { HttpResponse::InternalServerError().finish() diff --git a/src/controllers/admin_controller.rs b/src/controllers/admin_controller.rs index ce98fa6..2757640 100644 --- a/src/controllers/admin_controller.rs +++ b/src/controllers/admin_controller.rs @@ -8,6 +8,7 @@ use crate::actors::users_actor; use crate::actors::users_actor::UsersActor; use crate::constants::TEMPORARY_PASSWORDS_LEN; use crate::controllers::settings_controller::BaseSettingsPage; +use crate::data::action_logger::{Action, ActionLogger}; use crate::data::client::{Client, ClientID, ClientManager}; use crate::data::current_user::CurrentUser; use crate::data::user::{hash_password, User, UserID}; @@ -67,6 +68,7 @@ pub async fn users_route( user: CurrentUser, users: web::Data>, update_query: Option>, + logger: ActionLogger, ) -> impl Responder { let mut danger = None; let mut success = None; @@ -112,6 +114,8 @@ pub async fn users_route( let new_password = match update.0.gen_new_password.is_some() { false => None, true => { + logger.log(Action::AdminResetUserPassword(&user)); + let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN); user.password = hash_password(&temp_pass).expect("Failed to hash password"); user.need_reset_password = true; @@ -121,6 +125,7 @@ pub async fn users_route( }; if update.0.clear_2fa_history.is_some() { + logger.log(Action::AdminClear2FAHistory(&user)); user.last_successful_2fa = Default::default(); } @@ -140,8 +145,14 @@ pub async fn users_route( ) } else { success = Some(match is_creating { - true => format!("User {} was successfully created!", user.full_name()), - false => format!("User {} was successfully updated!", user.full_name()), + true => { + logger.log(Action::AdminCreateUser(&user)); + format!("User {} was successfully created!", user.full_name()) + } + false => { + logger.log(Action::AdminUpdateUser(&user)); + format!("User {} was successfully updated!", user.full_name()) + } }); if let Some(pass) = new_password { diff --git a/src/controllers/login_api.rs b/src/controllers/login_api.rs index b1e0bfa..19043a0 100644 --- a/src/controllers/login_api.rs +++ b/src/controllers/login_api.rs @@ -1,5 +1,6 @@ use crate::actors::users_actor; use crate::actors::users_actor::UsersActor; +use crate::data::action_logger::{Action, ActionLogger}; use crate::data::remote_ip::RemoteIP; use actix::Addr; use actix_identity::Identity; @@ -22,6 +23,7 @@ pub async fn auth_webauthn( http_req: HttpRequest, remote_ip: RemoteIP, users: web::Data>, + logger: ActionLogger, ) -> impl Responder { if !SessionIdentity(Some(&id)).need_2fa_auth() { return HttpResponse::Unauthorized().json("No 2FA required!"); @@ -32,15 +34,26 @@ pub async fn auth_webauthn( match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) { Ok(_) => { users - .send(users_actor::AddSuccessful2FALogin(user_id, remote_ip.0)) + .send(users_actor::AddSuccessful2FALogin( + user_id.clone(), + remote_ip.0, + )) .await .unwrap(); SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn); + logger.log(Action::LoginWebauthnAttempt { + success: true, + user_id, + }); HttpResponse::Ok().body("You are authenticated!") } Err(e) => { log::error!("Failed to authenticate user using webauthn! {:?}", e); + logger.log(Action::LoginWebauthnAttempt { + success: false, + user_id, + }); HttpResponse::InternalServerError().body("Failed to validate security key!") } } diff --git a/src/controllers/login_controller.rs b/src/controllers/login_controller.rs index 4517aed..54ad526 100644 --- a/src/controllers/login_controller.rs +++ b/src/controllers/login_controller.rs @@ -10,6 +10,7 @@ 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::remote_ip::RemoteIP; use crate::data::session_identity::{SessionIdentity, SessionStatus}; @@ -73,6 +74,7 @@ pub struct LoginRequestQuery { } /// Authenticate user +#[allow(clippy::too_many_arguments)] pub async fn login_route( remote_ip: RemoteIP, users: web::Data>, @@ -81,6 +83,7 @@ pub async fn login_route( req: Option>, id: Option, http_req: HttpRequest, + logger: ActionLogger, ) -> impl Responder { let mut danger = None; let mut success = None; @@ -102,6 +105,7 @@ pub async fn login_route( // 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()); @@ -138,11 +142,14 @@ pub async fn login_route( 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 }; @@ -151,7 +158,8 @@ pub async fn login_route( } LoginResult::AccountDisabled => { - log::warn!("Failed login for username {} : account is disabled", login); + log::warn!("Failed login for username {} : account is disabled", &login); + logger.log(Action::TryLoginWithDisabledAccount(&login)); danger = Some("Your account is disabled!".to_string()); } @@ -162,6 +170,7 @@ pub async fn login_route( login, c ); + logger.log(Action::FailedLoginWithBadCredentials(&login)); danger = Some("Login failed.".to_string()); bruteforce @@ -213,6 +222,7 @@ pub async fn reset_password_route( req: Option>, users: web::Data>, http_req: HttpRequest, + logger: ActionLogger, ) -> impl Responder { let mut danger = None; @@ -220,6 +230,8 @@ pub async fn reset_password_route( 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 { @@ -227,7 +239,7 @@ pub async fn reset_password_route( } else { let res: ChangePasswordResult = users .send(users_actor::ChangePasswordRequest { - user_id: SessionIdentity(id.as_ref()).user_id(), + user_id: user_id.clone(), new_password: req.password.clone(), temporary: false, }) @@ -238,6 +250,7 @@ pub async fn reset_password_route( 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()); } } @@ -328,6 +341,7 @@ pub async fn login_with_otp( users: web::Data>, http_req: HttpRequest, remote_ip: RemoteIP, + logger: ActionLogger, ) -> impl Responder { let mut danger = None; @@ -354,14 +368,25 @@ pub async fn login_with_otp( .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, remote_ip.0)) + .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()); } } diff --git a/src/controllers/openid_controller.rs b/src/controllers/openid_controller.rs index c65f4e2..c7c69f9 100644 --- a/src/controllers/openid_controller.rs +++ b/src/controllers/openid_controller.rs @@ -10,6 +10,7 @@ use crate::actors::users_actor::UsersActor; use crate::actors::{openid_sessions_actor, users_actor}; use crate::constants::*; use crate::controllers::base_controller::build_fatal_error_page; +use crate::data::action_logger::{Action, ActionLogger}; use crate::data::app_config::AppConfig; use crate::data::client::{ClientID, ClientManager}; use crate::data::code_challenge::CodeChallenge; @@ -112,6 +113,7 @@ pub async fn authorize( query: web::Query, clients: web::Data, sessions: web::Data>, + logger: ActionLogger, ) -> impl Responder { let client = match clients.find_by_id(&query.client_id) { None => { @@ -171,7 +173,7 @@ pub async fn authorize( // Save all authentication information in memory let session = Session { session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)), - client: client.id, + client: client.id.clone(), user: user.uid.clone(), auth_time: SessionIdentity(Some(&id)).auth_time(), redirect_uri, @@ -190,6 +192,7 @@ pub async fn authorize( .unwrap(); log::trace!("New OpenID session: {:#?}", session); + logger.log(Action::NewOpenIDSession { client: &client }); HttpResponse::Found() .append_header(( diff --git a/src/controllers/settings_controller.rs b/src/controllers/settings_controller.rs index 991c924..f62a88f 100644 --- a/src/controllers/settings_controller.rs +++ b/src/controllers/settings_controller.rs @@ -6,6 +6,7 @@ use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::users_actor::UsersActor; use crate::actors::{bruteforce_actor, users_actor}; use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; +use crate::data::action_logger::{Action, ActionLogger}; use crate::data::app_config::AppConfig; use crate::data::current_user::CurrentUser; use crate::data::remote_ip::RemoteIP; @@ -82,6 +83,7 @@ pub async fn change_password_route( req: Option>, bruteforce: web::Data>, remote_ip: RemoteIP, + logger: ActionLogger, ) -> impl Responder { let mut danger = None; let mut success = None; @@ -130,6 +132,7 @@ pub async fn change_password_route( danger = Some("An error occurred while trying to change your password!".to_string()); } else { + logger.log(Action::ChangedHisPassword); success = Some("Your password was successfully changed!".to_string()); } } diff --git a/src/controllers/two_factor_api.rs b/src/controllers/two_factor_api.rs index 2703762..8cb49ed 100644 --- a/src/controllers/two_factor_api.rs +++ b/src/controllers/two_factor_api.rs @@ -5,11 +5,22 @@ use webauthn_rs::prelude::RegisterPublicKeyCredential; use crate::actors::users_actor; use crate::actors::users_actor::UsersActor; +use crate::constants::MAX_SECOND_FACTOR_NAME_LEN; +use crate::data::action_logger::{Action, ActionLogger}; use crate::data::current_user::CurrentUser; use crate::data::totp_key::TotpKey; use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; use crate::data::webauthn_manager::WebAuthManagerReq; +fn preprocess_factor_name(name: &str) -> String { + name.replace('<', "<") + .replace('>', ">") + .chars() + .take(MAX_SECOND_FACTOR_NAME_LEN) + .filter(|c| *c != '\n' && *c != '\t' && *c != '\r' && c.is_ascii()) + .collect() +} + #[derive(serde::Deserialize)] pub struct AddTOTPRequest { factor_name: String, @@ -21,6 +32,7 @@ pub async fn save_totp_factor( user: CurrentUser, form: web::Json, users: web::Data>, + logger: ActionLogger, ) -> impl Responder { let key = TotpKey::from_encoded_secret(&form.secret); @@ -32,16 +44,20 @@ pub async fn save_totp_factor( )); } - if form.factor_name.is_empty() { - return HttpResponse::BadRequest().body("Please give a name to the factor!"); + let factor_name = preprocess_factor_name(&form.factor_name); + if factor_name.is_empty() { + return HttpResponse::BadRequest().body("Please give a valid name to the factor!"); } - let mut user = User::from(user); - user.add_factor(TwoFactor { + let factor = TwoFactor { id: FactorID(Uuid::new_v4().to_string()), - name: form.0.factor_name, + name: factor_name, kind: TwoFactorType::TOTP(key), - }); + }; + logger.log(Action::AddNewFactor(&factor)); + + let mut user = User::from(user); + user.add_factor(factor); let res = users .send(users_actor::UpdateUserRequest(user)) .await @@ -67,7 +83,13 @@ pub async fn save_webauthn_factor( form: web::Json, users: web::Data>, manager: WebAuthManagerReq, + logger: ActionLogger, ) -> impl Responder { + let factor_name = preprocess_factor_name(&form.factor_name); + if factor_name.is_empty() { + return HttpResponse::BadRequest().body("Please give a valid name to the factor!"); + } + let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) { Ok(k) => k, Err(e) => { @@ -76,12 +98,15 @@ pub async fn save_webauthn_factor( } }; - let mut user = User::from(user); - user.add_factor(TwoFactor { + let factor = TwoFactor { id: FactorID(Uuid::new_v4().to_string()), - name: form.0.factor_name, + name: factor_name, kind: TwoFactorType::WEBAUTHN(Box::new(key)), - }); + }; + logger.log(Action::AddNewFactor(&factor)); + + let mut user = User::from(user); + user.add_factor(factor); let res = users .send(users_actor::UpdateUserRequest(user)) .await @@ -104,9 +129,10 @@ pub async fn delete_factor( user: CurrentUser, form: web::Json, users: web::Data>, + logger: ActionLogger, ) -> impl Responder { let mut user = User::from(user); - user.remove_factor(form.0.id); + user.remove_factor(form.0.id.clone()); let res = users .send(users_actor::UpdateUserRequest(user)) @@ -117,6 +143,9 @@ pub async fn delete_factor( if !res { HttpResponse::InternalServerError().body("Failed to update user information!") } else { + logger.log(Action::Removed2FAFactor { + factor_id: &form.0.id, + }); HttpResponse::Ok().body("Removed factor!") } } @@ -124,11 +153,13 @@ pub async fn delete_factor( pub async fn clear_login_history( user: CurrentUser, users: web::Data>, + logger: ActionLogger, ) -> impl Responder { users .send(users_actor::Clear2FALoginHistory(user.uid.clone())) .await .unwrap(); + logger.log(Action::ClearedHisLoginHistory); HttpResponse::Ok().body("History successfully cleared") } diff --git a/src/controllers/two_factors_controller.rs b/src/controllers/two_factors_controller.rs index a9dc6e7..35711f9 100644 --- a/src/controllers/two_factors_controller.rs +++ b/src/controllers/two_factors_controller.rs @@ -1,5 +1,6 @@ use std::ops::Deref; +use crate::constants::MAX_SECOND_FACTOR_NAME_LEN; use actix_web::{HttpResponse, Responder}; use askama::Template; use qrcode_generator::QrCodeEcc; @@ -25,6 +26,7 @@ struct AddTotpPage { qr_code: String, account_name: String, secret_key: String, + max_name_len: usize, } #[derive(Template)] @@ -33,6 +35,7 @@ struct AddWebauhtnPage { _p: BaseSettingsPage, opaque_state: String, challenge_json: String, + max_name_len: usize, } /// Manage two factors authentication methods route @@ -70,6 +73,7 @@ pub async fn add_totp_factor_route(user: CurrentUser) -> impl Responder { qr_code: base64::encode(qr_code), account_name: key.account_name(&user, AppConfig::get()), secret_key: key.get_secret(), + max_name_len: MAX_SECOND_FACTOR_NAME_LEN, } .render() .unwrap(), @@ -104,6 +108,7 @@ pub async fn add_webauthn_factor_route( opaque_state: registration_request.opaque_state, challenge_json: urlencoding::encode(&challenge_json).to_string(), + max_name_len: MAX_SECOND_FACTOR_NAME_LEN, } .render() .unwrap(), diff --git a/src/data/action_logger.rs b/src/data/action_logger.rs new file mode 100644 index 0000000..cf752e0 --- /dev/null +++ b/src/data/action_logger.rs @@ -0,0 +1,180 @@ +use std::future::Future; +use std::net::IpAddr; +use std::pin::Pin; + +use actix::Addr; +use actix_identity::Identity; +use actix_web::dev::Payload; +use actix_web::{web, Error, FromRequest, HttpRequest}; + +use crate::actors::users_actor; +use crate::actors::users_actor::UsersActor; +use crate::data::client::Client; +use crate::data::remote_ip::RemoteIP; +use crate::data::session_identity::SessionIdentity; +use crate::data::user::{FactorID, TwoFactor, User, UserID}; + +pub enum Action<'a> { + AdminCreateUser(&'a User), + AdminUpdateUser(&'a User), + AdminDeleteUser(&'a User), + AdminResetUserPassword(&'a User), + AdminClear2FAHistory(&'a User), + LoginWebauthnAttempt { success: bool, user_id: UserID }, + Signout, + UserNeed2FAOnLogin(&'a User), + UserSuccessfullyAuthenticated(&'a User), + UserNeedNewPasswordOnLogin(&'a User), + TryLoginWithDisabledAccount(&'a str), + FailedLoginWithBadCredentials(&'a str), + UserChangedPasswordOnLogin(&'a UserID), + OTPLoginAttempt { user: &'a User, success: bool }, + NewOpenIDSession { client: &'a Client }, + ChangedHisPassword, + ClearedHisLoginHistory, + AddNewFactor(&'a TwoFactor), + Removed2FAFactor { factor_id: &'a FactorID }, +} + +impl<'a> Action<'a> { + pub fn as_string(&self) -> String { + match self { + Action::AdminDeleteUser(user) => { + format!("deleted account of {}", user.quick_identity()) + } + Action::AdminCreateUser(user) => { + format!("created account of {}", user.quick_identity()) + } + Action::AdminUpdateUser(user) => { + format!("updated account of {}", user.quick_identity()) + } + Action::AdminResetUserPassword(user) => { + format!( + "set a temporary password for the account of {}", + user.quick_identity() + ) + } + Action::AdminClear2FAHistory(user) => { + format!("cleared 2FA history of {}", user.quick_identity()) + } + Action::LoginWebauthnAttempt { success, user_id } => match success { + true => format!( + "successfully performed webauthn attempt for user {:?}", + user_id + ), + false => format!("performed FAILED webauthn attempt for user {:?}", user_id), + }, + Action::Signout => "signed out".to_string(), + Action::UserNeed2FAOnLogin(user) => { + format!( + "successfully authenticated as user {:?} but need to do 2FA authentication", + user.quick_identity() + ) + } + Action::UserSuccessfullyAuthenticated(user) => { + format!("successfully authenticated as {}", user.quick_identity()) + } + Action::UserNeedNewPasswordOnLogin(user) => format!( + "successfully authenticated as {}, but need to set a new password", + user.quick_identity() + ), + Action::TryLoginWithDisabledAccount(login) => format!( + "successfully authenticated as {}, but this is a DISABLED ACCOUNT", + login + ), + Action::FailedLoginWithBadCredentials(login) => format!( + "attempted to authenticate as {} but with a WRONG PASSWORD", + login + ), + Action::UserChangedPasswordOnLogin(user_id) => { + format!("set a new password at login as user {:?}", user_id) + } + Action::OTPLoginAttempt { user, success } => match success { + true => format!( + "successfully performed OTP attempt for user {}", + user.quick_identity() + ), + false => format!( + "performed FAILED OTP attempt for user {}", + user.quick_identity() + ), + }, + Action::NewOpenIDSession { client } => { + format!("opened a new OpenID session with {:?}", client.id) + } + Action::ChangedHisPassword => "changed his password".to_string(), + Action::ClearedHisLoginHistory => "cleared his login history".to_string(), + Action::AddNewFactor(factor) => format!( + "added a new {} factor with name {} and id {:?} to his account", + factor.type_str(), + factor.name, + factor.id, + ), + Action::Removed2FAFactor { factor_id } => format!("Removed his factor {:?}", factor_id), + } + } +} + +pub struct ActionLogger { + ip: IpAddr, + user: Option, +} + +impl ActionLogger { + pub fn log(&self, action: Action) { + log::info!( + "{} from {} has {}", + match &self.user { + None => "Anonymous user".to_string(), + Some(u) => u.quick_identity(), + }, + self.ip.to_string(), + action.as_string() + ) + } +} + +impl FromRequest for ActionLogger { + type Error = Error; + type Future = Pin>>>; + + #[inline] + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let req = req.clone(); + + Box::pin(async move { + let user_actor: &web::Data> = + req.app_data().expect("UserActor undefined!"); + + let user_actor: Addr = user_actor.as_ref().clone(); + + let user_id = Identity::from_request(&req, &mut Payload::None) + .into_inner() + .ok() + .and_then(|id| { + let sess = SessionIdentity(Some(&id)); + match sess.is_authenticated() { + true => Some(sess.user_id()), + false => None, + } + }); + + Ok(Self { + ip: RemoteIP::from_request(&req, &mut Payload::None) + .await + .unwrap() + .0, + user: match user_id { + None => None, + Some(u) => { + user_actor + .send(users_actor::GetUserRequest(u)) + .await + .unwrap() + .0 + } + }, + }) + }) + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs index ba74ba4..d752edd 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,4 +1,5 @@ pub mod access_token; +pub mod action_logger; pub mod app_config; pub mod client; pub mod code_challenge; diff --git a/src/data/user.rs b/src/data/user.rs index 4fe7c17..81351c0 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -113,6 +113,19 @@ impl User { format!("{} {}", self.first_name, self.last_name) } + pub fn quick_identity(&self) -> String { + format!( + "{} {} {} ({:?})", + match self.admin { + true => "admin", + false => "user", + }, + self.username, + self.email, + self.uid + ) + } + pub fn can_access_app(&self, id: &ClientID) -> bool { match &self.authorized_clients { None => true, diff --git a/templates/settings/add_2fa_totp_page.html b/templates/settings/add_2fa_totp_page.html index 3d15c6f..962595e 100644 --- a/templates/settings/add_2fa_totp_page.html +++ b/templates/settings/add_2fa_totp_page.html @@ -28,8 +28,8 @@ - Please give a name to your device to identity it more easily + value="Authenticator app" minlength="1" maxlength="{{ max_name_len }}" required/> + Please give a name to your device to identify it more easily later.
Please give a name to this authenticator app
diff --git a/templates/settings/add_webauthn_page.html b/templates/settings/add_webauthn_page.html index 0838d58..b65e6fb 100644 --- a/templates/settings/add_webauthn_page.html +++ b/templates/settings/add_webauthn_page.html @@ -9,7 +9,7 @@ + value="Security key" minlength="1" maxlength="{{ max_name_len }}" required/> Please give a name to your key to identify it more easily later.
Please give a name to this security key