Auth Web Microservice with rust using Actix-Web 1.0 - Complete Tutorial

June 9, 2019

Harry Gill
categories: rust tutorial | Tags: rust actix-web jwt auth web-server
19minute Read

What?

We are going to create a web-server in rust that only deals with user registration and authentication. I will be explaining the steps in each file as we go. The complete project code is here repo. Please take all this with a pinch of salt as I’m a still a noob to rust 😉.

Flow of the event would look like this:
  • Registers with email address ➡ Receive an 📨 with a link to verify
  • Follow the link ➡ register with same email and a password
  • Login with email and password ➡ Get verified and receive jwt token
Crates we are going to use
  • actix // Actix is a Rust actors framework.
  • actix-web // Actix web is a simple, pragmatic and extremely fast web framework for Rust.
  • actix-rt // Actix Runtime
  • actix-files // Static files support for actix web.
  • brcypt // Easily hash and verify passwords using bcrypt.
  • chrono // Date and time library for Rust.
  • diesel // A safe, extensible ORM and Query Builder for PostgreSQL, SQLite, and MySQL.
  • dotenv // A dotenv implementation for Rust.
  • derive_more // Convenience macros to derive tarits easily
  • env_logger // A logging implementation for log which is configured via an environment variable.
  • jsonwebtoken // Create and parse JWT in a strongly typed way.
  • futures // An implementation of futures and streams featuring zero allocations, composability, and iterator-like interfaces.
  • r2d2 // A generic connection pool.
  • serde // A generic serialization/deserialization framework.
  • serde_json // A JSON serialization file format.
  • serde_derive // Macros 1.1 implementation of #[derive(Serialize, Deserialize)].
  • sparkpost // Rust bindings for sparkpost email api v1.
  • uuid // A library to generate and parse UUIDs.

I have provided a brief info about the crates in use from their official description. If you want to know more about any of these crates please click on the name to go to crates.io. Shameless plug: sparkpost is my crate please leave feedback if you like/dislike it.

Prerequisite

I will assume here that you have some knowledge of programming, preferably some rust as well. A working setup of rust is required. Checkout https://rustup.rs for esasy rust setup. To know more about rust checkout The Book.

We will be using diesel to create models and deal with database, queries and migrations. Pleas head over to http://diesel.rs/guides/getting-started/ to get started and setup diesel_cli. In this tutorial we will be using postgresql so follow the instructions to setup for postgres. You need to have a running postgres server and can create a database to follow this tutorial through. Another nice to have tool is Cargo Watch that lets you watch the file system and re-compile and re-run the app when you make any changes.

Install Curl if don’t have it already on your system for testing the api locally.

Let’s Begin

After checking your rust and cargo version and creating a new project with

# at the time of writing this tutorial my setup is
rustc --version && cargo --version
# rustc 1.35.0 (3c235d560 2019-05-20)
# cargo 1.35.0 (6f3e9c367 2019-04-04)

cargo new simple-auth-server
# Created binary (application) `simple-auth-server` project

cd simple-auth-server # and then

# `cargo install cargo-watch` if you haven't already
# watch for changes re-compile and run
cargo watch -x run

Fill in the cargo dependencies with the following, I will go through each of them as get used in the project. I am using explicit versions of the crates, as you know things get old and change.(in case you are reading this tutorial after a long time it was created). In part 1 of this tutorial we won’t be using all of them but they will all become handy in the final app.

[dependencies]
actix = "0.8.2"
actix-rt = "0.2.2"
actix-web = "1.0.0"
actix-files = "0.1.1"

bcrypt = "0.4.0"
chrono = { version = "0.4.6", features = ["serde"] }
diesel = { version = "1.4.2", features = ["postgres", "uuidv07", "r2d2", "chrono"] }
dotenv = "0.14.1"
derive_more = "0.15.0"
env_logger = "0.6.1"
jsonwebtoken = "6.0.1"
futures = "0.1.27"
r2d2 = "0.8.5"
serde_derive="1.0"
serde_json="1.0"
serde="1.0"
sparkpost = "0.5.2"
uuid = { version = "0.7", features = ["serde", "v4"] }
Setup The Base APP

Create new file src/models.rs.

// models.rs
use actix::{Actor, SyncContext};
use diesel::pg::PgConnection;
use diesel::r2d2::{ConnectionManager, Pool};

/// This is db executor actor. can be run in parallel
pub struct DbExecutor(pub Pool<ConnectionManager<PgConnection>>);


// Actors communicate exclusively by exchanging messages.
// The sending actor can optionally wait for the response.
// Actors are not referenced directly, but by means of addresses.
// Any rust type can be an actor, it only needs to implement the Actor trait.
impl Actor for DbExecutor {
    type Context = SyncContext<Self>;
}

To use this Actor we need to set up actix-web server. We have the following in src/main.rs. We are leaving the resource or route builders empty for now. We will add logic to this as we go.

// main.rs
#![allow(dead_code)] // usful in dev mode
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate serde_derive;
use actix::prelude::*;
use actix_web::middleware::{
    Logger,
};
use actix_web::{web, App, HttpServer};
use diesel::{r2d2::ConnectionManager, PgConnection};
use dotenv::dotenv;


mod models;

use crate::models::DbExecutor;

fn main() -> std::io::Result<()> {
    dotenv().ok();
    std::env::set_var(
        "RUST_LOG",
        "simple-auth-server=debug,actix_web=info,actix_server=info",
    );
    env_logger::init();
    let sys = actix_rt::System::new("example");

    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    // create db connection pool
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.");

    let address: Addr<DbExecutor> =
        SyncArbiter::start(4, move || DbExecutor(pool.clone()));

    HttpServer::new(move || {

        App::new()
             // add database pool as data/state to the app
            .data(address.clone())
            // setup logger for requests
            .wrap(Logger::default())
            // everything under '/api/' route
            .service(
                web::scope("/api")
                    // routes for authentication
                    .service(
                        web::resource("/auth")
                            .route(web::get().to(||{})),
                    )
                    // routes to invitation
                    .service(
                        web::resource("/invitation")
                             .route(web::get().to(||{})),
                    )
                    // routes to register as a user after the
                    .service(
                        web::resource("/register/{invitation_id}")
                             .route(web::get().to(||{})),
                    ),
            )
    })
    .bind("127.0.0.1:3000")?
    .start();

    sys.run()
}

Assuming from the previous steps you have postgres and diesel-cli installed and working. In your terminal echo DATABASE_URL=postgres://username:password@localhost/database_name > .env replace database_name, username and password as you have setup. At this stage your server should compile and run on 127.0.0.1:3000. It doesn’t do anything useful for now. Let’s create some Models to use with diesel.

Setting up Diesel and creating our user Model

We start with creating a model for the user. We run diesel setup in the terminal. This will create our database if didn’t exist and setup a migration directory etc.

Let’s write some SQL, shall we. Create migrations by diesel migration generate users and invitation diesel migration generate invitations. Open the up.sql and down.sql files in migrations folder and add with following sql respectively.

--migrations/TIMESTAMP_users/up.sql
CREATE TABLE users (
  email VARCHAR(100) NOT NULL PRIMARY KEY,
  password VARCHAR(64) NOT NULL, --bcrypt hash
  created_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_users/down.sql
DROP TABLE users;

--migrations/TIMESTAMP_invitations/up.sql
CREATE TABLE invitations (
  id UUID NOT NULL PRIMARY KEY,
  email VARCHAR(100) NOT NULL,
  expires_at TIMESTAMP NOT NULL
);

--migrations/TIMESTAMP_invitations/down.sql
DROP TABLE invitations;

Command diesel migration run will create the table in the DB and a file src/schema.rs. This is the extent I will go about diesel-cli and migrations. Please read their documentation to learn more.

At this stage we have created the tables in the db, let’s write some code to create a representation of user and invitation in rust. In models.rs we add the following.

// models.rs
...
// --- snip
use chrono::NaiveDateTime;
use uuid::Uuid;
use schema::{users,invitations};

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "users"]
pub struct User {
    pub email: String,
    pub password: String,
    pub created_at: NaiveDateTime, // only NaiveDateTime works here due to diesel limitations
}

impl User {
    pub fn from_details(email: String, password: String) -> Self {
        User {
            email,
            password,
            created_at: Local::now().naive_local(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
#[table_name = "invitations"]
pub struct Invitation {
    pub id: Uuid,
    pub email: String,
    pub expires_at: NaiveDateTime,
}

Add, mod schema; to your main.rs to use schema generated by Diesel. Infact every time you create a new file in rust project you need to add it to the main.rs or lib.rs to use it. Check your implementation is free from errors/warnings and keep an eye on cargo watch -x run command in the terminal.

Our own error response type

Before we start implementing the handlers for various routes of our application let’s start by setting up a generic error response. It is not a compulsory requirement but can be useful in the future as your app grows.

This will allow us to send http error response with a custom message. Create a errors.rs file with the following content.

Rust provides use with really powerful tools that we can use to convert one type of error to another. In this app we will be doing a few operations using different carets i.e. saving data with diesel, hashing password with bcrypt etc. These operations may return errors but we need to convert them to our custom error type. std::convert::From is a trait that allows us to convert that. Read more about the From trait here. By implementing the From trait we can use the ? operator to propagate errors of many different types that would get converted to our ServiceError type.

Our error is defined in the errors.rs, let’s implement some From traits by adding impls for From uuid and diesel errors, we will also add a Unauthorized variant to our ServiceError enum.

// errors.rs
use actix_web::{error::ResponseError, HttpResponse};
use derive_more::Display;
use diesel::result::{DatabaseErrorKind, Error as DBError};
use std::convert::From;
use uuid::parser::ParseError;

#[derive(Debug, Display)]
pub enum ServiceError {
    #[display(fmt = "Internal Server Error")]
    InternalServerError,

    #[display(fmt = "BadRequest: {}", _0)]
    BadRequest(String),

    #[display(fmt = "Unauthorized")]
    Unauthorized,
}

// impl ResponseError trait allows to convert our errors into http responses with appropriate data
impl ResponseError for ServiceError {
    fn error_response(&self) -> HttpResponse {
        match *self {
            ServiceError::InternalServerError => HttpResponse::InternalServerError()
                .json("Internal Server Error, Please try later"),
            ServiceError::BadRequest(ref message) => {
                HttpResponse::BadRequest().json(message)
            }
            ServiceError::Unauthorized => {
                HttpResponse::Unauthorized().json("Unauthorized")
            }
        }
    }
}

// we can return early in our handlers if UUID provided by the user is not valid
// and provide a custom message
impl From<ParseError> for ServiceError {
    fn from(_: ParseError) -> ServiceError {
        ServiceError::BadRequest("Invalid UUID".into())
    }
}

impl From<DBError> for ServiceError {
    fn from(error: DBError) -> ServiceError {
        // Right now we just care about UniqueViolation from diesel
        // But this would be helpful to easily map errors as our app grows
        match error {
            DBError::DatabaseError(kind, info) => {
                if let DatabaseErrorKind::UniqueViolation = kind {
                    let message =
                        info.details().unwrap_or_else(|| info.message()).to_string();
                    return ServiceError::BadRequest(message);
                }
                ServiceError::InternalServerError
            }
            _ => ServiceError::InternalServerError,
        }
    }
}

Don’t forget to add mod errors; into your main.rs file to be able to use the custom error message.

Implementing handlers

We want our server to get an email from the client and create in invitation entry in the database. In this implementation we will be sending an email to user. If you don’t have the email service setup, you could simply ignore the email feature and just use the response data from the server for the purpose of learning.

From the actix documentation:

An Actor communicates with other actors by sending messages. In actix all messages are typed. A message can be any rust type which implements the Message trait.

And also

A request handler can be any object that implements Handler trait. Request handling happens in two stages. First the handler object is called, returning any object that implements the Responder trait. Then, respond_to() is called on the returned object, converting itself to a AsyncResult or Error.

Let’s implement the Handler for such request. Start by creating a new file src/invitation_handler.rs and create a following struct in it.

// invitation_handler.rs
use actix::{Handler, Message};
use chrono::{Duration, Local};
use diesel::{self, prelude::*};
use uuid::Uuid;

use crate::errors::ServiceError;
use crate::models::{DbExecutor, Invitation};

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

impl Message for CreateInvitation {
    type Result = Result<Invitation, ServiceError>;
}

impl Handler<CreateInvitation> for DbExecutor {
    type Result = Result<Invitation, ServiceError>;

    fn handle(&mut self, msg: CreateInvitation, _: &mut Self::Context) -> Self::Result {
        use crate::schema::invitations::dsl::*;
        let conn: &PgConnection = &self.0.get().unwrap();

        // creating a new Invitation object with expired at time that is 24 hours from now
        let new_invitation = Invitation {
            id: Uuid::new_v4(),
            email: msg.email.clone(),
            expires_at: Local::now().naive_local() + Duration::hours(24),
        };

        let inserted_invitation = diesel::insert_into(invitations)
            .values(&new_invitation)
            .get_result(conn)?;

        Ok(inserted_invitation)
    }
}

Don’t forget to add or uncomment mod invitation_handler; in your main.rs file.

Now we have a handler to insert and return an invitation to and from the DB. Create another file with the following content. register_email() function receives CreateInvitation struct and the state that holds the address of the DB. We send the actual signup_invitation struct by calling into_inner(). This function returns either the Invitation or an error as defined in our handler asynchronously.

// invitation_routes.rs

use actix::Addr;
use actix_web::{web, Error, HttpResponse, ResponseError};
use futures::future::Future;

// use crate::email_service::send_invitation;
use crate::invitation_handler::CreateInvitation;
use crate::models::DbExecutor;

pub fn register_email(
    signup_invitation: web::Json<CreateInvitation>,
    db: web::Data<Addr<DbExecutor>>,
) -> impl Future<Item = HttpResponse, Error = Error> {
    db.send(signup_invitation.into_inner())
        .from_err()
        .and_then(|db_response| match db_response {
            Ok(invitation) => {
                // send_invitation(&invitation); // this will become handy later on to send email
                dbg!(invitation); // to print data to terminal
                Ok(HttpResponse::Ok().into())
            }
            Err(err) => Ok(err.error_response()),
        })
}
Test your server

Register your new route function with the actix app, your router service closuer will look like the following.

// main.rs
// snip
// routes to invitation
    .service(
        web::resource("/invitation").route(
            web::post().to_async(invitation_routes::register_email),
        ),
    )
// snip

At this sage you should be able to test the http://localhost:3000/invitation route with the following curl command.

curl --request POST \
  --url http://localhost:3000/api/invitation \
  --header 'content-type: application/json' \
  --data '{"email":"test@test.com"}'
# dbg! will print something like this in your terminal where you are runnig the app
{
    "id": "67a68837-a059-43e6-a0b8-6e57e6260f0d",
    "email": "test@test.com",
    "expires_at": "2018-10-23T09:49:12.167510"
}
Using Sparkpost to send registration email

Please feel free to use any email service you like (I have no association with sparkpost apart from personal use) as long as you are able to replicate the sent email. Now that out of the way you need to add following in your .env file.

SPARKPOST_API_KEY='yourapikey'
SENDING_EMAIL_ADDRESS='register@yourdomain.com'

Api key is obtained from sparkpost account, you can create one for free as long as you have a domain name that you can control. To handle email we create a file email_service.rs and add the following.

// email_service.rs
use crate::models::Invitation;
use sparkpost::transmission::{
    EmailAddress, Message, Options, Recipient, Transmission, TransmissionResponse,
};

fn get_api_key() -> String {
    std::env::var("SPARKPOST_API_KEY").expect("SPARKPOST_API_KEY must be set")
}

pub fn send_invitation(invitation: &Invitation) {
    let tm = Transmission::new_eu(get_api_key());
    let sending_email = std::env::var("SENDING_EMAIL_ADDRESS")
        .expect("SENDING_EMAIL_ADDRESS must be set");
    // new email message with sender name and email
    let mut email = Message::new(EmailAddress::new(sending_email, "Let's Organise"));

    let options = Options {
        open_tracking: false,
        click_tracking: false,
        transactional: true,
        sandbox: false,
        inline_css: false,
        start_time: None,
    };

    // recipient from the invitation email
    let recipient: Recipient = invitation.email.as_str().into();

    let email_body = format!(
        "Please click on the link below to complete registration. <br/>
         <a href=\"http://localhost:3000/register.html?id={}&email={}\">
         http://localhost:3030/register</a> <br>
         your Invitation expires on <strong>{}</strong>",
        invitation.id,
        invitation.email,
        invitation
            .expires_at
            .format("%I:%M %p %A, %-d %B, %C%y")
            .to_string()
    );

    // complete the email message with details
    email
        .add_recipient(recipient)
        .options(options)
        .subject("You have been invited to join Simple-Auth-Server Rust")
        .html(email_body);

    let result = tm.send(&email);

    // Note that we only print out the error response from email api
    match result {
        Ok(res) => match res {
            TransmissionResponse::ApiResponse(api_res) => {
                println!("API Response: \n {:#?}", api_res);
            }
            TransmissionResponse::ApiError(errors) => {
                println!("Response Errors: \n {:#?}", &errors);
            }
        },
        Err(error) => {
            println!("error \n {:#?}", error);
        }
    }
}

To be able to use this service in our app we add the mod email_service; in our main.js file. Note that we do not return anything from the send_invitation function. It is up to you what you would want to do in a real app, for now we just log to the terminal. Then we can uncomment send_invitation(&invitation); in invitation_routes.rs to actually send emails on user registering.

Please note you could leave sending emails part out and just work with information from the terminal on register.

Get some help

We all need some help at times. We will need to hash the password before we store it in the DB. There was a suggestion on Reddit rust community abut what algorithm to use. argon2 was suggested here. I agree with the suggestion but for the sake of simplicity I have decided to use bcrypt. BTW bcrypt algorithm is a widely used in production, and the bcrypt crate provides a really nice interface to hash and verify passwords.

To keep some concerns separate we create a new file src/utils.rs and define a helper hashing function and JWT encode and decode functions as following.

//utils.rs
use bcrypt::{hash, DEFAULT_COST};
use chrono::{Duration, Local};
use jsonwebtoken::{decode, encode, Header, Validation};

use crate::errors::ServiceError;
use crate::models::SlimUser;

pub fn hash_password(plain: &str) -> Result<String, ServiceError> {
    // get the hashing cost from the env variable or use default
    let hashing_cost: u32 = match std::env::var("HASH_ROUNDS") {
        Ok(cost) => cost.parse().unwrap_or(DEFAULT_COST),
        _ => DEFAULT_COST,
    };
    println!("{}", &hashing_cost);
    hash(plain, hashing_cost).map_err(|_| ServiceError::InternalServerError)
}

// JWT claim
#[derive(Debug, Serialize, Deserialize)]
struct Claim {
    // issuer
    iss: String,
    // subject
    sub: String,
    //issued at
    iat: i64,
    // expiry
    exp: i64,
    // user email
    email: String,
}

// struct to get converted to token and back
impl Claim {
    fn with_email(email: &str) -> Self {
        Claim {
            iss: "localhost".into(),
            sub: "auth".into(),
            email: email.to_owned(),
            iat: Local::now().timestamp(),
            exp: (Local::now() + Duration::hours(24)).timestamp(),
        }
    }
}


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

pub fn create_token(data: &SlimUser) -> Result<String, ServiceError> {
    let claims = Claim::with_email(data.email.as_str());
    encode(&Header::default(), &claims, get_secret().as_ref())
        .map_err(|_err| ServiceError::InternalServerError)
}

pub fn decode_token(token: &str) -> Result<SlimUser, ServiceError> {
    decode::<Claim>(token, get_secret().as_ref(), &Validation::default())
        .map(|data| Ok(data.claims.into()))
        .map_err(|_err| ServiceError::Unauthorized)?
}

// this could be implemented using lazy_static crate
fn get_secret() -> String {
    std::env::var("JWT_SECRET").unwrap_or_else(|_| "my secret".into())
}

You may have noticed that we return a Result and use map_error() to return our custom error. This is to allow using the ? operator later when we call this function (Another way to convert error is to implement From trait for the error returned by bcrypt function, instead).

Code above wont compile as we haven’t implemented SlimUser yet. While we are at it, lets add a convenience method to our User struct defined in models.rs. We will also define another struct SlimUser that does not have the password field. We impl From trait to generate SlimUser from the user. It will all become clear as we get to use this in a while.

// models.rs
use chrono::{NaiveDateTime, Local};
use std::convert::From;
//... snip
#[derive(Debug, Serialize, Deserialize)]
pub struct SlimUser {
    pub email: String,
}

impl From<User> for SlimUser {
    fn from(user: User) -> Self {
        SlimUser { email: user.email }
    }
}

Don’t forget to add mod utils in your main.rs. Make sure your code compiles at this stage. (maybe with some)

Registering User

Let’s create a handler for registering a user. We are going to create a struct RegisterUser with some data that allows us to verify the Invitation and then create and return a user from the database.

Create a new file src/register_handler.rs and add mod register_handler; to you main.rs.

// register_handler.rs
use actix::{Handler, Message};
use chrono::Local;
use diesel::prelude::*;
use uuid::Uuid;

use crate::errors::ServiceError;
use crate::models::{DbExecutor, Invitation, SlimUser, User};
use crate::utils::hash_password;

// UserData is used to extract data from a post request by the client
#[derive(Debug, Deserialize)]
pub struct UserData {
    pub password: String,
}

// to be used to send data via the Actix actor system
#[derive(Debug)]
pub struct RegisterUser {
    pub invitation_id: String,
    pub password: String,
}

impl Message for RegisterUser {
    type Result = Result<SlimUser, ServiceError>;
}

impl Handler<RegisterUser> for DbExecutor {
    type Result = Result<SlimUser, ServiceError>;
    fn handle(&mut self, msg: RegisterUser, _: &mut Self::Context) -> Self::Result {
        use crate::schema::invitations::dsl::{id, invitations};
        use crate::schema::users::dsl::users;
        let conn: &PgConnection = &self.0.get().unwrap();

        // try parsing the string provided by the user as url parameter
        // return early with error that will be converted to ServiceError
        let invitation_id = Uuid::parse_str(&msg.invitation_id)?;

        invitations
            .filter(id.eq(invitation_id))
            .load::<Invitation>(conn)
            .map_err(|_db_error| ServiceError::BadRequest("Invalid Invitation".into()))
            .and_then(|mut result| {
                if let Some(invitation) = result.pop() {
                    // if invitation is not expired
                    if invitation.expires_at > Local::now().naive_local() {
                        // try hashing the password, else return the error that will be converted to ServiceError
                        let password: String = hash_password(&msg.password)?;
                        let user = User::from_details(invitation.email, password);
                        let inserted_user: User =
                            diesel::insert_into(users).values(&user).get_result(conn)?;

                        return Ok(inserted_user.into());
                    }
                }
                Err(ServiceError::BadRequest("Invalid Invitation".into()))
            })
    }
}
Parsing url parameters

actix-web has many easy ways to extract data from a request.

One of the way is to use Path extractor.

Path provides information that can be extracted from the Request’s path. You can deserialize any variable segment from the path.

This will allow us to create a unique path for every invitation to be register as a user.

Let’s modify our register route in the main.rs file, and add a handler function that we will implement later.

// main.rs
//...snip
// routes to register as a user after the
    .service(
        web::resource("/register/{invitation_id}")
            .route(web::post().to_async(register_routes::register_user)),
    ),

You may want to comment the changes out for now as things are not implemented and keep your app compiled and running. (I do that whenever possible, for continuous feedback).

All we need now is to implement that register_user() function that extracts the data from request sent by the client, invoke the handler by sending RegisterUser message to the Actor. Apart from the url parameter we also need to extract password from the client. We have already created a UserData struct in register_handler.rs for that purpose. We will ues the type Json to create UserData struct.

Json allows to deserialize a request body into a struct. To extract typed information from a request’s body, the type T must implement the Deserialize trait from serde.

Create a new file src/register_routes.rs and add mod register_routes; to you main.rs.

// register_routes.rs
use actix::Addr;
use actix_web::{web, Error, HttpResponse, ResponseError};
use futures::Future;

use crate::models::DbExecutor;
use crate::register_handler::{RegisterUser, UserData};

pub fn register_user(
    invitation_id: web::Path<String>,
    user_data: web::Json<UserData>,
    db: web::Data<Addr<DbExecutor>>,
) -> impl Future<Item = HttpResponse, Error = Error> {
    let msg = RegisterUser {
        // into_inner() returns the inner string value from Path
        invitation_id: invitation_id.into_inner(),
        password: user_data.password.clone(),
    };

    db.send(msg)
        .from_err()
        .and_then(|db_response| match db_response {
            Ok(slim_user) => Ok(HttpResponse::Ok().json(slim_user)),
            Err(service_error) => Ok(service_error.error_response()),
        })
}
Test your implementation

After taking care of the errors if you had any, lets give it a spin. invoking

curl --request POST \
  --url http://localhost:3000/api/invitation \
  --header 'content-type: application/json' \
  --data '{"email":"name@domain.com"}'

Should send an email to the adderss and also dbg! log something like

{
  "id": "f87910d7-0e33-4ded-a8d8-2264800d1783",
  "email": "name@domain.com",
  "expires_at": "2018-10-27T13:02:00.909757"
}

If you didn’t implemented the email prat of the tutorial, imagine that we sent an email to the user by creating a link that takes to a form for the user to fill. From there we would have our client post a request to http://localhost:3000/api/register/f87910d7-0e33-4ded-a8d8-2264800d1783. For the sake of this demo you can test your app with the following test command.

curl --request POST \
  --url http://localhost:3000/api/register/f87910d7-0e33-4ded-a8d8-2264800d1783 \
  --header 'content-type: application/json' \
  --data '{"password":"password"}'

Which should return something like

{
  "email": "name@domain.com"
}

Running the command again would result with an error

"Key (email)=(name@domain.com) already exists."

Congratulations now you have a web service that can invite, verify and create a user and even send you a semi useful error message. 🎉🎉

Let’s do Auth

According to w3.org:

The general concept behind a token-based authentication system is simple. Allow users to enter their username and password in order to obtain a token which allows them to fetch a specific resource - without using their username and password. Once their token has been obtained, the user can offer the token - which offers access to a specific resource for a time period - to the remote site.

Now how you choose to exchange that token can have security implications. You will find many discussions/debates around the internet and many ways that people use. I am very wary of storing things on client side that can be accessed by the client side JavaScript. Unfortunately this approach is suggested in thousands of tutorial everywhere. Here is a good read Stop using JWT for sessions.

I am not sure here, what to suggest you as the reader, apart from don't follow online tutorials blindly and do your own research. The purpose of this tutorial is to learn about Actix-web and rust not how to prevent your server from vulnerabilities. For the sake of this tutorial we will be using http only cookies to exchange tokens.

PLEASE DO NOT USE IN PRODUCTION.

Now that is out of the way 😰, let’s see what we can do here. actix-web provides us with a neat way as middleware of handling a session cookie documented here actix_web::middleware::identity. To enable this functionality we modify our main.rs file as following.

use actix_web::middleware::{
    identity::{CookieIdentityPolicy, IdentityService},Logger
    };
use chrono::Duration;
//--snip
.wrap(Logger::default())
.wrap(IdentityService::new(
    CookieIdentityPolicy::new(secret.as_bytes())
        .name("auth")
        .path("/")
        .domain(domain.as_str())
        .max_age_time(Duration::days(1))
        .secure(false), // this can only be true if you have https
))
// everything under '/api/' route
 .service(
         web::scope("/api")
        //--snip
}

This gives us very convenient methods like req.remember(data), req.identity() and req.forget() etc on HttpRequest prams in our route functions. That in turn will set and remove the auth cookie from client.

JWT

While writing this tutorial I ran into a few discussions about What JWT lib to use. From a simple search I found a few, and decided to go with frank_jwt but then Vincent Prouillet Pointed out the incompleteness and suggested to go with jsonwebtoken. After having trouble with using the lib I got a great response from them. Now the repo has working examples and I was able to implement the following default solution. Please note this is not the most secure implementation of JWT you might want to look up resources for making it better suit your needs.

Auth Handling

You know the drill now 😉, lets create a new file src/auth_handler.rs and add mod auth_handler; to you main.rs.

//auth_handler.rs
use actix::{Handler, Message};
use actix_web::{dev::Payload, Error, HttpRequest};
use actix_web::{middleware::identity::Identity, FromRequest};
use bcrypt::verify;
use diesel::prelude::*;

use crate::errors::ServiceError;
use crate::models::{DbExecutor, SlimUser, User};
use crate::utils::decode_token;

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

impl Message for AuthData {
    type Result = Result<SlimUser, ServiceError>;
}

impl Handler<AuthData> for DbExecutor {
    type Result = Result<SlimUser, ServiceError>;
    fn handle(&mut self, msg: AuthData, _: &mut Self::Context) -> Self::Result {
        use crate::schema::users::dsl::{email, users};
        let conn: &PgConnection = &self.0.get().unwrap();

        let mut items = users.filter(email.eq(&msg.email)).load::<User>(conn)?;

        if let Some(user) = items.pop() {
            if let Ok(matching) = verify(&msg.password, &user.password) {
                if matching {
                    return Ok(user.into());
                }
            }
        }
        Err(ServiceError::BadRequest(
            "Username and Password don't match".into(),
        ))
    }
}

// if we need the same data
// simple aliasing makes the intentions clear and its more readable
pub type LoggedUser = SlimUser;

// extract user from the request cookie
impl FromRequest for LoggedUser {
    type Error = Error;
    type Future = Result<LoggedUser, Error>;
    type Config = ();

    fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
        if let Some(identity) = Identity::from_request(req, pl)?.identity() {
            let user: SlimUser = decode_token(&identity)?;
            return Ok(user as LoggedUser);
        }
        Err(ServiceError::Unauthorized.into())
    }
}

Handler above takes the AuthData struct that contains email and password sent by the client. We use the email to extract user from the db and use bcrypt verify function to match the password. If all goes well we return the user otherwise return BadRequest error.

The whole point of having Auth is to have way to verify a request is coming from a authenticated client. Actix-web has a trait FromRequest that we can implement on any type and then use that to extract data from the request. See documentation here.

Now let’s also create src/auth_routes.rs with the following content:

// auth_routes.rs
use actix::Addr;
use actix_web::middleware::identity::Identity;
use actix_web::{web, Error, HttpResponse, Responder, ResponseError};
use futures::Future;

use crate::auth_handler::{AuthData, LoggedUser};
use crate::models::DbExecutor;
use crate::utils::create_token;

pub fn login(
    auth_data: web::Json<AuthData>,
    id: Identity,
    db: web::Data<Addr<DbExecutor>>,
) -> impl Future<Item = HttpResponse, Error = Error> {
    db.send(auth_data.into_inner())
        .from_err()
        .and_then(move |res| match res {
            Ok(user) => {
                let token = create_token(&user)?;
                id.remember(token);
                Ok(HttpResponse::Ok().into())
            }
            Err(err) => Ok(err.error_response()),
        })
}

pub fn logout(id: Identity) -> impl Responder {
    id.forget();
    HttpResponse::Ok()
}

pub fn get_me(logged_user: LoggedUser) -> HttpResponse {
    HttpResponse::Ok().json(logged_user)
}

Our login method extracts the AuthData from request and sends a message to DbEexcutor Actor handler which we implemented in auth_handler.rs. Here if all is good we get a user returned to us, We use our helper function that we defined earlier in utils.rs to create a token and call req.remember(token). This in turn sets a cookie header with token for the client to save.

Last thing we need to do now is use login/logout function in our main.rs. Change the web::rsource("/auth") closure to following:

.// routes for authentication
.service(
    web::resource("/auth")
        .route(web::post().to_async(auth_routes::login))
        .route(web::delete().to(auth_routes::logout))
        .route(web::get().to_async(auth_routes::get_me)),
)
Test run Auth

If you have been following the tutorial, you have already created a user with email and password. Use the following curl command to test our server.

curl -i --request POST \
  --url http://localhost:3000/api/auth \
  --header 'content-type: application/json' \
  --data '{"email": "name@domain.com","password":"password"}'

## response
HTTP/1.1 200 OK
set-cookie: auth=iqsB4KUUjXUjnNRl1dVx9lKiRfH24itiNdJjTAJsU4CcaetPpaSWfrNq6IIoVR5+qKPEVTrUeg==; HttpOnly; Path=/; Domain=localhost; Max-Age=86400
content-length: 0
date: Sun, 28 Oct 2018 12:36:43 GMT

If you received a 200 response like above with a set-cookie header, Congratulations you have successfully logged in.

To test the logout we send a DELETE request to the /auth, make sure you get set-cookie header with empty data and immediate expiry date.

curl -i --request DELETE \
  --url http://localhost:3000/auth

## response
HTTP/1.1 200 OK
set-cookie: auth=; HttpOnly; Path=/; Domain=localhost; Max-Age=0; Expires=Fri, 27 Oct 2017 13:01:52 GMT
content-length: 0
date: Sat, 27 Oct 2018 13:01:52 GMT
Testing logged in user

Try the following Curl command in the terminal.

curl -i --request POST \
  --url http://localhost:3000/auth \
  --header 'content-type: application/json' \
  --data '{
        "email": "name@domain.com",
        "password":"password"
}'
# result would be something like
HTTP/1.1 200 OK
set-cookie: auth=HdS0iPKTBL/4MpTmoUKQ5H7wft5kP7OjP6vbyd05Ex5flLvAkKd+P2GchG1jpvV6p9GQtzPEcg==; HttpOnly; Path=/; Domain=localhost; Max-Age=86400
content-length: 0
date: Sun, 28 Oct 2018 19:16:12 GMT

## and then pass the cookie back for a get request
curl -i --request GET \
  --url http://localhost:3000/auth \
  --cookie auth=HdS0iPKTBL/4MpTmoUKQ5H7wft5kP7OjP6vbyd05Ex5flLvAkKd+P2GchG1jpvV6p9GQtzPEcg==
## result
HTTP/1.1 200 OK
content-length: 27
content-type: application/json
date: Sun, 28 Oct 2018 19:21:04 GMT

{"email":"name@domain.com"}

It should successfully return your email as json back. Only logged in users or requests with a valid auth cookie and token will pass through the routes you extract the LoggedUser.

What’s next

Please look at the repo for full code and frontend for this app. Thank you for reading this tutorial.

Get in touch with me on twitter if you have a question or suggestion about any of it.

Happy Coding!