totp: generate backup code, attempt to use backup code when checking totp, delete backup after successful use
This commit is contained in:
parent
830f1dc0ae
commit
da4b204601
8 changed files with 169 additions and 18 deletions
|
|
@ -44,7 +44,8 @@ impl Icon {
|
||||||
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
|
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
|
||||||
"users", Users, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#,
|
"users", Users, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#,
|
||||||
"mail", Mail, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>"#,
|
"mail", Mail, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>"#,
|
||||||
"password", Password, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-password" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 10v4"></path><path d="M10 13l4 -2"></path><path d="M10 11l4 2"></path><path d="M5 10v4"></path><path d="M3 13l4 -2"></path><path d="M3 11l4 2"></path><path d="M19 10v4"></path><path d="M17 13l4 -2"></path><path d="M17 11l4 2"></path></svg>"#
|
"password", Password, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-password" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 10v4"></path><path d="M10 13l4 -2"></path><path d="M10 11l4 2"></path><path d="M5 10v4"></path><path d="M3 13l4 -2"></path><path d="M3 11l4 2"></path><path d="M19 10v4"></path><path d="M17 13l4 -2"></path><path d="M17 11l4 2"></path></svg>"#,
|
||||||
|
"2fa-large", TwoFaLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-2fa" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path><path d="M10 16v-8h4"></path><path d="M10 12l3 0"></path><path d="M17 16v-6a2 2 0 0 1 4 0v6"></path><path d="M17 13l4 0"></path></svg>"#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +71,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
|
||||||
Icon::Users,
|
Icon::Users,
|
||||||
Icon::Mail,
|
Icon::Mail,
|
||||||
Icon::Password,
|
Icon::Password,
|
||||||
|
Icon::TwoFaLarge,
|
||||||
];
|
];
|
||||||
|
|
||||||
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::routes::oauth::{redirect_uri, AUTHORIZATION_CODE_LEN};
|
||||||
use crate::routes::prelude::*;
|
use crate::routes::prelude::*;
|
||||||
use apps::App;
|
use apps::App;
|
||||||
use authorization_codes::AuthorizationCode;
|
use authorization_codes::AuthorizationCode;
|
||||||
use hash::SecretString;
|
use hash::{Secret, SecretString};
|
||||||
use rocket::http::{Cookie, CookieJar};
|
use rocket::http::{Cookie, CookieJar};
|
||||||
use rocket::{get, post};
|
use rocket::{get, post};
|
||||||
use users::totp_login_request::TOTP_REQUEST_COOKIE_NAME;
|
use users::totp_login_request::TOTP_REQUEST_COOKIE_NAME;
|
||||||
|
|
@ -108,16 +108,36 @@ pub async fn totp_verify(
|
||||||
.totp_secret()
|
.totp_secret()
|
||||||
.ok_or_else(|| Error::bad_request("TOTP is not enabled for user"))?;
|
.ok_or_else(|| Error::bad_request("TOTP is not enabled for user"))?;
|
||||||
|
|
||||||
// Create totp
|
let (check_totp, delete_totp_backup) = match user.totp_backup_hashed().map(Secret::from_hash) {
|
||||||
let totp = totp::new(totp_secret, None, user.username().to_string())?;
|
Some(totp_backup) => {
|
||||||
|
let input_code = form.code.to_string();
|
||||||
|
|
||||||
// Verify totp code
|
let totp_backup_matches =
|
||||||
if !totp.check_current(form.code)? {
|
task::spawn_blocking(move || totp_backup.compare(&input_code)).await??;
|
||||||
return Ok(Either::Right(Flash::new(
|
|
||||||
Redirect::to(uri!(totp_page(auth_request))),
|
if totp_backup_matches {
|
||||||
FlashKind::Danger,
|
// Don't check totp, delete backup
|
||||||
"Wrong code. Please try again.",
|
(false, true)
|
||||||
)));
|
} else {
|
||||||
|
// Check totp (since the check failed), do not delete backup
|
||||||
|
(true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => (true, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
if check_totp {
|
||||||
|
// Create totp
|
||||||
|
let totp = totp::new(totp_secret, None, user.username().to_string())?;
|
||||||
|
|
||||||
|
// Verify totp code
|
||||||
|
if !totp.check_current(form.code)? {
|
||||||
|
return Ok(Either::Right(Flash::new(
|
||||||
|
Redirect::to(uri!(totp_page(auth_request))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
"Wrong code. Please try again.",
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate authorization code
|
// Generate authorization code
|
||||||
|
|
@ -131,6 +151,11 @@ pub async fn totp_verify(
|
||||||
// Mark totp token as used
|
// Mark totp token as used
|
||||||
totp_request.use_code(&mut transaction).await?;
|
totp_request.use_code(&mut transaction).await?;
|
||||||
|
|
||||||
|
// Delete totp backup if it got used
|
||||||
|
if delete_totp_backup {
|
||||||
|
user.set_totp_backup(&mut transaction, None).await?;
|
||||||
|
}
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
// Delete cookie
|
// Delete cookie
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
user_settings_security_password,
|
user_settings_security_password,
|
||||||
user_settings_security_totp,
|
user_settings_security_totp,
|
||||||
user_settings_security_totp_form,
|
user_settings_security_totp_form,
|
||||||
|
user_settings_security_totp_backup,
|
||||||
user_settings_visual,
|
user_settings_visual,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use crate::tokens::{
|
||||||
JWT_COOKIE_NAME, JWT_DURATION_MINUTES, REFRESH_TOKEN_COOKIE_NAME, REFRESH_TOKEN_DURATION_DAYS,
|
JWT_COOKIE_NAME, JWT_DURATION_MINUTES, REFRESH_TOKEN_COOKIE_NAME, REFRESH_TOKEN_DURATION_DAYS,
|
||||||
};
|
};
|
||||||
use apps::App;
|
use apps::App;
|
||||||
use hash::PaperKey;
|
use hash::{PaperKey, Secret, SecretString};
|
||||||
use jwt::database::Key;
|
use jwt::database::Key;
|
||||||
use jwt::PrivateKey;
|
use jwt::PrivateKey;
|
||||||
use refresh_tokens::RefreshToken;
|
use refresh_tokens::RefreshToken;
|
||||||
|
|
@ -402,3 +402,63 @@ pub async fn user_settings_security_totp_form(
|
||||||
"One-time password has been generated.",
|
"One-time password has been generated.",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct TotpBackupForm {
|
||||||
|
pub generate_totp_backup: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/settings/security/totp_backup", data = "<form>")]
|
||||||
|
pub async fn user_settings_security_totp_backup(
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
jwt_user: JwtUser,
|
||||||
|
form: Form<TotpBackupForm>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
let (flash_kind, flash_message) = if form.generate_totp_backup {
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
let user = User::get_by_id(&mut transaction, &UserID(jwt_user.0.subject))
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Could not find user"))?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
if user.totp_secret().is_some() {
|
||||||
|
// Create TOTP backup code
|
||||||
|
let backup_code = task::spawn_blocking(|| SecretString::new_digits(8)).await?;
|
||||||
|
let plain_backup_code = backup_code.to_string();
|
||||||
|
|
||||||
|
let secret = task::spawn_blocking(|| Secret::new(backup_code)).await??;
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Save backup code
|
||||||
|
user.set_totp_backup(&mut transaction, Some(&secret))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
(
|
||||||
|
FlashKind::Success,
|
||||||
|
format!(
|
||||||
|
"Your backup code has been generated. It will only be shown once!\
|
||||||
|
<div class=\"mt-1 user-select-all\">{plain_backup_code}</div>"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
FlashKind::Warning,
|
||||||
|
"Enable TOTP first to get backup code.".into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(FlashKind::Warning, "Nothing to do.".into())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(user_settings_security)),
|
||||||
|
flash_kind,
|
||||||
|
flash_message,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,17 @@
|
||||||
Protect your account by requiring an additional code when you log in.</p>
|
Protect your account by requiring an additional code when you log in.</p>
|
||||||
<div>
|
<div>
|
||||||
{% if totp_enabled %}
|
{% if totp_enabled %}
|
||||||
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-disable-totp">
|
<div class="col-6 col-sm-4 col-md-2 col-xl py-1">
|
||||||
Disable OTP
|
<a class="btn btn-danger" data-bs-toggle="modal"
|
||||||
</a>
|
data-bs-target="#modal-disable-totp">
|
||||||
|
Disable OTP
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-4 col-md-2 col-xl py-1">
|
||||||
|
<a class="btn" data-bs-toggle="modal" data-bs-target="#modal-otp-backup">
|
||||||
|
Generate backup code
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="./security/totp" method="post">
|
<form action="./security/totp" method="post">
|
||||||
<button type="submit" name="enable" value="true" class="btn">
|
<button type="submit" name="enable" value="true" class="btn">
|
||||||
|
|
@ -273,4 +281,51 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Otp backup modal -->
|
||||||
|
<div class="modal modal-blur" tabindex="-1" id="modal-otp-backup">
|
||||||
|
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-status bg-info"></div>
|
||||||
|
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
|
||||||
|
<div class="text-info mb-2">
|
||||||
|
{% include "icons/2fa-large" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Generate new backup code</h3>
|
||||||
|
<div class="mt-2">A new backup code will be generated for your account.</div>
|
||||||
|
<div class="mt-2">This action will replace your current backup code if you have one.</div>
|
||||||
|
<div class="mt-2">Keep this code in a safe place!</div>
|
||||||
|
|
||||||
|
<div class="mt-4">Note: the backup code can only be used once.</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="w-100">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a href="#" class="btn w-100" data-bs-dismiss="modal">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<form action="./security/totp_backup" method="post">
|
||||||
|
<button type="submit" name="generate_totp_backup" value="true"
|
||||||
|
class="btn btn-primary w-100">
|
||||||
|
Generate code
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::hash::{hash, Hash};
|
use crate::hash::{hash, Hash};
|
||||||
use nanoid::nanoid;
|
use nanoid::nanoid;
|
||||||
use nanoid_dictionary::ALPHANUMERIC;
|
use nanoid_dictionary::{ALPHANUMERIC, NUMBERS};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
// Struct to generate the secret
|
// Struct to generate the secret
|
||||||
|
|
@ -10,6 +10,9 @@ impl SecretString {
|
||||||
pub fn new(length: usize) -> Self {
|
pub fn new(length: usize) -> Self {
|
||||||
Self(nanoid!(length, ALPHANUMERIC))
|
Self(nanoid!(length, ALPHANUMERIC))
|
||||||
}
|
}
|
||||||
|
pub fn new_digits(length: usize) -> Self {
|
||||||
|
Self(nanoid!(length, NUMBERS))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Default for SecretString {
|
impl Default for SecretString {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use database::sqlx::SqliteExecutor;
|
||||||
use database::Error as DatabaseError;
|
use database::Error as DatabaseError;
|
||||||
use database::Users as DatabaseUsers;
|
use database::Users as DatabaseUsers;
|
||||||
use email_address::EmailAddress;
|
use email_address::EmailAddress;
|
||||||
use hash::{PaperKey, Password};
|
use hash::{PaperKey, Password, Secret};
|
||||||
use id::UserID;
|
use id::UserID;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
@ -244,8 +244,10 @@ impl User {
|
||||||
pub async fn set_totp_backup(
|
pub async fn set_totp_backup(
|
||||||
&self,
|
&self,
|
||||||
conn: impl SqliteExecutor<'_>,
|
conn: impl SqliteExecutor<'_>,
|
||||||
backup: Option<&str>,
|
backup: Option<&Secret>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
let backup = backup.map(|backup| backup.hash());
|
||||||
|
|
||||||
DatabaseUsers::set_totp_backup(conn, self.id.as_ref(), backup).await?;
|
DatabaseUsers::set_totp_backup(conn, self.id.as_ref(), backup).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,7 @@ impl User {
|
||||||
pub fn totp_secret(&self) -> Option<Vec<u8>> {
|
pub fn totp_secret(&self) -> Option<Vec<u8>> {
|
||||||
self.totp_secret.clone()
|
self.totp_secret.clone()
|
||||||
}
|
}
|
||||||
|
pub fn totp_backup_hashed(&self) -> Option<&str> {
|
||||||
|
self.totp_backup.as_deref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue