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:

on /create , the user has an option of generating a QR code to be scanned into e.g. FreeOTP

, the user has an option of generating a QR code to be scanned into e.g. FreeOTP if the user does this and enters the correct code, the auth_token for the account they’re making is set

for the account they’re making is set users for whom auth_token IS NOT NULL are required to use their soft tokens

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