Basic 2FA in Rocket

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:

As with last time, this is for demonstration purposes only and this code is not to be trusted.

Time-based One-time Password Basics

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.

QR codes

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.

Verifying OTP

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("/")
}

Code

The demo mentioned in the intro can be found here.

Author

Shawn Kinkade — January, 2017