This is the final part of our exploratory series on creating a web service with Rust. Our exploration is broken down into distinct steps to make it easier for you to follow along:

Creating a password via API

We need a new error type for cases where resources are not found.

// src/errors.rs
//...

#[derive(Clone, Debug, Display)]
pub enum AuthError {
   // ...

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


impl ResponseError for AuthError {
    fn error_response(&self) -> HttpResponse {
        match self { 
            // ...

            AuthError::NotFound(ref message) => HttpResponse::NotFound().json(message),
        }
    }
}
// ...

In main.rs, let's add a handler that would take a confirmation id and password and create a user account.

// src/main.rs
// ...
mod password_handler;


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    // ...

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            // Routes
            .service(
                web::scope("/")
                    // ...
                    .service(
                        web::resource("/register/{path_id}")
                            .route(web::post().to(password_handler::create_account)),
                    ),
            )
    })
    // ...
}

We then create src/password_handler.rs which will contain our handler:

// src/password_handler.rs
use actix_web::{error::BlockingError, web, HttpResponse};
use actix_session::Session;
use diesel::prelude::*;
use serde::Deserialize;

use crate::{
    models::{Confirmation, Pool, SessionUser, User}, 
    errors::AuthError, 
    schema::{
      confirmations::dsl::{id, confirmations},
      users::dsl::users
    },
    utils::{hash_password, is_signed_in, set_current_user}
};


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

pub async fn create_account(session: Session,
                          path_id: web::Path<String>,
                          data: web::Json<PasswordData>,
                          pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    if is_signed_in(&session) {
        return Ok(HttpResponse::BadRequest().finish());
    }

    let result = web::block(move || create_user(&path_id.into_inner(), &data.into_inner().password, &pool)).await;

    match result {
        Ok(user) => {
            set_current_user(&session, &user);

            Ok(HttpResponse::Created().json(&user))
        },
        Err(err) => match err {
            BlockingError::Error(auth_error) => Err(auth_error),
            BlockingError::Canceled => Err(AuthError::GenericError(String::from("Could not complete the process"))),
        },
    }
}


fn create_user(path_id: &str, password: &str, pool: &web::Data<Pool>) -> Result<SessionUser, AuthError> {
    let path_uuid = uuid::Uuid::parse_str(path_id)?;
    let conn = &pool.get().unwrap();

    confirmations
        .filter(id.eq(path_uuid))
        .load::<Confirmation>(conn)
        .map_err(|_db_error| AuthError::NotFound(String::from("Confirmation not found")))
        .and_then(|mut result| {
            if let Some(confirmation) = result.pop() {
                if confirmation.expires_at > chrono::Local::now().naive_local() { // confirmation has not expired
                    let password: String = hash_password(password)?;
                    let user: User = diesel::insert_into(users)
                                            .values(&User::from(confirmation.email, password))
                                            .get_result(conn)?;

                    return Ok(user.into());
                }
            }

            Err(AuthError::AuthenticationError(String::from("Invalid confirmation")))
        })
}

In create_account(), we terminate the request if the user is signed in, create a user account, sign the user in if the account creation was successful and return a response.

The create_user() support function parses the id as a UUID, queries the confirmation record and if found, creates and returns a user record.


Before we move on, we have some housecleaning to do.

In errors.rs, change plain_text and html_text to:

let html_text = format!(
      "Please click on the link below to complete registration. <br/>
       <a href=\"{domain}/register/{id}\">Complete registration</a> <br/>
      This link expires on <strong>{expires}</strong>",
      domain=domain_url,
      id=confirmation.id,
      expires=expires
  );
  let plain_text = format!(
      "Please visit the link below to complete registration:\n
      {domain}/register/{id}\n
      This link expires on {expires}.",
      domain=domain_url,
      id=confirmation.id,
      expires=expires
  );

In main.rs, delete .allowed_origin("*") and add OPTIONS to the list of allowed headers in the CORs configuration.


Allow password creation from a browser

Create a new template templates/pages/password.hbs:


{{#> layouts/base title = "Auth Service | Set password" }}

  {{#if error.is_some() }}
  {{> includes/message success = false, message = error.as_ref().unwrap() }}
  {{/if}}

  <div>
    <h2 class="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900">
      Complete account creation
    </h2>
  </div>
  
  <form class="mt-8" action="/register2/{{ path_id }}" method="POST">
    <div class="rounded-md shadow-sm">
      <div>
        <input 
          aria-label="Email address" 
          type="email" 
          disabled
          readonly
          value="{{ email }}" 
          class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5" 
        />
      </div>

      <div class="-mt-px">
        <input aria-label="Password" name="password" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5" placeholder="Password" />
      </div> 
    </div>

    <div class="mt-6">
      <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
        <span class="absolute left-0 inset-y-0 flex items-center pl-3">
          <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400 transition ease-in-out duration-150" fill="currentColor" viewBox="0 0 20 20">
            <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
          </svg>
        </span>
        Create password
      </button>
    </div>
  </form>
  
{{~/layouts/base }}

We then add a struct for the above template in src/templates.rs:

// src/templates.rs
// ...

#[derive(Template)]
#[template(path = "pages/password.hbs")]
pub struct Password {
    pub email: String,
    pub path_id: String,
    pub error: Option<String>
}

In main.rs, we update our route specification to include routes for setting password through a browser:

// ...
web::scope("/")
                    .service(
                        web::resource("/register")
                            .route(web::get().to(register_handler::show_confirmation_form))
                            .route(web::post().to(register_handler::send_confirmation)),
                    )
                    .service(
                        web::resource("/register/{path_id}")
                            .route(web::get().to(password_handler::show_password_form))
                            .route(web::post().to(password_handler::create_account)),
                    )
                    .route("/register2/{path_id}", web::post().to(password_handler::create_account_for_browser))
                    .route("/register2", web::post().to(register_handler::send_confirmation_for_browser)),

In src/password_handler.rs, we add the corresponding route handlers:

// src/password_handler.rs
use actix_web::{/*...,*/ http::header::LOCATION};
use uuid::Uuid;
use yarte::Template;
use crate::{
    // ...
    templates::Password,
    utils::{/*...,*/ to_home}
};
// ...
pub async fn show_password_form(session: Session, 
                                path_id: web::Path<String>,
                                pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    if is_signed_in(&session) {
        Ok(to_home())
    } else {
        let id_str = path_id.into_inner();

        match get_invitation(&id_str, &pool) {
            Ok(Confirmation { email, .. }) => {
                let t = Password { path_id: id_str, email, error: None };

                Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(t.call().unwrap()))
            },
            Err(_) => Ok(HttpResponse::MovedPermanently().header(LOCATION, "/register").finish()),
        }
    }
}

pub async fn create_account_for_browser(path_id: web::Path<String>,
                                        data: web::Form<PasswordData>,
                                        session: Session,
                                        pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    let id_str = path_id.into_inner();
    let id_str2 = String::from(id_str.as_str());
    let result = web::block(move || create_user(&id_str, &data.into_inner().password, &pool)).await;

    match result {
        Ok(user) => {
            set_current_user(&session, &user);

            Ok(to_home())
        },
        Err(_) => {
            let t = Password { 
                path_id: id_str2, 
                email: String::from("unknown@email.com"), 
                error: Some(String::from("Invalid/expired confirmation id"))
            };

            Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(t.call().unwrap()))
        },
    }
}


fn get_invitation(path_id: &str, pool: &web::Data<Pool>) -> Result<Confirmation, AuthError> {
    let path_uuid = Uuid::parse_str(path_id)?;

    if let Ok(record) = confirmations.find(path_uuid).get_result::<Confirmation>(&pool.get().unwrap()) {
        Ok(record)
    } else {
        Err(AuthError::AuthenticationError(String::from("Invalid confirmation")))
    }
}
//...

In show_password_form() we redirect the user to their home page if they're signed in. We then try to find the supplied confirmation id; if found we render a password, else we  redirect the user to register page.

In create_account_for_browser() we create the user account. If we succeed, we set the current user in the session and redirect to their home page. If anything goes wrong, we re-render the password form.


For some reason, CookieSession cannot set cookies on my test browsers (Chrome and Firefox), though curl works ok. I noticed that RedisSession does not have that problem, so we're switching.

Add actix-redis = { version = "0.8.0", features = ["web"] } to your dependencies in Cargo.toml.

Modify the cookie setup in main.rs:

// src/main.rs
// ...


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    //...
    use actix_redis::RedisSession;

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            // Enable sessions
            .wrap(RedisSession::new("127.0.0.1:6379", &[0; 32]))
            // ...
    })
    // ...
}

With that, our sessions should work without hiccups.


Showing the current user

After successfully signing in from a browser, you notice that it redirects to a non-existent home page. Let's fix that.

We'll be using the same route handler for API and browser requests. In main.rs, we register our new modules as well as the route handler that will return the currently signed in user or render the user's home page depending on the nature of the request.

// src/main.rs
// ...

mod auth_handler;
// ...


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    // ...

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            // Routes
            .service(
                web::scope("/")
                    // ...
                    .route("/me", web::get().to(auth_handler::me)),
    })
    .bind(format!("{}:{}", vars::domain(), vars::port()))?
    .run()
    .await
}

Next, we'll create the template that will be rendered to the browser. Modify src/templates.rs:

// src/templates.rs
// ...

use crate::models::SessionUser;
// ...

#[derive(Template)]
#[template(path = "pages/me.hbs")]
pub struct Me {
    pub user: SessionUser,
}

Create the template file that will be rendered, templates/pages/me.hbs:

<!-- templates/pages/me.hbs -->
{{#> layouts/base title = "Auth Service | User page" }}
  <div>
    <h2 class="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900">
      Your email: {{ user.email }}
    </h2>
  </div>
  <p class="mt-6 text-center leading-9">
    <a class="underline" href="/signout">Sign out →</a>
  </p>

{{~/layouts/base }}

Create src/auth_handler.rs if you haven't already and add the route handler:

// src/auth_handler.rs
use actix_session::Session;
use actix_web::{
    http::header::LOCATION,
    HttpRequest,
    HttpResponse,
    web
};
use diesel::prelude::*;
use serde::Deserialize;
use yarte::Template;

use crate::{
    models::{Pool, SessionUser, User},
    errors::AuthError,
    utils::{is_json_request, get_current_user, is_signed_in, set_current_user, to_home, verify}, 
    templates::Me
};

pub async fn me(session: Session, req: HttpRequest) -> HttpResponse {
    let user_result = dbg!(get_current_user(&session));

    match is_json_request(&req) {
        true => {
            user_result.map_or(HttpResponse::Unauthorized().json("Unauthorized"), |user| HttpResponse::Ok().json(user))
        },
        false => {
            user_result.map_or(
                HttpResponse::MovedPermanently().header(LOCATION, "/signin").finish(), 
                |user| {
                    let t = Me { user };
            
                    HttpResponse::Ok().content_type("text/html; charset=utf-8").body(t.call().unwrap())
                }
            )
        }
    }
}

Inside the handler, we first check if this is an API request by calling is_json_request(). If it is, we send back the user record if the user is signed in. Otherwise, we send back a 401 error response.

For browser requests we redirect to the sign in page if the user is not signed in. If the user is signed in, we render the user's home page.

Oh, and the dbg! macro is to print out the contents of the variable to the console.

Signing out

First we register the handlers in main.rs:

//src/main.rs
//...

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    //...

    // Start http server
    HttpServer::new(move || {
        App::new()
            //...
            // Routes
            .service(
                web::scope("/")
                    //...
                    .service(
                        web::resource("/signout")
                            .route(web::get().to(auth_handler::sign_out))
                            .route(web::delete().to(auth_handler::sign_out)),
                    ),
            )
    })
    // ...
}

In auth_handler::sign_out(), we clear the session and redirect to the sign in page for browser requests or return a No Content response for API requests.

// src/auth_handler.rs
// ...

pub async fn sign_out(session: Session, req: HttpRequest) -> HttpResponse {
    session.clear();
    
    match is_json_request(&req) {
        true => HttpResponse::NoContent().finish(),
        false => HttpResponse::MovedPermanently().header(LOCATION, "/signin").finish(),
    }
}

Signing in via API

We first register the route handler in main.rs:

// src/main.rs
// ...

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    // ...

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            // Routes
            .service(
                web::scope("/")
                    // ...
                    .service(
                        web::resource("/signin")
                            .route(web::post().to(auth_handler::sign_in)),
                    ),
            )
    })
    // ...
}

We then implement the handler we've registered in src/auth_handler.rs:

// src/auth_handler.rs
// ...

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

pub async fn sign_in(data: web::Json<AuthData>, 
                  session: Session, 
                  req: HttpRequest,
                  pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    match is_signed_in(&session) {
        true => {
            let response = get_current_user(&session).map(|user| HttpResponse::Ok().json(user)).unwrap();

            Ok(response)
        },
        false => handle_sign_in(data.into_inner(), &session, &req, &pool)
    }
}

fn handle_sign_in(data: AuthData, 
                session: &Session, 
                req: &HttpRequest,
                pool: &web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    let result = find_user(data, pool);
    let is_json = is_json_request(req);

    match result {
        Ok(user) => {
            set_current_user(&session, &user);

            if is_json {
                Ok(HttpResponse::Ok().json(user))
            } else {
                Ok(to_home())
            }
        },
        Err(err) => {
            if is_json {
                Ok(HttpResponse::Unauthorized().json(err.to_string()))
            } else {
                let t = SignIn { error: Some(err.to_string()) };
    
                Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(t.call().unwrap()))
            }
        },
    }
}

fn find_user(data: AuthData, pool: &web::Data<Pool>) -> Result<SessionUser, AuthError> {
    use crate::schema::users::dsl::{email, users};
    
    let mut items = users
        .filter(email.eq(&data.email))
        .load::<User>(&pool.get().unwrap())?;

    if let Some(user) = items.pop() {
        if let Ok(matching) = verify(&user.hash, &data.password) {
            if matching {
                return Ok(user.into());
            }
        }
    }

    Err(AuthError::NotFound(String::from("User not found")))
}

As usual, we first check if the user is signed in and return the user's record. We've abstracted the bulk of the procedure into handle_sign_in() because we want to use it for browser sign in as well.

In handle_sign_in(), we call find_user() to retrieve the user record from the database if it exists and based on that create a session and return the user record or an error.

Signing in from a browser

We want to show a form when the user visits /signin and run the sign in process when the form is submitted to /signin2.

Since we've abstracted the sign in process into a private function let's add the browser handlers in src/auth_handler.rs:

// src/auth_handler.rs
// ...
use crate::{
    // ...
    templates::{SignIn, Me}
};

// ...

pub async fn show_sign_in_form(session: Session) -> Result<HttpResponse, AuthError> {
    match is_signed_in(&session) {
        true => Ok(to_home()),
        false => {
            let t = SignIn { error: None };

            Ok(HttpResponse::Ok().content_type("text/html; charset=utf-8").body(t.call().unwrap()))
        }
    }
}

pub async fn sign_in_for_browser(data: web::Form<AuthData>, 
                                session: Session, 
                                req: HttpRequest,
                                pool: web::Data<Pool>) -> Result<HttpResponse, AuthError> {
    handle_sign_in(data.into_inner(), &session, &req, &pool)
}

In src/templates.rs, we declare the sign in template:

// src/templates.rs
// ...

#[derive(Template)]
#[template(path = "pages/sign_in.hbs")]
pub struct SignIn {
    pub error: Option<String>,
}

In our template pages directory, create sign_in.hbs and paste in:

<!--templates/pages/sign_in.hbs -->
{{#> layouts/base title = "Auth Service | Sign in" }}

  {{#if error.is_some() }}
  {{> includes/message success = false, message = error.as_ref().unwrap() }}
  {{/if}}

  <div>
    <h2 class="mt-6 text-center text-3xl leading-9 font-extrabold text-gray-900">
      Sign in
    </h2>
  </div>
  
  <form class="mt-8" action="/signin2" method="POST">
    <div class="rounded-md shadow-sm">
      <div>
        <input 
          placeholder="Email address" 
          type="email" 
          name="email"
          class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5" 
        />
      </div>

      <div class="-mt-px">
        <input aria-label="Password" name="password" type="password" required class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm sm:leading-5" placeholder="Password" />
      </div> 
    </div>

    <div class="mt-6">
      <button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out">
        <span class="absolute left-0 inset-y-0 flex items-center pl-3">
          <svg class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400 transition ease-in-out duration-150" fill="currentColor" viewBox="0 0 20 20">
            <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
          </svg>
        </span>
        Sign in
      </button>
    </div>
  </form>
  
{{~/layouts/base }}

Finally, register the route handlers in main.rs:

// src/main.rs
// ...


#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    // ...

    // Start http server
    HttpServer::new(move || {
        App::new()
            // ...
            // Routes
            .service(
                web::scope("/")
                    // ...
                    .service(
                        web::resource("/signin")
                            .route(web::get().to(auth_handler::show_sign_in_form))
                            .route(web::post().to(auth_handler::sign_in)),
                    )
                    .route("/signin2", web::post().to(auth_handler::sign_in_for_browser)),
            )
    })
    // ...
}

With that, we should be able to sign in from a browser.

The complete source code can be seen on GitHub.