JWT & Access Roles in Rocket

This experiment:

uses Argon2i hashes for passwords

generates the user a JSON Web Token

uses the roles stored in the JWT to determine page access

Note: This is purely an experiment. It is ugly code and you should not use it for literally anything.

Purpose

In the Yii framework for PHP, a relatively easy form of access control is to assign accounts roles (or derive them from LDAP/AD), and check them with:

Yii:: $app ->user->can ( 'role' );

I wanted to see if I could do something similar in Rust, and I also wanted to learn a bit about JSON Web Tokens. What I have at the moment, is putting a string array in the token, so we can just more or less do token.roles.contains("role") .

Diesel

This is a less-than-ideal setup but is sufficient for a test. Our Diesel migration and model are simply username, password, and a (PostgreSQL-specific) string array of roles.

CREATE TABLE users ( username TEXT PRIMARY KEY , pw_hash TEXT NOT NULL , user_roles TEXT[] NOT NULL DEFAULT '{"user"}' );

#[ derive ( Queryable )] pub struct User { pub username: String , pub pw_hash: String , pub user_roles: Vec < String >, }

JWT

There are a number of JWT crates of varying maturity. I settled on jsonwebtoken, but you would need e.g. franke_jwt if you want signatures and not just HMAC.

#[ derive ( Debug , RustcEncodable , RustcDecodable )] struct UserRolesToken { // issued at iat: i64 , // expiration exp: i64 , user: String , roles: Vec < String >, } impl UserRolesToken { fn has_role(& self , role: & str ) -> bool { self .roles.contains(&role.to_string()) } } fn jwt_generate(user: String , roles: Vec < String >) -> String { let now = time::get_time().sec; let payload = UserRolesToken { iat: now, exp: now + ONE_WEEK, user: user, roles: roles, }; encode(Header::default(), &payload, KEY).unwrap() }

Authentication

Pretty straight-forward. Get the user, verify the password. If things check out, generate a JWT and store it in their cookies.

#[ derive ( FromForm )] struct Login { username: String , password: String , } #[ post ( "/login" , data = "<login_form>" )] fn login(cookies: &Cookies, login_form: Form<Login>) -> Redirect { use schema::users::dsl::*; let login = login_form.get(); let connection = establish_connection(); let user = match users.filter(username.eq(&login.username)) .first::<models::User>(&connection) { Ok (u) => u, Err (_) => return Redirect::to( "/login" ), }; let hash = user.pw_hash.into_bytes(); // Argon2 password verifier let db_hash = Encoded::from_u8(&hash).expect( "Failed to read password hash" ); if !db_hash.verify(login.password.as_ref()) { return Redirect::to( "/login" ); } // Add JWT to cookies cookies.add(Cookie::new( "jwt" .into(), jwt_generate(user.username, user.roles))); Redirect::to( "/" ) }

Restricted Pages

Here’s where things get a bit messy. Obviously we don’t want to be constantly checking and re-checking cookies and roles for every route. What I would really like is some additional macro goodness that would allow us to simply write something like…

#[ jwt ( user == "admin" || roles . contains ( "c-level" ))] #[ get ( "/path" )] fn special_page() -> ...

In this case, I have a dynamic path for a /admin prefix which performs the token verification, and returns the results of non-routed functions.

#[ get ( "/admin/<path>" )] fn admin_handler(cookies: &Cookies, path: & str ) -> Option <Template> { let token = match cookies.find( "jwt" ).map(|cookie| cookie.value) { Some (jwt) => jwt, _ => return None , }; // You'll want to match on and log errors instead of unwrapping, of course let token_data = decode::<UserRolesToken>(&token, KEY, Algorithm::HS256).unwrap(); if !token_data.claims.has_role( "admin" ) { return None ; } match path { "index" => return Some (admin_index()), "user" => return Some (display_user(token_data.claims.user)), _ => return None , } }

Code

Working demo site can be found here.

Author

Shawn Kinkade — January, 2017