Auth Web Microservice with rust using Actix-Web - Complete Tutorial Part 2
Harry Gill
Posted on October 30, 2018
Originally posted on gill.net.in
This post is outdated now please read the updated version Auth Web Microservice with rust using Actix-Web 1.0 - Complete Tutorial
Welcome back to part 2 of the tutorial
In part one we have successfully setup bulk base of our Auth microservice. Congratulations if you have your app running and no errors so far. Checkout Part One if you haven't already. Complete code for this tutorial is on Gitlab master branch.
Verify User Email or not?
Picking up from part one, we now have as server that takes an email address from a request and spits out a JSON response with an invitation object. In part one I said that we will send an email to the user, after some thought and feedback, we will be skipping this part now (look out for part 3). The service that I use is sparkpost and you as the reader of this tutorial may not have an account with them(which is free for small usage).
WARNING: Do not use this workaround in any real app without proper email verification.
Workaround
For now we will use the http response from the server to verify the email so to speak. The simplest way of creating an email verification is to have our server to use some sort of secret sent via the email to the user email and have them click an link with the secret to verify. We could use the UUID
from the invitation object as a secret. Let's say the client gets an invitation after entering their email with uuid 67a68837-a059-43e6-a0b8-6e57e6260f0d
.
We can send a request to register a new user with the above UUID
in the url. Our server can take that id and find the Invitation object in the database and then compare the expiry date with current time. If all of these conditions are true we would let the user register, otherwise send an error response back. For now we will return the Invitation Object to the client as a workaround. Email support will get implemented next in Part 3.
Error handling and the From
trait
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. the file look like the following:
// errors.rs
use actix_web::{error::ResponseError, HttpResponse};
use std::convert::From;
use diesel::result::{DatabaseErrorKind, Error};
use uuid::ParseError;
#[derive(Fail, Debug)]
pub enum ServiceError {
#[fail(display = "Internal Server Error")]
InternalServerError,
#[fail(display = "BadRequest: {}", _0)]
BadRequest(String),
#[fail(display = "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<Error> for ServiceError {
fn from(error: Error) -> 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 {
Error::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
}
}
}
Great! this will all become handy as we go.
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 as following.
//utils.rs
use bcrypt::{hash, DEFAULT_COST};
use errors::ServiceError;
use std::env;
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 env::var("HASH_ROUNDS") {
Ok(cost) => cost.parse().unwrap_or(DEFAULT_COST),
_ => DEFAULT_COST,
};
hash(plain, hashing_cost).map_err(|_| ServiceError::InternalServerError)
}
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).
While we are at it, lets add a convenience method to our User
struct defined in models.rs
in the last tutorial. We also removing the remove_pwd() method, instead we will 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.
use chrono::{NaiveDateTime, Local};
use std::convert::From;
//... snip
impl User {
pub fn with_details(email: String, password: String) -> Self {
User {
email,
password,
created_at: Local::now().naive_local(),
}
}
}
//--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 extern crate bcrypt;
and mod utils
in your main.rs
.
Another neat thing I forgot in the part one was logging to console. To enable it add the following to the main.rs
extern crate env_logger;
// --snip
fn main(){
dotenv().ok();
std::env::set_var("RUST_LOG", "simple-auth-server=debug,actix_web=info");
env_logger::init();
//--snip
}
Registering User
If you remember from the previous tutorial, we created a handler for Invitation
now 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 errors::ServiceError;
use models::{DbExecutor, Invitation, User, SlimUser};
use uuid::Uuid;
use 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 schema::invitations::dsl::{invitations, id};
use 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::with_details(invitation.email, password);
let inserted_user: User = diesel::insert_into(users)
.values(&user)
.get_result(conn)?;
return Ok(inserted_user.into()); // convert User to SlimUser
}
}
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 app.rs
file, and add a handler function that we will implement later.
// app.rs
/// creates and returns the app after mounting all routes/resources
// add use statement at the top.
use register_routes::register_user;
//...snip
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
App::with_state(AppState { db })
//... snip
// routes to register as a user
.resource("/register/{invitation_id}", |r| {
r.method(Method::POST).with(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
.
use actix_web::{AsyncResponder, FutureResponse, HttpResponse, ResponseError, State, Json, Path};
use futures::future::Future;
use app::AppState;
use register_handler::{RegisterUser, UserData};
pub fn register_user((invitation_id, user_data, state): (Path<String>, Json<UserData>, State<AppState>))
-> FutureResponse<HttpResponse> {
let msg = RegisterUser {
// into_inner() returns the inner string value from Path
invitation_id: invitation_id.into_inner(),
password: user_data.password.clone(),
};
state.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()),
}).responder()
}
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/invitation \
--header 'content-type: application/json' \
--data '{
"email":"name@domain.com"
}'
Should return something like
{
"id": "f87910d7-0e33-4ded-a8d8-2264800d1783",
"email": "name@domain.com",
"expires_at": "2018-10-27T13:02:00.909757"
}
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/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/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 app.rs
file as following.
use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
use chrono::Duration;
//--snip
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
// secret is a random 32 character long base 64 string
let secret: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| "0".repeat(32));
let domain: String = std::env::var("DOMAIN").unwrap_or_else(|_| "localhost".to_string());
App::with_state(AppState { db })
.middleware(Logger::default())
.middleware(IdentityService::new(
CookieIdentityPolicy::new(secret.as_bytes())
.name("auth")
.path("/")
.domain(domain.as_str())
.max_age(Duration::days(1)) // just for testing
.secure(false),
))
//--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.
Before we create auth handler and route function, let's add some helper functions to our util.rs
for jwt encoding and decoding. Don't forget to add extern crate jsonwebtoken as jwt;
in your main.rs
.
I Will gladly accept a PR if someone has a better implementation.
// utils.rs
use models::SlimUser;
use std::convert::From;
use jwt::{decode, encode, Header, Validation};
use chrono::{Local, Duration};
//--snip
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
// 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 Claims {
fn with_email(email: &str) -> Self {
Claims {
iss: "localhost".into(),
sub: "auth".into(),
email: email.to_owned(),
iat: Local::now().timestamp(),
exp: (Local::now() + Duration::hours(24)).timestamp(),
}
}
}
impl From<Claims> for SlimUser {
fn from(claims: Claims) -> Self {
SlimUser { email: claims.email }
}
}
pub fn create_token(data: &SlimUser) -> Result<String, ServiceError> {
let claims = Claims::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::<Claims>(token, get_secret().as_ref(), &Validation::default())
.map(|data| Ok(data.claims.into()))
.map_err(|_err| ServiceError::Unauthorized)?
}
// take a string from env variable
fn get_secret() -> String {
env::var("JWT_SECRET").unwrap_or("my secret".into())
}
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 diesel::prelude::*;
use errors::ServiceError;
use models::{DbExecutor, User, SlimUser};
use bcrypt::verify;
use actix_web::{FromRequest, HttpRequest, middleware::identity::RequestIdentity};
#[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 schema::users::dsl::{users, email};
let conn: &PgConnection = &self.0.get().unwrap();
let mismatch_error = Err(ServiceError::BadRequest("Username and Password don't match".into()));
let mut items = users
.filter(email.eq(&msg.email))
.load::<User>(conn)?;
if let Some(user) = items.pop() {
match verify(&msg.password, &user.password) {
Ok(matching) => {
if matching { return Ok(user.into()); } else { return mismatch_error; }
}
Err(_) => { return mismatch_error; }
}
}
mismatch_error
}
}
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 or we return BadRequest
error.
Now let's also create src/auth_routes.rs
with the following content:
// auth_routes.rs
use actix_web::{AsyncResponder, FutureResponse, HttpResponse, HttpRequest, ResponseError, Json};
use actix_web::middleware::identity::RequestIdentity;
use futures::future::Future;
use utils::create_token;
use app::AppState;
use auth_handler::AuthData;
pub fn login((auth_data, req): (Json<AuthData>, HttpRequest<AppState>))
-> FutureResponse<HttpResponse> {
req.state()
.db
.send(auth_data.into_inner())
.from_err()
.and_then(move |res| match res {
Ok(slim_user) => {
let token = create_token(&slim_user)?;
req.remember(token);
Ok(HttpResponse::Ok().into())
}
Err(err) => Ok(err.error_response()),
}).responder()
}
pub fn logout(req: HttpRequest<AppState>) -> HttpResponse {
req.forget();
HttpResponse::Ok().into()
}
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 app.rs
. Change the .rsource("/auth")
closure to following:
.resource("/auth", |r| {
r.method(Method::POST).with(login);
r.method(Method::DELETE).with(logout);
})
Don't forget to add use auth_routes::{login, logout};
at the top of the file though.
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/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
Implementing a protected route
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. We will add the following at the bottom of auth_handler.rs
.
//auth_handler.rs
//--snip
use actix_web::FromRequest;
use utils::decode_token;
//--snip
// we need the same data as SlimUser
// simple aliasing makes the intentions clear and its more readable
pub type LoggedUser = SlimUser;
impl<S> FromRequest<S> for LoggedUser {
type Config = ();
type Result = Result<LoggedUser, ServiceError>;
fn from_request(req: &HttpRequest<S>, _: &Self::Config) -> Self::Result {
if let Some(identity) = req.identity() {
let user: SlimUser = decode_token(&identity)?;
return Ok(user as LoggedUser);
}
Err(ServiceError::Unauthorized)
}
}
We chose to use a type alias rather that creating a whole new type. When we extract LoggedUser
from a request reader would know that its is the authenticated user. FromRequest trait simply tries to Deserialize the string from the cookie into our struct and if that fails it simply returns Unauthorized error back. To test this we need to add an actual route to or app. We simply add another function to auth_routes.rs
//auth_routes.rs
//--snip
pub fn get_me(logged_user: LoggedUser) -> HttpResponse {
HttpResponse::Ok().json(logged_user)
}
To call this we register this method in app.rs
resources. It would look like following.
//app.rs
use auth_routes::{login, logout, get_me};
//--snip
.resource("/auth", |r| {
r.method(Method::POST).with(login);
r.method(Method::DELETE).with(logout);
r.method(Method::GET).with(get_me);
})
//--snip
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?
In Part 3 of this tutorial we will create the email verification and a frontend for this app. I wold like to use some sort of rust html templating system for that. At the same time I'm learning Angular so I might slap a small angular app at the front of it.
Get in touch with me on twitter if you have a question or suggestion about any of it.
Posted on October 30, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.