half impl done
This commit is contained in:
parent
77a681c02a
commit
62c9ea0855
8 changed files with 202 additions and 3 deletions
41
Cargo.lock
generated
41
Cargo.lock
generated
|
@ -229,6 +229,12 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base32"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
|
@ -431,6 +437,12 @@ version = "0.9.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
|
@ -654,6 +666,7 @@ dependencies = [
|
|||
"rocket_dyn_templates",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"totp-rs",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
@ -2068,6 +2081,17 @@ version = "1.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
|
||||
|
||||
[[package]]
|
||||
name = "qrcodegen-image"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "221b7eace1aef8c95d65dbe09fb7a1a43d006045394a89afba6997721fcb7708"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"image",
|
||||
"qrcodegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.38"
|
||||
|
@ -3110,6 +3134,23 @@ dependencies = [
|
|||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "totp-rs"
|
||||
version = "5.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90"
|
||||
dependencies = [
|
||||
"base32",
|
||||
"constant_time_eq",
|
||||
"hmac",
|
||||
"qrcodegen-image",
|
||||
"rand",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
|
|
|
@ -23,3 +23,4 @@ orion = "0.17"
|
|||
uuid = { version = "1.11", features = ["serde"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
qrcode-generator = "5"
|
||||
totp-rs = { version = "5.6", features = ["gen_secret", "qr"] }
|
||||
|
|
|
@ -371,6 +371,18 @@ input#ident {
|
|||
font-size: 2em;
|
||||
}
|
||||
|
||||
.totp-qrcode>img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: 160pt;
|
||||
}
|
||||
|
||||
.totp-form input {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
header.padded>nav {
|
||||
display: flex;
|
||||
|
@ -405,4 +417,4 @@ input#ident {
|
|||
div.fields.submit {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod otp;
|
||||
pub mod pw;
|
||||
pub mod session;
|
||||
|
|
53
src/auth/otp.rs
Normal file
53
src/auth/otp.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use rocket::{
|
||||
http::{Cookie, CookieJar},
|
||||
time::Duration,
|
||||
};
|
||||
use totp_rs::{Secret, TOTP};
|
||||
|
||||
const ISSUER: &'static str = "dolltags";
|
||||
|
||||
fn make_cookie<'a>(value: String) -> Cookie<'a> {
|
||||
Cookie::build(("otp_init", value))
|
||||
.max_age(Duration::minutes(5))
|
||||
.http_only(true)
|
||||
.same_site(rocket::http::SameSite::Strict)
|
||||
.path("/account/settings")
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Creates a TOTP instance using in-code defaults and user OTP settings.
|
||||
/// i use the internal user account UUID instead of their username to cover the case
|
||||
/// where a user needs to change their account username.
|
||||
pub fn make_totp(account_id: &str, secret: Vec<u8>) -> Result<TOTP, totp_rs::TotpUrlError> {
|
||||
TOTP::new(
|
||||
totp_rs::Algorithm::SHA1,
|
||||
6,
|
||||
1,
|
||||
30,
|
||||
secret,
|
||||
Some(String::from(ISSUER)),
|
||||
String::from(account_id),
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds the provided OTP secret as encrypted cookie for 5 minutes
|
||||
pub fn cache_secret<'a>(cookies: &CookieJar<'a>, secret: &'a Secret) {
|
||||
if let Secret::Encoded(b32) = secret.to_encoded() {
|
||||
cookies.add_private(make_cookie(b32));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to get a potentially existing secret from the cookies.
|
||||
/// If no cookie is set or if the cookie's value doesn't parse to
|
||||
/// a valid base32 secret this returns None
|
||||
pub fn get_secret(cookies: &CookieJar<'_>) -> Option<Secret> {
|
||||
cookies.get_private("otp_init").and_then(|cookie| {
|
||||
let val = cookie.value_trimmed();
|
||||
Secret::Encoded(String::from(val)).to_raw().ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Manually revokes the encrypted cookie
|
||||
pub fn remove_secret(cookies: &CookieJar<'_>) {
|
||||
cookies.remove_private(make_cookie(String::from("woof")))
|
||||
}
|
|
@ -83,6 +83,7 @@ fn rocket() -> _ {
|
|||
account::common::confirm_terminate_account,
|
||||
account::common::export_data,
|
||||
account::otp::show_otp_enable_start,
|
||||
account::otp::handle_otp_enable_start,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
|
|
|
@ -1,6 +1,70 @@
|
|||
use rocket::{form::Form, http::CookieJar, response::Redirect};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use totp_rs::Secret;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{
|
||||
self,
|
||||
schema::{DollTagsDb, User},
|
||||
},
|
||||
pages::CommonTemplateState,
|
||||
routes,
|
||||
routes::error_handlers::PageResult,
|
||||
};
|
||||
|
||||
#[get("/settings/otp")]
|
||||
pub fn show_otp_enable_start() -> Template {
|
||||
Template::render("account/otp/start", context! {})
|
||||
pub async fn show_otp_enable_start(
|
||||
mut db: DollTagsDb,
|
||||
user: User,
|
||||
cookies: &CookieJar<'_>,
|
||||
meta: CommonTemplateState,
|
||||
) -> PageResult {
|
||||
if db::otp::has_otp(&mut *db, &user.id).await? {
|
||||
return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into());
|
||||
}
|
||||
let totp_secret = Secret::generate_secret();
|
||||
auth::otp::cache_secret(cookies, &totp_secret);
|
||||
|
||||
let totp = auth::otp::make_totp(&user.id.to_string(), totp_secret.to_bytes()?)?;
|
||||
let totp_qrcode = totp.get_qr_base64()?;
|
||||
|
||||
Ok(Template::render(
|
||||
"account/otp/start",
|
||||
context! {
|
||||
meta,
|
||||
totp_qrcode,
|
||||
secret: totp.get_secret_base32(),
|
||||
},
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct OtpEnableForm {
|
||||
#[field(validate = len(6..=6))]
|
||||
pub otp_code: String,
|
||||
}
|
||||
|
||||
#[post("/settings/otp", data = "<form>")]
|
||||
pub async fn handle_otp_enable_start(
|
||||
mut db: DollTagsDb,
|
||||
form: Form<OtpEnableForm>,
|
||||
user: User,
|
||||
cookies: &CookieJar<'_>,
|
||||
_meta: CommonTemplateState,
|
||||
) -> PageResult {
|
||||
if db::otp::has_otp(&mut *db, &user.id).await? {
|
||||
return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into());
|
||||
}
|
||||
let secret = match auth::otp::get_secret(cookies) {
|
||||
Some(v) => v,
|
||||
None => return Ok(Redirect::to(uri!("/account", show_otp_enable_start)).into()),
|
||||
};
|
||||
|
||||
let totp = auth::otp::make_totp(&user.id.to_string(), secret.to_bytes()?)?;
|
||||
|
||||
if !totp.check_current(&form.otp_code)? {}
|
||||
|
||||
todo!("meow")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
{% extends "base" %}
|
||||
{% import "macros/form" as form %}
|
||||
{% block title %}Adding a one-time password - {% endblock title %}
|
||||
{% block main %}
|
||||
<p>To add a one-time password provider, scan this QrCode with your authenticator app.</p>
|
||||
|
||||
<section class="split">
|
||||
<div class="totp-qrcode">
|
||||
<img src="data:image/png;base64, {{totp_qrcode}}" alt="{{secret}}" />
|
||||
<p class="note">Alternatively, you can copy/paste this code: <code>{{secret}}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="totp-form">
|
||||
<p><label for="otp_code">
|
||||
Once it's added on your authenticator app,
|
||||
enter the generated 6-digit code.
|
||||
</label></p>
|
||||
|
||||
<form method="post">
|
||||
<input type="text" name="otp_code" id="otp_code" minlength="6" maxlength="6" required />
|
||||
|
||||
<button type="submit">Enable 2FA</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock main %}
|
Loading…
Add table
Reference in a new issue