Authentication system using rust (actix-web) and sveltekit - Backend Intro
Tags: webdev, rust, tutorial, actix
Published: 2023-04-23 14:54:46

## Introduction

Howdy guys! It's been a while here. I have been learning some rust while I was away and I will be sharing some of the things I've learned.

An authentication system is an integral part of modern applications. It's so important that almost all modern applications have some sort of it. Because of their critical nature, such systems should be secure and should follow [OWAP®][1]'s recommendations on web security and password hashing as well as storage to prevent attacks such as Preimage and Dictionary attacks (common to SHA algorithms). To demonstrate some of the recommendations, we'll be building a robust session-based authentication system in Rust and a complementary frontend application. For this article series, we'll be using Rust's [actix-web][2] and some awesome crates for the backend service. [SvelteKit][3] will be used for the frontend. It should be noted however that what we'll be building is largely framework agnostic. As a result, you can decide to opt for [axum][4], [rocket][5], [warp][6] or any other rust's web framework for the backend and [react][7], [vue][8] or any other javascript framework for the frontend. You can even use rust's [yew][9], [seed][10] or some templating engines such as [MiniJinja][11] or [tera][12] at the frontend. It's entirely up to you. Our focus will be more on the concepts.

> NOTE: We'll be utilizing the book, [Zero to Production in Rust][13], heavily with some additional features and modifications.

Though we'll be building a session-based authentication system, it's noteworthy that with the introduction of some concepts which will be discussed in due time, you can turn it into [JWT][14]- or, more securely and appropriately, [PASETO][15]-based authentication system.

> **NOTE: This tutorial will be split into several short articles. At least one (1) article will be uploaded every week until the entire series is complete.**

## System's Requirement Specification

Throughout this tutorial series, we'll be working towards implementing these requirements:

> Build a user authentication system where a user authenticates with an E-mail/Password combination. E-mail addresses must be **unique** and **verified** by sending time-limited verification emails upon registration and the verification **emails must support HTML**. **Until verified, no user is allowed to log in**. Time attacks must be addressed by **sending the mails asynchronously**. Password hashing must be strong and only hashed passwords should be stored in the database. Password reset functionality should be incorporated and incepted using e-mail address verifications. A protected user profile update feature should be added so that only authenticated and authorized users can access it. The user profile should include a thumbnail which should be stored in AWS S3.

😲 That was a lot, huh?! It is from this end too 😫😩. From the specification, we are bound to have some fun. We'll be moving high and charting the territory of image uploads to [AWS S3][16], email verification, token generation and destruction, some templating and a host of others.

## Technology stack

For emphasis, our tech stack comprises:

- Backend - Some crates that will be used are:
  - [Actix web (v4)][2] - Main backend web framework
  - [tokio v1][19] - An asynchronous runtime for Rust
  - [serde v1][20] - Serializing and Deserializing Rust data structures
  - [MiniJinja v0.32][11] - Templating engine
  - [SQLx v0.6][21] - Async SQL toolkit for rust
  - [PostgreSQL v15][22] - Database
  - [Redis][23] - A store to facilitate the expiration of tokens, etc.

- Frontend - Some tools that will be used are:
  - [SvelteKit v1][3] - Main frontend framework
  - [Typescript v5][17] - Language in which the frontend will be written
  - Pure CSS3 and [TailwindCSS v3.3][18] - Styles
  - HTML5 - Structure

## Assumption

A simple prerequisite to follow along is some familiarity with the Rust Programming language &mdash; like some understanding of structs, ownership model, borrow checker, module system, and co. &mdash; JavaScript (Typescript) and CSS. You do not need to be an expert &mdash; I ain't one in any of the technologies.

## Source code

The source code for this series is hosted on [github][3] or more expressed via:

{% github Sirneij/rust-auth %}

## Initial project structure

> You can get this full starter template from [github][24].

I inherited a rather, in my opinion, robust Rust web services structure from [Zero to Production in Rust][13]. I have fallen in love with the structure and will most likely be using it for most of my Rust web project irrespective of the framework of choice. This starter template is available [here][24] and how it was made will be discussed briefly. I encourage you to pick up the book, [Zero to Production in Rust][13]. It's fantastic!!! Currently, the `backend` structure looks like this:

```shell
├── Cargo.lock
├── Cargo.toml
├── settings
│   ├── base.yaml
│   ├── development.yaml
│   └── production.yaml
├── src
│   ├── lib.rs
│   ├── main.rs
│   ├── routes
│   │   ├── health.rs
│   │   └── mod.rs
│   ├── settings.rs
│   ├── startup.rs
│   └── telemetry.rs
└── tests
```

### Step 1: Create a new project and install some dependencies

Create a directory that will house the entire (both frontend and backend) application. I called mine `rust-auth`. Change the directory into the newly created folder and issue the following command in your terminal:

```shell
~/rust-auth$ cargo new backend
```

This creates a new project called `backend` with `Cargo.toml`, `Cargo.lock`, and `src/main.rs` files created. Open it up in your editor of choice. Make your `Cargo.toml` file look like this:

```toml
# Cargo.toml

[package]
name = "backend"
version = "0.1.0"
authors = ["Your name <your email>"]
edition = "2021"

[lib]
path = "src/lib.rs"

[[bin]]
path = "src/main.rs"
name = "backend"

[dependencies]
actix-web = "4"
config = { version = "0.13.3", features = ["yaml"] }
dotenv = "0.15.0"
serde = "1.0.160"
tokio = { version = "1.27.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = [
    "fmt",
    "std",
    "env-filter",
    "registry",
    'json',
    'tracing-log',
] }
```

We added `authors` to the `[package]` segment. Then we created a new segment, `[lib]`, which points to the path of the project's `lib.rs` file. A project can have only one `lib.rs` file. Next is the binary segment, `[[bin]]`. Double square brackets in `.toml` files mean an array. It was used because we can have more than one binary package in a Rust project. These two new segments make it easier for us to write seamlessly integrated testing where tests are "independent" of the web framework used. Then, the `[dependencies]` section. We registered the preliminary crates we will be using. `config` helps with the easy transformation of `.yaml` or `.json` files containing some app-wide settings, like the variables in Django's `settings.py` file, into rust's structs. `dotenv` loads environment variables from a `.env` file. `serde` is rust's generic serialization/deserialization framework. `tokio` is an industry-standard runtime for writing reliable, asynchronous, and slim applications with rust. At runtime or when our application is in production, we ultimately need to log requests and responses. Sometimes, our users make some complaints or our app crashes. We cannot just figure out why certain situation occurs out of thin air. We need a point of reference to debug our application. In the rust ecosystem, `tracing` and its extension `tracing-subscriber` is widely used for this. `Telemetry` is what it's called.

### Step 2: Build out the project's skeleton

Inside the `src` folder, issue the following commands to create some files and folders:

```shell
~/rust-auth/backend$ touch src/lib.rs src/startup.rs src/settings.rs src/telemetry.rs

~/rust-auth/backend$ mkdir src/routes && touch src/routes/mod.rs src/routes/health.rs
```

For the created files and folders to be recognized, we need to turn them into modules in `lib.rs`:

```rust
// src/lib.rs

pub mod routes;
pub mod settings;
pub mod startup;
pub mod telemetry;
```

Let's start with `telemetry.rs`. Make it look like this:

```rust
// src/telemetry.rs

use tracing_subscriber::layer::SubscriberExt;

pub fn get_subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync {
    let env_filter = if debug {
        "trace".to_string()
    } else {
        "info".to_string()
    };
    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter));

    let stdout_log = tracing_subscriber::fmt::layer().pretty();
    let subscriber = tracing_subscriber::Registry::default()
        .with(env_filter)
        .with(stdout_log);

    let json_log = if !debug {
        let json_log = tracing_subscriber::fmt::layer().json();
        Some(json_log)
    } else {
        None
    };

    let subscriber = subscriber.with(json_log);

    subscriber
}

pub fn init_subscriber(subscriber: impl tracing::Subscriber + Send + Sync) {
    tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
}
```

We are configuring `tracing_subscriber`'s level and format depending on whether or not our app is in production. If `debug` is `true`, then we're in development mode. Else, in production. We want `JSON` output in production since that is easier to parse. Then, we initialized tracing in another function based on the subscriber given.

Next, `settings.rs`:

```rust
// src/settings.rs

/// Global settings for exposing all preconfigured variables
#[derive(serde::Deserialize, Clone)]
pub struct Settings {
    pub application: ApplicationSettings,
    pub debug: bool,
}

/// Application's specific settings to expose `port`,
/// `host`, `protocol`, and possible URL of the application
/// during and after development
#[derive(serde::Deserialize, Clone)]
pub struct ApplicationSettings {
    pub port: u16,
    pub host: String,
    pub base_url: String,
    pub protocol: String,
}

/// The possible runtime environment for our application.
pub enum Environment {
    Development,
    Production,
}

impl Environment {
    pub fn as_str(&self) -> &'static str {
        match self {
            Environment::Development => "development",
            Environment::Production => "production",
        }
    }
}

impl TryFrom<String> for Environment {
    type Error = String;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        match s.to_lowercase().as_str() {
            "development" => Ok(Self::Development),
            "production" => Ok(Self::Production),
            other => Err(format!(
                "{} is not a supported environment. Use either `development` or `production`.",
                other
            )),
        }
    }
}

/// Multipurpose function that helps detect the current environment the application
/// is running using the `APP_ENVIRONMENT` environment variable.
///
/// \`\`\`
/// APP_ENVIRONMENT = development | production.
/// \`\`\`
///
/// After detection, it loads the appropriate .yaml file
/// then it loads the environment variable that overrides whatever is set in the .yaml file.
/// For this to work, you the environment variable MUST be in uppercase and starts with `APP`,
/// a `_` separator then the category of settings,
/// followed by `__` separator,  and then the variable, e.g.
/// `APP__APPLICATION_PORT=5001` for `port` to be set as `5001`
pub fn get_settings() -> Result<Settings, config::ConfigError> {
    let base_path = std::env::current_dir().expect("Failed to determine the current directory");
    let settings_directory = base_path.join("settings");

    // Detect the running environment.
    // Default to `development` if unspecified.
    let environment: Environment = std::env::var("APP_ENVIRONMENT")
        .unwrap_or_else(|_| "development".into())
        .try_into()
        .expect("Failed to parse APP_ENVIRONMENT.");
    let environment_filename = format!("{}.yaml", environment.as_str());
    let settings = config::Config::builder()
        .add_source(config::File::from(settings_directory.join("base.yaml")))
        .add_source(config::File::from(
            settings_directory.join(environment_filename),
        ))
        // Add in settings from environment variables (with a prefix of APP and '__' as separator)
        // E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port`
        .add_source(
            config::Environment::with_prefix("APP")
                .prefix_separator("_")
                .separator("__"),
        )
        .build()?;

    settings.try_deserialize::<Settings>()
}
```

We have a couple of structs and an enum. These structs map directly to the `.yaml` files we will create soon. The bulk of the work in this `settings.rs` file is done in the `get_settings` function. Anytime we need some settings variables, we will get them by calling this function. Looking inwards, we first try to get the path of the directory where our `.yaml` files are located. Then we detect whether or not we are in development. By default, we assume that the app is in development. To change it to production, you must set `APP_ENVIRONMENT=production` in your `.env` file or any other way you set your environment variables. Since development and production environments share some variables &mdash; we store those in `base.yaml` &mdash; we use our `config` crate to first load those common configurations before loading environment-specific ones. This is because we are likely to override those common configurations on a per-environment basis. There are some configurations in the `.yaml` files that we may want to change their values using environment variables. Tokens, passwords and secret_key are some examples. We want the ones set via environment variables to take precedence. For example, if we set `debug: true` in `base.yaml` but in production, we want `debug: false`. We can just do `APP_DEBUG=true` in our `.env` file and this will override the one in `base.yaml`. Notice the prefix, `APP_`. It's required for such to be recognized as the setting's variable. You are at liberty to change the prefix as well. We learn more about these nuances as we progress. Now, let's create the `settings/` directory at the root of the project. We will also create `base.yaml`, `development.yaml` and `production.yaml` in it:

```shell
~/rust-auth/backend$ mkdir settings && touch settings/base.yaml settings/development.yaml settings/production.yaml
```

For now, make `settings/base.yaml` looks like this:

```yaml
# settings/base.yaml
application:
  port: 5000
```

`settings/development.yaml`:

```yaml
# settings/development.yaml

application:
  protocol: http
  host: 127.0.0.1
  base_url: "http://127.0.0.1"

debug: true
```

And, `settings/production.yaml`:

```yaml
# settings/production.yaml

application:
  protocol: https
  host: 0.0.0.0
  base_url: ""

debug: false
```

Next is `src/startup.rs`:

```rust
// src/startup.rs

pub struct Application {
    port: u16,
    server: actix_web::dev::Server,
}

impl Application {
    pub async fn build(settings: crate::settings::Settings) -> Result<Self, std::io::Error> {
        let address = format!(
            "{}:{}",
            settings.application.host, settings.application.port
        );

        let listener = std::net::TcpListener::bind(&address)?;
        let port = listener.local_addr().unwrap().port();
        let server = run(listener).await?;

        Ok(Self { port, server })
    }

    pub fn port(&self) -> u16 {
        self.port
    }

    pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
        self.server.await
    }
}

async fn run(listener: std::net::TcpListener) -> Result<actix_web::dev::Server, std::io::Error> {
    let server = actix_web::HttpServer::new(move || {
        actix_web::App::new().service(crate::routes::health_check)
    })
    .listen(listener)?
    .run();

    Ok(server)
}
```

It starts up our entire application and is done in the `run_until_stopped` method of the `Application` struct. The motive for writing this way is for easy testing. This is NOT the only way to start `actix-web` server. The normal practice is way shorter, at least for a start. But this is entirely a design decision which is optional.

Let's enter into `src/main.rs`:

```rust
// src/main.rs

#[tokio::main]
async fn main() -> std::io::Result<()> {
    dotenv::dotenv().ok();

    let settings = backend::settings::get_settings().expect("Failed to read settings.");

    let subscriber = backend::telemetry::get_subscriber(settings.clone().debug);
    backend::telemetry::init_subscriber(subscriber);

    let application = backend::startup::Application::build(settings).await?;

    tracing::event!(target: "backend", tracing::Level::INFO, "Listening on http://127.0.0.1:{}/", application.port());

    application.run_until_stopped().await?;
    Ok(())
}
```

`src/main.rs` is the entry point for Rust's applications. We opt for `#[tokio::main]` runtime. You can use `#[actix_web::main]` instead. Then, we brought `dotenv` into the game to help load all environment variables in our `.env` file. We then get our settings as written in `src/settings.rs`. Telemetry is then initialized. Our entire app was then built and subsequently run but before it was run, we let the developer know the port where our app runs using `tracing::event` macro. We could get the port here because we made it available in our `src/startup.rs`. With this, we may not touch this file, `src/main.rs`, again throughout this series. Our point-of-contact will be `src/startup.rs`.

If you try to run our skeletal app at this point, it will not compile yet. Let's fix that.

Navigate to `src/routes/health.rs` and make it look like this:

```rust
// src/routes/health.rs

#[tracing::instrument]
#[actix_web::get("/health-check/")]
pub async fn health_check() -> actix_web::HttpResponse {
    tracing::event!(target: "backend", tracing::Level::DEBUG, "Accessing health-check endpoint.");
    actix_web::HttpResponse::Ok().json("Application is safe and healthy.")
}
```

We have a simple endpoint to check whether or not is online. You can see how easy it is to write an API endpoint in actix-web. Apart from the instrumentations, you can wire up a "fully functional" GET request endpoint with just 3 lines of code!!!

Looking into the endpoint, we used `#[tracing::instrument]` to help keep logs of all requests into this function. That's instrumentation. We then use `#[actix_web::get("/health-check/")]` to signal that only `GET` requests are allowed on `/health-check/`. Any other methods will be rejected. One of the reasons for using [actix-web][2] is its native support for asynchronous functions coupled with the fact that it's extremely fast. We made our function async and we expect the function to return an HTTP response, `actix_web::HttpResponse`. There are other ways to achieve this but I favour this method due to its brevity. We then return a message, `Application is safe and healthy.`, in JSON format to the user using HTTP Ok status, 200. There are other HTTP Response methods available in actix-web and we'll encounter some.

Next, we need to make this method available. Open up `src/routes/mod.rs`:

```rust
// src/routes/mod.rs

mod health;

pub use health::health_check;
```

This makes it publicly accessible. We then registered it as a service in `src/startup.rs` using `crate::routes::health_check`:

```rust
// src/startup.rs

...
async fn run(listener: std::net::TcpListener) -> Result<actix_web::dev::Server, std::io::Error> {
    let server = actix_web::HttpServer::new(move || {
       actix_web::App::new().service(crate::routes::health_check)
    })
    .listen(listener)?
    .run();

    Ok(server)
}
```

That's it for the first article in the series!! See y'all in the next one.

## Outro

Enjoyed this article? Consider [contacting me for a job, something worthwhile or buying a coffee ☕](mailto:sirneij@gmail.com). You can also connect with/follow me on [LinkedIn](https://www.linkedin.com/in/idogun-john-nelson/) and [Twitter](https://twitter.com/Sirneij). It isn't bad if you help share this article for wider coverage. I will appreciate it...

[1]: https://owasp.org/ "The Open Worldwide Application Security Project®"
[2]: https://actix.rs/ "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
[3]: https://kit.svelte.dev/ "web development, streamlined"
[4]: https://crates.io/crates/axum "Web framework that focuses on ergonomics and modularity"
[5]: https://rocket.rs/ "Rocket is a web framework for Rust that makes it simple to write fast, secure web applications without sacrificing flexibility, usability, or type safety."
[6]: https://crates.io/crates/warp "serve the web at warp speeds"
[7]: https://react.dev/ "The library for web and native user interfaces"
[8]: https://vuejs.org/ "An approachable, performant and versatile framework for building web user interfaces."
[9]: https://yew.rs/ "A framework for creating reliable and efficient web applications."
[10]: https://seed-rs.org/ "Rust framework for creating fast and reliable web apps"
[11]: https://github.com/mitsuhiko/minijinja "MiniJinja is a powerful but minimal dependency template engine for Rust"
[12]: https://tera.netlify.app/ "A powerful, easy to use template engine for Rust"
[13]: https://www.zero2prod.com/ "An introduction to backend development"
[14]: https://jwt.io/ "JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties."
[15]: https://paseto.io/ "Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the many design deficits that plague the JOSE standards."
[16]: https://s3.console.aws.amazon.com/ "Store and retrieve any amount of data from anywhere"
[17]: https://www.typescriptlang.org/ "TypeScript is JavaScript with syntax for types."
[18]: https://tailwindcss.com/ "A utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup."
[19]: https://tokio.rs/ "Tokio is an asynchronous runtime for the Rust programming language."
[20]: https://serde.rs/ "Serde is a framework for serializing and deserializing Rust data structures efficiently and generically."
[21]: https://crates.io/crates/sqlx "The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite."
[22]: https://www.postgresql.org/ "The World's Most Advanced Open Source Relational Database"
[23]: https://redis.io/ "The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker."
[24]: https://github.com/Sirneij/rust-auth/tree/rust-application-template/backend "Initial backend project structure"