Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 1

October 18, 2018

Harry Gill
Tags: tutorial rust actix-web jwt auth | 9min 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.
  • 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.
  • env_logger // A logging implementation for log which is configured via an environment variable.
  • failure // Experimental error handling abstraction.
  • 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.29.1 (b801ae664 2018-09-20)
# cargo 1.29.0 (524a578d7 2018-08-05)

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

cd simple-auth-server # and then 

# 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.7.4"
actix-web = "0.7.8"
bcrypt = "0.2.0"
chrono = { version = "0.4.6", features = ["serde"] }
diesel = { version = "1.3.3", features = ["postgres", "uuid", "r2d2", "chrono"] }
dotenv = "0.13.0"
env_logger = "0.5.13"
failure = "0.1.2"
frank_jwt = "3.0"
futures = "0.1"
r2d2 = "0.8.2"
serde_derive="1.0.79"
serde_json="1.0"
serde="1.0"
sparkpost = "0.4"
uuid = { version = "0.6.5", features = ["serde", "v4"] }
Setup The Base APP

Create new files src/models.rs src/app.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/app.rs. We are leaving the resource builders empty for now. This is where the meat of the routing is going to go.

// app.rs
use actix::prelude::*;
use actix_web::{http::Method, middleware, App};
use models::DbExecutor;

pub struct AppState {
    pub db: Addr<DbExecutor>,
}

// helper function to create and returns the app after mounting all routes/resources
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
    App::with_state(AppState { db })
        // setup builtin logger to get nice logging for each request
        .middleware(middleware::Logger::new("\"%r\" %s %b %Dms"))

         // routes for authentication
        .resource("/auth", |r| {
        })
        // routes to invitation
        .resource("/invitation/", |r| {
        })
        // routes to register as a user after the
        .resource("/register/", |r| {
        })
}
// main.rs
// to avoid the warning from diesel macros
#![allow(proc_macro_derive_resolution_fallback)]

extern crate actix;
extern crate actix_web;
extern crate serde;
extern crate chrono;
extern crate dotenv;
extern crate futures;
extern crate r2d2;
extern crate uuid;
#[macro_use] extern crate diesel;
#[macro_use] extern crate serde_derive;
#[macro_use] extern crate failure;

mod app;
mod models;
mod schema;
// mod errors;
// mod invitation_handler;
// mod invitation_routes;

use models::DbExecutor;
use actix::prelude::*;
use actix_web::server;
use diesel::{r2d2::ConnectionManager, PgConnection};
use dotenv::dotenv;
use std::env;


fn main() {
    dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let sys = actix::System::new("Actix_Tutorial");

    // 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()));

    server::new(move || app::create_app(address.clone()))
        .bind("127.0.0.1:3000")
        .expect("Can not bind to '127.0.0.1:3000'")
        .start();

    sys.run();
}

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.

Setting up Diesel and creating our user Model

We start with creating a model for the user. 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. Then 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 {
    // this is just a helper function to remove password from user just before we return the value out later
    pub fn remove_pwd(mut self) -> Self {
        self.password = "".to_string();
        self
    }
}

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

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.

Actix-web provides automatic compatibility with the failure library so that errors deriving fail will be converted automatically to an actix error. Keep in mind that those errors will render with the default 500 status code unless you also provide your own error_response() implementation for them.

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

// errors.rs
use actix_web::{error::ResponseError, HttpResponse};


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

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

// 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")
            },
            ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
        }
    }
}

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::result::{DatabaseErrorKind, Error::DatabaseError};
use diesel::{self, prelude::*};
use errors::ServiceError;
use models::{DbExecutor, Invitation};
use uuid::Uuid;

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

// impl Message trait allows us to make use if the Actix message system and
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 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
        // this could be any duration from current time we will use it later to see if the invitation is still valid
        let new_invitation = Invitation {
            id: Uuid::new_v4(),
            email: msg.email.clone(),
            expires_at: Local::now().naive_local() + Duration::hours(24),
        };

        diesel::insert_into(invitations)
            .values(&new_invitation)
            .execute(conn)
            .map_err(|error| {
                println!("{:#?}",error); // for debugging purposes
                ServiceError::InternalServerError
            })?;

        let mut items = invitations
            .filter(email.eq(&new_invitation.email))
            .load::<Invitation>(conn)
            .map_err(|_| ServiceError::InternalServerError)?;

        Ok(items.pop().unwrap())
    }
}

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_web::{AsyncResponder, FutureResponse, HttpResponse, Json, ResponseError, State};
use futures::future::Future;

use app::AppState;
use invitation_handler::CreateInvitation;

pub fn register_email((signup_invitation, state): (Json<CreateInvitation>, State<AppState>))
    -> FutureResponse<HttpResponse> {
    state
        .db
        .send(signup_invitation.into_inner())
        .from_err()
        .and_then(|db_response| match db_response {
            Ok(invitation) => Ok(HttpResponse::Ok().json(invitation)),
            Err(err) => Ok(err.error_response()),
        }).responder()
}
Test your server

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/invitation \
  --header 'content-type: application/json' \
  --data '{"email":"test@test.com"}'
# result would look something like
{
    "id": "67a68837-a059-43e6-a0b8-6e57e6260f0d",
    "email": "test@test.com",
    "expires_at": "2018-10-23T09:49:12.167510"
}
End Part 1

In the next part we will be expanding our app to genarate an email and send it to registering user for verification. We will also allow users to register and authenticate after the verification.

UPDATE: Part 2 published 29-Oct-2018;

Please get in touch if you have any questions or suggestions, my twitter handle is @mygnu_.