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

November 26, 2018

Harry Gill
Tags: tutorial rust actix-web jwt auth | 4min Read
Welcome back to part 3 of the tutorial

Checkout Part One and Part Two if you haven’t already. Complete code for this project is on Gitlab master branch.

Updating our cargo.toml?

Rust ecosystem is moving fast, in a matter of few week we have many of our crates updated upstream including my sparkpost crate(more on that later). Without any delay let’s just update the follwing crates in our cargo.toml file.

[dependencies]
actix = "0.7.7"
actix-web = "0.7.14"
env_logger = "0.6.0"
r2d2 = "0.8.3"
sparkpost = "0.5.2"
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.

use 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")
}

#[allow(unused)]
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"));

    // set options for a transactional email for now
    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);

    match result {
        Ok(res) => {
            // println!("{:?}", &res);
            match res {
                TransmissionResponse::ApiResponse(api_res) => {
                    println!("API Response: \n {:#?}", api_res);
                    //   assert_eq!(1, api_res.total_accepted_recipients);
                    //   assert_eq!(0, api_res.total_rejected_recipients);
                }
                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 extern crate sparkpost; and 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.

Adjust your invitation handling

In the previous tutorial we implemented our Invitation in a way that it returned the object itself. Let’s change that and send the email to the user instead. In our invitation_routes.rs we call teh send_invitation function with invitation. Code looks like following.

use actix_web::{AsyncResponder, FutureResponse, HttpResponse, Json, ResponseError, State};
use futures::future::Future;

use app::AppState;
use email_service::send_invitation;
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) => {
             send_invitation(&invitation);
             Ok(HttpResponse::Ok().into())
         }
         Err(err) => Ok(err.error_response()),
     }).responder()
}
Setup a mock frontend for testing

I’m not going to go in the details for frontend setup. For the purpose of this tutorial I have created some html/css/js files to be served as static files here repo for convenience. You can simply copy this folder to the root dir of your code as ‘static/’ or go wild and make a react/vue/angular frontend.

We are going to change our routes a bit to fit our needs and separate the static file routes form our business logic ones. I have decided to go with static files served at the root level and move all app routs to /api prefix. To achieve this we modify our app.rs file to the following.

use actix::prelude::*;
use actix_web::middleware::identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{fs, http::Method, middleware::Logger, App};
use auth_routes::{get_me, login, logout};
use chrono::Duration;
use invitation_routes::register_email;
use models::DbExecutor;
use register_routes::register_user;

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

/// creates and returns the app after mounting all routes/resources
pub fn create_app(db: Addr<DbExecutor>) -> App<AppState> {
    // secret is a random minimum 32 bytes long base 64 string
    let secret: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| "0123".repeat(8));
    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))
                .secure(false), // this can only be true if you have https
        ))
        // everything under '/api/' route
        .scope("/api", |api| {
            // routes for authentication
            api.resource("/auth", |r| {
                r.method(Method::POST).with(login);
                r.method(Method::DELETE).with(logout);
                r.method(Method::GET).with(get_me);
            })
            // routes to invitation
            .resource("/invitation", |r| {
                r.method(Method::POST).with(register_email);
            })
            // routes to register as a user after the
            .resource("/register/{invitation_id}", |r| {
                r.method(Method::POST).with(register_user);
            })
        })
        // serve static files
        .handler(
            "/",
            fs::StaticFiles::new("./static/")
                .unwrap()
                .index_file("index.html"),
        )
}

Notice the scope method enclosing all routes. That’s all you need to setup email verification and a simple frontend.

What’s next?

As you can see I have not implemented the login front end yet. This is deliberate, as front end is out of the scope of this tutorial, and I would be nice for you as a learner to try and implement on your own.

Thank you for reading this tutorial and happy coding.

Get in touch with me on twitter if you have a question or suggestion, I’d be happy to help.