Based on a modularized version of my last Rocket experiment, I decided to mess around with OTP-based second-factor authentication. How this demo is set up:
/create
, the user has an option of generating a QR code to be scanned into e.g. FreeOTPauth_token
for the account they’re making is setauth_token IS NOT NULL
are required to use their soft tokensAs with last time, this is for demonstration purposes only and this code is not to be trusted.
The URI format for TOTP at its most basic is very simple:
otpauth://totp/[username]?secret=[key]
Where key
is a base32-encoded string. If you have any spaces or special characters, this will need to be percent-encoded.
By default, this will be used to generate a six-digit code based on our key and 30-second time intervals using HMAC-SHA1.
We can use the qrcode crate to render our key URI to an image. Something like…
fn gen_qr(key: String, user: String) {
let path = "qrcodes/".to_string() + &key + ".png";
let payload = "otpauth://totp/RocketDemo:".to_string() + &user + "?secret=" + &key;
let qr = QrCode::new(payload.as_bytes()).unwrap();
let image: GrayImage = qr.render().to_image();
image.save(&path).unwrap();
}
Instead of saving them to a resolvable location however, for the moment I’m encoding them as a data URI. The demo code for this is ugly and hacky and I’m very welcome to recommendations.
There’s a number of OTP crates. I’m using libreauth.
fn verify_totp(key: String, code: String) -> bool {
let totp = TOTPBuilder::new()
.base32_key(&key)
.tolerance(1) // allows for one period (30s) of clock drift
.finalize()
.unwrap();
totp.is_valid(&code)
}
#[derive(FromForm)]
struct Login {
username: String,
password: String,
auth_code: Option<String>,
}
#[post("/login", data="<login_form>")]
fn login(cookies: &Cookies, login_form: Form<Login>) -> Redirect {
let login = login_form.get();
// input validation and checks
// fetch user from db
if user.auth_token.is_some() {
let auth_code = match login.auth_code.clone() {
Some(code) => code,
None => return Redirect::to("/login"),
};
if !auth_2fa::verify_topt(user.auth_token.unwrap(), auth_code) {
return Redirect::to("/login");
}
}
// check more stuff
// set cookies appropriately
Redirect::to("/")
}
The demo mentioned in the intro can be found here.
Shawn Kinkade — January, 2017