Practical Rust Web Development - Authentication

werner

Werner Echezuría

Posted on May 28, 2019

Practical Rust Web Development - Authentication

In this post of the series, I'll be covering authentication. This is based on this one but I'll be using the 1.0 version of actix-web.

We're going to use jwt to authenticate the user in a cookie, one security consideration is the CSRF vulnerability when using cookies, so, we'll use a crate to help us with that. If we were going to use local storage we would need XSS protection. There are other security precautions when using jwt you should be aware of, like these. If you have any other security suggestion, please make a comment.

We'll be creating the user after register, to improve security you can add email verification, a captcha or 2FA.

We're going to need to add some crates:

src/Cargo.toml:

jsonwebtoken = "6"
bcrypt = "0.4.0"
chrono = { version = "0.4.6", features = ["serde"] }
csrf-token = { git = "ssh://git@github.com/3dom-co-jp/csrf-token.git", branch="v0.2.x" }

We need our user model as well, but first let's create a migration for the table.

diesel migration generate create_users

The generated migration:

migrations/2019-05-19-165021_create_users/up.sql:

CREATE TABLE users (
  email VARCHAR(100) NOT NULL PRIMARY KEY,
  password VARCHAR(64) NOT NULL,
  created_at TIMESTAMP NOT NULL
);
CREATE INDEX users_email_company_idx ON users (email, company);

migrations/2019-05-19-165021_create_users/down.sql:

DROP TABLE users;
diesel migration run

The User model

The user model is next, in order to create an user we used a RegisterUser struct and created it through NewUser, I'm doing it this way because we don't need a password_confirmation field in the database, however we would need it in the register action. The other struct is AuthUser, I'm using it for authentication only, with two fields, email and password, seems a little boilerplate but the rewards worth it.

src/models/mod.rs:

pub mod user;

src/models/user.rs:

use chrono::NaiveDateTime; // This type is used for date field in Diesel.
use crate::schema::users;

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "users"]
pub struct User {
    #[serde(skip)] // we're removing id from being show in the response
    pub id: i32,
    pub email: String,
    pub company: String,
    #[serde(skip)] // we're removing password from being show in the response
    pub password: String,
    pub created_at: NaiveDateTime
}

#[derive(Debug, Serialize, Deserialize, Insertable)]
#[table_name = "users"]
pub struct NewUser {
    pub email: String,
    pub company: String,
    pub password: String,
    pub created_at: NaiveDateTime
}

use bcrypt::{hash, DEFAULT_COST};
use diesel::PgConnection;
use chrono::Local;
use crate::errors::MyStoreError;

// MyStoreError is a custom error that I will show it next.
impl User {
    pub fn create(register_user: RegisterUser, connection: &PgConnection) ->
     Result<User, MyStoreError> {
        use diesel::RunQueryDsl;

        Ok(diesel::insert_into(users::table)
            .values(NewUser {
                email: register_user.email,
                company: register_user.company,
                password: Self::hash_password(register_user.password)?,
                created_at: Local::now().naive_local()
            })
            .get_result(connection)?)
    }

    // This might look kind of weird, 
    // but if something fails it would chain 
    // to our MyStoreError Error, 
    // otherwise it will gives us the hash, 
    // we still need to return a result 
    // so we wrap it in an Ok variant from the Result type. 
    pub fn hash_password(plain: String) -> Result<String, MyStoreError> {
        Ok(hash(plain, DEFAULT_COST)?)
    }
}

#[derive(Deserialize)]
pub struct RegisterUser {
    pub email: String,
    pub company: String,
    pub password: String,
    pub password_confirmation: String
}

impl RegisterUser {
    pub fn validates(self) ->
     Result<RegisterUser, MyStoreError> {
         if self.password == self.password_confirmation {
             Ok(self)
         } else {
             Err(
                 MyStoreError::PasswordNotMatch(
                     "Password and Password Confirmation does not match".to_string()
                 )
             )
         }
    }
}

#[derive(Deserialize)]
pub struct AuthUser {
    pub email: String,
    pub password: String
}

impl AuthUser {

    // The good thing about ? syntax and have a custom error is 
    // that the code would look very straightforward, I mean, 
    // the other way would imply a lot of pattern matching 
    // making it look ugly. 
    pub fn login(&self, connection: &PgConnection) ->
     Result<User, MyStoreError> {
        use bcrypt::verify;
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use diesel::ExpressionMethods;
        use crate::schema::users::dsl::email;

        let mut records =
            users::table
                .filter(email.eq(&self.email))
                .load::<User>(connection)?;

        let user =
            records
                .pop()
                .ok_or(MyStoreError::DBError(diesel::result::Error::NotFound))?;

        let verify_password =
            verify(&self.password, &user.password)
                .map_err( |_error| {
                    MyStoreError::WrongPassword(
                        "Wrong password, check again please".to_string()
                    )
                })?;

        if verify_password {
            Ok(user)
        } else {
            Err(MyStoreError::WrongPassword(
                "Wrong password, check again please".to_string()
            ))
        }

    }
}

If you run cargo build you would see an error:

the trait `diesel::Expression` is not implemented for 
`chrono::naive::datetime::NaiveDateTime`

That indicates we just need to add a feature to diesel in Cargo.toml, it would look like this:

src/Cargo.toml:

diesel = { version = "1.0.0", features = ["postgres", "r2d2", "chrono"] }

Now, it should compile without problems.

Custom Error

In the User model you could see a lot of MyStoreError errors, the idea is have a unified custom error you can manipulate and make it easy to have a more readable code, thanks to the ? syntax sugar, because it needs to have the same error so it can chain itself to other functions that calls another function that returns a Result type, the Rust book has a very good explanation about the ? operator.

src/errors.rs:

use std::fmt;
use bcrypt::BcryptError;
use diesel::result;

pub enum MyStoreError {
    HashError(BcryptError),
    DBError(result::Error),
    PasswordNotMatch(String),
    WrongPassword(String)
}

// We need this to performs a conversion from BcryptError to MyStoreError
impl From<BcryptError> for MyStoreError {
    fn from(error: BcryptError) -> Self {
        MyStoreError::HashError(error)
    }
}

// We need this to performs a conversion from diesel::result::Error to MyStoreError
impl From<result::Error> for MyStoreError {
    fn from(error: result::Error) -> Self {
        MyStoreError::DBError(error)
    }
}

// We need this so we can use the method to_string over MyStoreError 
impl fmt::Display for MyStoreError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            MyStoreError::HashError(error) => write!(f, "{}", error),
            MyStoreError::DBError(error) => write!(f, "{}", error),
            MyStoreError::PasswordNotMatch(error) => write!(f, "{}", error),
            MyStoreError::WrongPassword(error) => write!(f, "{}", error)
        }
    }
}

src/main.rs:

pub mod errors;

The handlers

Now, we just need the handlers for register an user and for login.

src/handlers/register.rs:

use actix_web::web;
use crate::db_connection::PgPool;
use actix_web::HttpResponse;
use crate::handlers::pg_pool_handler;

use crate::models::user::{ User, RegisterUser };

// We get a new connection pool, validates the data, 
// `password` and `password_confirmation` should be the same, 
// finally we create the user and return it.
pub fn register(new_user: web::Json<RegisterUser>, pool: web::Data<PgPool>) ->
 Result<HttpResponse, HttpResponse> {
    let pg_pool = pg_pool_handler(pool)?;
    let register_user = new_user
        .into_inner()
        .validates()
        .map_err(|e| {
           HttpResponse::InternalServerError().json(e.to_string())
        })?;
    User::create(register_user, &pg_pool)
        .map(|user| HttpResponse::Ok().json(user))
        .map_err(|e| {
           HttpResponse::InternalServerError().json(e.to_string())
        })
}

src/handlers/authentication.rs:

use actix_web::HttpResponse;
use actix_web::middleware::identity::Identity;
use actix_web::web;
use csrf_token::CsrfTokenGenerator;
use hex;
use crate::utils::jwt::create_token;

use crate::models::user::AuthUser;
use crate::db_connection::PgPool;
use crate::handlers::pg_pool_handler;

// We get a new connection pool, then look up for the user,
// If there is no user a NotFound error would raise otherwise
// this would just through an InternalServerError.
pub fn login(auth_user: web::Json<AuthUser>, 
             id: Identity, 
             pool: web::Data<PgPool>, 
             generator: web::Data<CsrfTokenGenerator>) 
    -> Result<HttpResponse, HttpResponse> {
    let pg_pool = pg_pool_handler(pool)?;
    let user = auth_user
        .login(&pg_pool)
        .map_err(|e| {
            match e {
                MyStoreError::DBError(diesel::result::Error::NotFound) =>
                    HttpResponse::NotFound().json(e.to_string()),
                _ =>
                    HttpResponse::InternalServerError().json(e.to_string())
            }
        })?;

    // This is the jwt token we will send in a cookie.
    let token = create_token(&user.email, &user.company)?;

    id.remember(token);

    // Finally our response will have a csrf token for security. 
    let response =
        HttpResponse::Ok()
        .header("X-CSRF-TOKEN", hex::encode(generator.generate()))
        .json(user);
    Ok(response)
}

pub fn logout(id: Identity) -> Result<HttpResponse, HttpResponse> {
    id.forget();
    Ok(HttpResponse::Ok().into())
}

src/handlers/mod.rs:

pub mod products;
pub mod register;
pub mod authentication;

use actix_web::web;
use actix_web::HttpResponse;
use crate::db_connection::{ PgPool, PgPooledConnection };

// Because I'm using this function a lot, 
// I'm including it in the mod file accessible to all handlers.
pub fn pg_pool_handler(pool: web::Data<PgPool>) -> Result<PgPooledConnection, HttpResponse> {
    pool
    .get()
    .map_err(|e| {
        HttpResponse::InternalServerError().json(e.to_string())
    })
}

Json Web Token implementation

Now we can go on with the Jwt library, let's create a folder called utils and create a file named jwt.rs.

src/utils/jwt.rs:

use jwt::{decode, encode, Header, Validation};
use chrono::{Local, Duration};
use actix_web::HttpResponse;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    company: String,
    exp: usize
}

// We're using a struct so we can implement a conversion from
// Claims to SlimUser, useful in the decode function.
pub struct SlimUser {
    pub email: String,
    pub company: String
}

impl From<Claims> for SlimUser {
    fn from(claims: Claims) -> Self {
        SlimUser {
            email: claims.sub,
            company: claims.company
        }
    }
}

impl Claims {
    fn with_email(email: &str, company: &str) -> Self {
        Claims {
            sub: email.into(),
            company: company.into(),
            exp: (Local::now() + Duration::hours(24)).timestamp() as usize
        }
    }
}

pub fn create_token(email: &str, company: &str) -> Result<String, HttpResponse> {
    let claims = Claims::with_email(email, company);
    encode(&Header::default(), &claims, get_secret())
        .map_err(|e| HttpResponse::InternalServerError().json(e.to_string()))
}

pub fn decode_token(token: &str) -> Result<SlimUser, HttpResponse> {
    decode::<Claims>(token, get_secret(), &Validation::default())
        .map(|data| data.claims.into())
        .map_err(|e| HttpResponse::Unauthorized().json(e.to_string()))
}

fn get_secret<'a>() -> &'a [u8] {
    dotenv!("JWT_SECRET").as_bytes()
}

src/utils/mod.rs:

pub mod jwt;

FromRequest

We will need to use Actix Web FromRequest trait in order to our implementation can work with the log in authentication, the idea is use this trait to catch all requests and validates the csrf token and the jwt token.

In products handler there is a few modifications, because we need the LoggedUser struct in the request. I will omit the code in this post, but you can take a look at the source code in Github.

src/handlers/mod.rs:

use actix_web::{ FromRequest, HttpRequest, dev };
use actix_web::middleware::identity::Identity;
use crate::utils::jwt::{ decode_token, SlimUser };
pub type LoggedUser = SlimUser;

use hex;
use csrf_token::CsrfTokenGenerator;

impl FromRequest for LoggedUser {
    type Error = HttpResponse;
    type Config = ();
    type Future = Result<Self, HttpResponse>;

    fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
        let generator = 
            req.app_data::<CsrfTokenGenerator>()
            .ok_or(HttpResponse::InternalServerError())?;

        let csrf_token =
            req
                .headers()
                .get("x-csrf-token")
                .ok_or(HttpResponse::Unauthorized())?;

        let decoded_token =
            hex::decode(&csrf_token)
                .map_err(|error| HttpResponse::InternalServerError().json(error.to_string()))?;

        generator
            .verify(&decoded_token)
            .map_err(|_| HttpResponse::Unauthorized())?;

        // We're using the CookieIdentityPolicy middleware
        // to handle cookies, with this implementation this 
        // will validate the cookie according to the secret
        // provided in main function
        if let Some(identity) = Identity::from_request(req, payload)?.identity() {
            let user: SlimUser = decode_token(&identity)?;
            return Ok(user as LoggedUser);
        }  
        Err(HttpResponse::Unauthorized().into())
    }
}

Finally the main.rs file will look like this, we use different middlewares, one for logging, another for cookies, other for Cors and another for csrf token, although in the last one we use the data method that means is not really a middleware, it's just application data we're sharing though the application like the database connection:

src/main.rs:

fn main() {
    std::env::set_var("RUST_LOG", "actix_web=debug");
    env_logger::init();
    let sys = actix::System::new("mystore");

    let csrf_token_header = header::HeaderName::from_lowercase(b"x-csrf-token").unwrap();

    HttpServer::new(
    move || App::new()
        .wrap(Logger::default())
        // we implement middleares with the warp method
        .wrap( 
            IdentityService::new(
                CookieIdentityPolicy::new(dotenv!("SECRET_KEY").as_bytes())
                    .domain(dotenv!("MYSTOREDOMAIN"))
                    .name("mystorejwt")
                    .path("/")
                    .max_age(Duration::days(1).num_seconds())
                    .secure(dotenv!("COOKIE_SECURE").parse().unwrap())
            )
        )
        .wrap(
            cors::Cors::new()
                .allowed_origin(dotenv!("ALLOWED_ORIGIN"))
                .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
                .allowed_headers(vec![header::AUTHORIZATION,
                                      header::CONTENT_TYPE,
                                      header::ACCEPT,
                                      csrf_token_header.clone()])
                .expose_headers(vec![csrf_token_header.clone()])
                .max_age(3600)
        )
        .data(
            CsrfTokenGenerator::new(
                dotenv!("CSRF_TOKEN_KEY").as_bytes().to_vec(),
                Duration::hours(1)
            )
        )
        .data(establish_connection())
        .service(
            web::resource("/products")
                .route(web::get().to(handlers::products::index))
                .route(web::post().to(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to(handlers::products::show))
                .route(web::delete().to(handlers::products::destroy))
                .route(web::patch().to(handlers::products::update))
        )
        .service(
            web::resource("/register")
                .route(web::post().to(handlers::register::register))
        )
        .service(
            web::resource("/auth")
                .route(web::post().to(handlers::authentication::login))
                .route(web::delete().to(handlers::authentication::logout))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();

    println!("Started http server: 127.0.0.1:8088");
    let _ = sys.run();
}

Conclusion

We need to be cautious when authenticate our users, there are several security precautions you might take, OWASP provides several resources to protect our web application, like the top ten, so, just make your research and be diligent, if you have another suggestion to protect the app, please make a comment.

You can take a look at the full source code here

💖 💪 🙅 🚩
werner
Werner Echezuría

Posted on May 28, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related