DEV Community

Werner Echezuría
Werner Echezuría

Posted on • Updated on

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

Oldest comments (14)

Collapse
 
michaeltharrington profile image
Michael Tharrington

Hey Werner,

Great series here!

Anyway, got a quick tip for ya!

You could edit all of these posts to include "series: whatever name you'd like for your series" in the front matter of each one. This'll connect your posts with a cool little dot scroll option at the top of each post that lets the reader easily flip between posts in the series.

I've no idea what this option is actually called, so we're going with "dot scroll" ... but anyway, it looks like this:

my series

... in my posts here & here. Haha, totally feeling the guilt for abandoning this series, right now. 😔

Anyway, it's not a must-do by any means, just a nice-to-have in case you wanna!

Collapse
 
werner profile image
Werner Echezuría

Thanks!, I was thinking about how to do something like that.

Collapse
 
michaeltharrington profile image
Michael Tharrington

No problem at all! 😀

Collapse
 
ghost profile image
Ghost • Edited

Hi Werner,

Nice series, have you consider making a testing part? I'm having problems testing handlers with the Identity middleware and passing Form data to the TestRequest. I've searched everywhere for this and found nothing; and also the official documentation is very lacking in this regard.

Again, thanks for the good work

Collapse
 
werner profile image
Werner Echezuría

Hi, thanks for your kind words, the testing part is out, if you find it useful, please let me know, if you doesn't please let me know too, :).

Collapse
 
ghost profile image
Ghost

Seriously? It was a coincidence? are you some sort of superhero? are you reading my mind right now? (I wouldn't recommend that)

thanks a lot, plus another lot. I'm gonna check it out right now :)

Thread Thread
 
werner profile image
Werner Echezuría

lol, you made my day, thanks for your words. Yes, it was a coincidence. I'm planning on creating more content and release it as soon as I can, I'm waiting for your comments on that post, even if it's a bad one, :P.

Collapse
 
ghost profile image
Ghost • Edited

Hi, is there a reason to choose jwt over the default actix-web auth system?

Collapse
 
werner profile image
Werner Echezuría

The default actix-web auth system uses cookies, I wanted to take advantage of all the features that jwt provides, like stateless authentication, so, I don't need to request a token against the database for every action the user does. That's the idea in theory, in a next post I'll try to write a front application that consumes the jwt.

Collapse
 
lightwizzard profile image
Jeffrey Scott Flesher

Not you codes problem, but a problem with csrf-token

Problem with this line:

csrf-token = { git = "ssh://git@github.com/3dom-co-jp/csrf-token.git", branch="v0.2.x" }

It has issues, I am sure someone will fix it soon, seems to be with ssh, it is not set to public, so it asks for credentials.

I tried to use this with the same result, and I am sure this is temporary.

csrf-token = { git = "ssh://git@github.com/future-science-research/csrf-token.git", branch="v0.2.x" }

My question is why not use this instead:

github.com/heartsucker/rust-csrf

Currently, I can not build the project because of this, but great article.

I am looking at converting it just to check it out, thanks.

Collapse
 
werner profile image
Werner Echezuría

Hi, thanks for reporting this, I'll fix it as soon as possible.

Regards.

Collapse
 
lightwizzard profile image
Jeffrey Scott Flesher

I was reading about security issues with jwt, I am looking at docs.rs/crate/rust-argon2/0.6.0 it uses Argon2i, I know you said you use it for its stateless features, but that is also an exploitable security risk, whereas Cookies are not the best way to stay stateless and be secure, you can use in-memory cookies or even in-memory sessions, you can even encrypt them, but passing them in json is a nightmare for middle man attacks, and in-memory are safer, and faster IMO.

I am working on this now, trying to come up with a better solution.

Collapse
 
werner profile image
Werner Echezuría

csrf-token = { git = "ssh://git@github.com/3dom-co-jp/csrf-token.git", branch="v0.2.x" }

Yeah, sorry, it's fixed in master:

csrf-token = { git = "git@github.com/3dom-co-jp/csrf-tok...", branch="v0.2.x" }

-

My question is why not use this instead:

github.com/heartsucker/rust-csrf

I had not found a way to use it easily with Actix web, it seems a plugin to be used with iron.

Collapse
 
henrik41 profile image
Henrik

It would be great to add Oauth2 login to include facebook and google using the Oauth2 library would be a good start.