Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 1
Harry Gill
Posted on October 22, 2018
This post is outdated now please read the updated version Auth Web Microservice with rust using Actix-Web 1.0 - Complete Tutorial
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.
read more...
Posted on October 22, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.