Authentication system using rust (actix-web) and sveltekit - Log in/out, Dockerize and Deploy on fly.io
Tags: docker, rust, javascript, webdev
Published: 2023-04-27 11:42:43



## Introduction

Previously, we successfully connected our front-end application with the backend system. There was an awesome feeling afterwards! However, we have not fully utilized the capabilities of our backend systems yet. We haven't enabled users' login and logout at the front-end. Also, to follow some best practices, we will be containerizing our application to run on any infrastructure and for easy deployment. Specifically, we'll be deploying our backend system on [fly.io][1] using an optimized [Docker][2] file. Let's go.

## Source code

The source code for this series is hosted on GitHub via:

{% github Sirneij/rust-auth %}

## Implementation

> You can get the overview of the code for this article on [github][3]. Some changes were also made directly to the repo's main branch. We will cover them.

### Step 1: Login and Logout users

Having set the pace and foundation for the frontend application in the previous article by registering users directly via the app, it's time to leverage that and extend it to log users in and out:

```html
<!--frontend/src/routes/auth/login/+page.svelte-->

<script lang="ts">
    import { goto } from '$app/navigation';
    import { page } from '$app/stores';
    import { loading } from '$lib/stores/loading.store';
    import { notification } from '$lib/stores/notification.store';
    import { isAuthenticated, loggedInUser } from '$lib/stores/user.store';
    import { BASE_API_URI, happyEmoji } from '$lib/utils/constant';
    import { post } from '$lib/utils/requests/posts.requests';
    import type { CustomError, User } from '$lib/utils/types';
    import { flip } from 'svelte/animate';
    import { scale } from 'svelte/transition';
    let email = '',
        password = '',
        errors: Array<CustomError> = [];
    const handleLogin = async () => {
        loading.setLoading(true, 'Please wait while we log you in...');
        const [res, err] = await post(
            $page.data.fetch,
            `${BASE_API_URI}/users/login/`,
            {
                email: email,
                password: password
            },
            'include'
        );
        if (err.length > 0) {
            loading.setLoading(false);
            errors = err;
        } else {
            loading.setLoading(false);
            const response: User = res as User;
            $loggedInUser = {
                id: response['id'],
                email: response['email'],
                first_name: response['first_name'],
                last_name: response['last_name'],
                is_staff: response['is_staff'],
                is_superuser: response['is_superuser'],
                thumbnail: response['thumbnail']
            };
            $isAuthenticated = true;
            $notification = {
                message: `Login successfull ${happyEmoji}...`,
                colorName: `green`
            };
            let nextPage = $page.url.search.split('=')[1];
            if ($page.url.hash) {
                nextPage = `${nextPage}${$page.url.hash}`;
            }
            await goto(nextPage || '/', { noScroll: true });
        }
    };
</script>

<svelte:head>
    <title>Auth - Login | Actix Web & SvelteKit</title>
</svelte:head>

<div class="flex items-center justify-center h-[60vh]">
    <form
        class="w-11/12 md:w-2/3 lg:w-1/3 rounded-xl flex flex-col items-center align-middle bg-slate-800 py-4"
        on:submit|preventDefault={handleLogin}
    >
        <h1 class="text-center text-2xl font-bold text-sky-400 mb-6">Login</h1>

        {#if errors}
            {#each errors as error (error.id)}
                <p
                    class="text-center text-rose-600"
                    transition:scale|local={{ start: 0.7 }}
                    animate:flip={{ duration: 200 }}
                >
                    {error.error}
                </p>
            {/each}
        {/if}

        <div class="w-3/4 mb-2">
            <input
                type="email"
                name="email"
                id="email"
                bind:value={email}
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="Email address"
                required
            />
        </div>

        <div class="w-3/4 mb-6">
            <input
                type="password"
                name="password"
                id="password"
                bind:value={password}
                class="w-full text-sky-500 placeholder:text-slate-600 border-none focus:ring-0 bg-main-color focus:outline-none py-2 px-3 rounded"
                placeholder="Password"
                required
            />
        </div>

        <div class="w-3/4 flex flex-row justify-between">
            <div class=" flex items-center gap-x-1">
                <input type="checkbox" name="remember" id="" class=" w-4 h-4" />
                <label for="" class="text-sm text-sky-400">Remember me</label>
            </div>
            <div>
                <a href={null} class="text-sm underline text-slate-400 hover:text-sky-400">Forgot?</a>
            </div>
        </div>

        <div class="w-3/4 mt-4">
            <button
                type="submit"
                class="py-2 bg-sky-800 w-full rounded text-blue-50 font-bold hover:bg-sky-700"
            >
                Login
            </button>
        </div>
        <div class="w-3/4 flex flex-row justify-center mt-1">
            <span class="text-sm text-sky-400">
                No account?
                <a href="/auth/register" class="ml-2 text-slate-400 underline hover:text-sky-400">
                    Create an account.
                </a>
            </span>
        </div>
    </form>
</div>
```

The login process is exactly the same thing as the registration process. The only differences are in the number of input elements, and the contents of `handleLogin` function. In the function, we used our login URL endpoint, and set `credentials` to `include`. The latter is extremely important for our cookies to be saved in the browser and available for further requests to the server. If we have a successful login process, we save the user details returned from the server in our `loggedInUser` store and `isAuthenticated` is set to `true`. This is to appropriately change the UI based on the logged-in user. Lastly, we check whether or not there's a redirect path and go to such a path or the home page. What's a redirect path, you ask? Take for instance, we have a protected page, `/auth/about`, that only authenticated users can access. We can redirect users to the login page and after login, redirect them back to where they wanted to access before we forced them to log in. We'll see this in action later.

That's it for login. Next is logout:

```typescript
// frontend/src/lib/utils/requests/logout.requests.ts

import { goto } from '$app/navigation';
import { notification } from '$lib/stores/notification.store';
import { isAuthenticated } from '$lib/stores/user.store';
import { BASE_API_URI, happyEmoji, sadEmoji } from '../constant';
import type { ApiResponse } from '../types';
import { post } from './posts.requests';

/**
 * Logs a user out of the application.
 * @file lib/utils/requests/logout.requests.ts
 * @param {typeof fetch} svelteKitFetch - Fetch object from sveltekit
 * @param {string} [redirectUrl='/auth/login'] - URL to redirect to after logout.
 */
export const logout = async (svelteKitFetch: typeof fetch, redirectUrl = '/auth/login') => {
    const [res, err] = await post(
        svelteKitFetch,
        `${BASE_API_URI}/users/logout/`,
        undefined,
        'include'
    );
    if (err.length > 0) {
        notification.set({
            message: `${err[0].error} ${sadEmoji}...`,
            colorName: 'rose'
        });
    } else {
        const response: ApiResponse = res;
        notification.set({
            message: `${response.message} ${happyEmoji}...`,
            colorName: 'green'
        });

        isAuthenticated.set(false);
        if (redirectUrl !== '') {
            await goto(redirectUrl);
        }
    }
};
```

We wrote a custom logout function which uses our `post` under the hood. It's a simple function. Then we went to our app's header, `frontend/src/lib/component/Header/Header.svelte`, and used the function:

```html
<!--frontend/src/lib/component/Header/Header.svelte-->
<script lang="ts">
    import { page } from '$app/stores';
    import { isAuthenticated, loggedInUser } from '$lib/stores/user.store';
    import JohnImage from '$lib/svgs/john.svg';
    import Avatar from '$lib/svgs/teamavatar.png';
    import { logout } from '$lib/utils/requests/logout.requests';
</script>

<header aria-label="Page Header" class="mb-6">
    <nav class="mx-auto max-w-screen-xl px-4 py-4 sm:px-6 lg:px-8">
        <div class="flex items-center justify-end gap-4">
            <div class="flex items-center gap-4">
                <a
                    href="https://github.com/Sirneij"
                    class="block shrink-0 rounded-full bg-white p-2.5 text-gray-600 shadow-sm hover:text-gray-700"
                >
                    <span class="sr-only">Programmer</span>
                    <img src={JohnImage} alt="John Idogun" class="h-6 w-6 rounded-full object-cover" />
                </a>
            </div>

            <span aria-hidden="true" class="block h-6 w-px rounded-full bg-gray-200" />

            <a
                href="/"
                class="block shrink-0 {$page.url.pathname === `/`
                    ? 'text-sky-500'
                    : 'text-white'} hover:text-sky-400"
            >
                /
            </a>
            {#if !$isAuthenticated}
                <a
                    href="/auth/login"
                    class="block shrink-0 {$page.url.pathname === `/auth/login`
                        ? 'text-sky-500'
                        : 'text-white'} hover:text-sky-400"
                >
                    Login
                </a>
            {:else}
                <a href="/auth/about" class="block shrink-0">
                    <span class="sr-only">{$loggedInUser.first_name} Profile</span>
                    <img
                        alt={$loggedInUser.first_name}
                        src={$loggedInUser.thumbnail ? $loggedInUser.thumbnail : Avatar}
                        class="h-10 w-10 rounded-full object-cover"
                    />
                </a>
                <button
                    type="button"
                    class="text-white hover:text-sky-400"
                    on:click={() => logout($page.data.fetch)}
                >
                    Logout
                </button>
            {/if}
        </div>
    </nav>
</header>
```

The `Logout` button used it. In the header, we also made it smart to detect when a user is authenticated or not and display appropriate links. Based on your web address, we also set active links using JavaScript's ternary operator.

That's it! Let's get to containerization.

### Step 2: Dockerize our backend application

In modern times, containerization is the new normal. We can't do less here! First, we need to do some safekeeping since we are already thinking about production.

Many hosting platforms issue your database configuration in the form of a URL. For PostgreSQL, the URL looks like this:

```shell
postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
```

Because of this, let's delete the database settings we have and convert it into a `DATABASE_URL` which is set in our `.env` file. We will also remove the `URI` under `redis` settings since that will also be replaced with `REDIS_URL`. Having done that, our `build` method in `backend/src/startup.rs` will be modified:

```rust
...
let connection_pool = if let Some(pool) = test_pool {
    pool
} else {
    let db_url = std::env::var("DATABASE_URL").expect("Failed to get DATABASE_URL.");
    match sqlx::postgres::PgPoolOptions::new()
        .max_connections(5)
        .connect(&db_url)
        .await
    {
        Ok(pool) => pool,
        Err(e) => {
            tracing::event!(target: "sqlx",tracing::Level::ERROR, "Couldn't establish DB connection!: {:#?}", e);
            panic!("Couldn't establish DB connection!")
        }
    }
};
...
```

We are getting `db_url` from our environment variable. We then use it to connect to our database. Pretty neat! Now, you can safely delete `get_connection_pool`.

In the `run` method, we also obtain our `redis_url` from `REDIS_URL`. Also note that we now use redis to store our cookies:

```toml
# backend/Cargo.toml
...
actix-session = { version = "0.7", features = [
    "redis-rs-session",
    "redis-rs-tls-session",
] }
...
```

We discarded `CookieSessionStore` because of [its limitations][4].

There were other minor changes in the `run` function inside `backend/src/startup.rs` such as removing the `settings.debug` condition in the SessionMiddleware configuration and some others. The repo has the updated code.

Now to containerization proper. For our application, we will be using a `Dockerfile` to deploy it. We, therefore, need to ensure that the build resulting from the file is as small as possible. Hence, we separate the build process into two stages: `builder` and `runtime`. Doing it this way makes the resulting build minimal in size. Our app's `Dockerfile` looks like this:

```dockerfile
# backend/Dockerfile

# Builder stage
FROM rust:1.63.0 AS builder
WORKDIR /app
RUN apt update && apt install lld clang -y
COPY . .
RUN cargo build --release

# Runtime stage
FROM debian:bullseye-slim AS runtime
WORKDIR /app
# Install OpenSSL - it is dynamically linked by some of our dependencies
# Install ca-certificates - it is needed to verify TLS certificates
# when establishing HTTPS connections
RUN apt-get update -y \
    && apt-get install -y --no-install-recommends openssl ca-certificates \
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/backend backend
# We need the settings file at runtime!
COPY settings settings
COPY templates templates
ENV APP_ENVIRONMENT production
ENV APP_DEBUG false
ENTRYPOINT ["./backend"]
```

We only did what mattered. At the "builder stage", our aim is to compile our app. To do that, all we need is a Rust environment. Having done that we moved to the "runtime stage". At this stage, we discard the previous Rust environment, because it is not needed again, and used a small "OS" that can successfully run the executable made available by the "builder stage", copied over using `COPY --from=builder /app/target/release/backend backend`. We also remove unnecessary files that normally come with Debian OS that are not useful for our app. Other important folders, viz: settings, and templates, were also copied and we told docker to use the `production` environment. Our app's entry point is the `backend` executable made available by our build process.

This `Dockerfile` is remarkably slim, less than `110 MiB`. However, we still have an issue, inherent to Rust. Considerable compile time! Applications in Rust with a fair amount of dependencies like ours take time to compile every time it is built. We can employ caching here so that once built, it won't take time to rebuild unless we add packages or modify existing ones. To do this, the Rust ecosystem comes to the rescue once again. We will use a crate called [cargo-chef][5]. You don't need to install it in your app. We just need to change our Dockerfile to use it:

```dockerfile
# backend/Dockerfile

FROM lukemathwalker/cargo-chef:latest-rust-latest as chef
WORKDIR /app
RUN apt update && apt install lld clang -y

FROM chef as planner
COPY . .
# Compute a lock-like file for our project
RUN cargo chef prepare  --recipe-path recipe.json

FROM chef as builder
COPY --from=planner /app/recipe.json recipe.json
# Build our project dependencies, not our application!
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
# Build our project
RUN cargo build --release --bin backend

FROM debian:bullseye-slim AS runtime
WORKDIR /app
RUN apt-get update -y \
    && apt-get install -y --no-install-recommends openssl ca-certificates \
    # Clean up
    && apt-get autoremove -y \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/backend backend
COPY settings settings
COPY templates templates
ENV APP_ENVIRONMENT production
ENV APP_DEBUG false
ENTRYPOINT ["./backend"]
```

Almost the same thing as our previous file aside from some specifics. [Cargo-chef's docs][5] does justice to it.

Now to deployment!

### Step 3: Deploy the rust app to [fly.io][1] freely

[fly.io][1] offers us a free hosting service, enough to test your application: `1 CPU, 256 MiB`. You are also given a free PostgreSQL database of 1GB size and Redis. That's just enough for testing!  Though [fly.io][1] does not support rust natively, using Docker, we can get it deployed successfully! We will be using these [guide][6] and [guide][7] to achieve it.

According to [this guide][6], you can deploy your app in 3 steps:

- [Install][8] fly.io CLI, called flyctl.
- Login to your account using `fly auth login` if you have an account or register using `fly auth signup` if otherwise.
- Create, configure, and deploy your application application using `fly launch`.

Kindly do the first two before proceeding.

Having installed the CLI and created or logged in to your account, let's deploy our app!

We will be using [this other guide][7] to do that.

At the root of our backend app, where our `Dockerfile` resides, open your terminal and issue this command:

```shell
~/rust-auth/backend$ fly launch
```

It detects your Dockerfile automatically! You should see something like:

```shell
? App Name (leave blank to use an auto-generated name):
? Select organization: Mark Ericksen (personal)
? Select region: lax (Los Angeles, California (US))
Created app weathered-wave-1020 in organization personal
Wrote config file fly.toml
? Would you like to deploy now? (y/N)
```

It first asks for your app's name. I put `auth-actix-web-sveltekit`. If you don't provide a name, it'll generate one for you. I accepted other defaults. It will then ask if you will like to provision a PostgreSQL database and Redis for your app. Please type `y`. It will provision them and display their credentials. It will also help you set `DATABASE_URL` and `REDIS_URL` automatically. 

> **NOTE: Please, ensure you copy the Database credentials shown to you because you won't get to see them again**.

After that, a file, `fly.toml`, will be generated for you. You will then be asked if you want to deploy your app now. I chose `N` because I needed to set some environment variables for my emails and secret keys.

Now, to set your email credentials for SMTP, do:

```shell
~/rust-auth/backend$ flyctl secrets set APP_EMAIL__HOST_USER_PASSWORD=<your_password>
~/rust-auth/backend$ flyctl secrets set APP_EMAIL__HOST_USER=<your_email_address>
```

Any environment variable set with the `secrets` command remains secret! You can use that to set `APP_SECRET__SECRET_KEY` and co as well but I opted to do it in `fly.toml` file instead for demonstration.

In your `backend/fly.toml` file, create an `[env]` section and make it look like this:

```toml
# backend/fly.toml
...
[env]
APP_SECRET__SECRET_KEY = "KaPdSgVkYp3s6v9y$B&E)H+MbQeThWm6"
APP_SECRET__HMAC_SECRET = "001448809fd614fcf2b19a6caeac40834f0771a0af0ef0849280e8042fd95918"
APP_SECRET__TOKEN_EXPIRATION = "15"
APP_APPLICATION__BASE_URL = "https://auth-actix-web-sveltekit.fly.dev"
APP_APPLICATION__PORT = "8080"
APP_FRONTEND_URL = "https://rust-auth.vercel.app"
```

I set `secret_key` and `hmac_secret` as well as `token_expiration`. Our app's `base_url` is now `https://${APP_NAME}.fly.dev`. Please ensure you set the app's `port` to the `internal_port` at the top of the `fly.toml` file. If not, your app may not go live. I have already deployed the frontend app to [vercel][9] and set the URL as `frontend_url`. Deploying a SvelteKit app on Vercel is extremely simple and [SvelteKit docs has a guide][10]. Before deploying your front-end, ensure you set your `VITE_BASE_API_URI_PROD` to `https://${APP_NAME}.fly.dev`.

Congratulations!!! Your application is live!!! 💃💃💃🕺🕺🕺

I will see you in the next article...

## 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://fly.io/ "Deploy App Servers Close to Your Users"
[2]: https://www.docker.com/ "Develop faster. Run anywhere."
[3]: https://github.com/Sirneij/rust-auth/pull/7/commits/e0289b97e46f31d624d9bfaf0001041dd9f65054 "Login logout deployment"
[4]: https://docs.rs/actix-session/latest/actix_session/storage/struct.CookieSessionStore.html#limitations "Limitations of CookieSessionStore"
[5]: https://crates.io/crates/cargo-chef/ "Cargo-chef"
[6]: https://fly.io/docs/speedrun/ "Speedrun! Deploying to Fly.io!"
[7]: https://fly.io/docs/languages-and-frameworks/dockerfile/ "Deploy via Dockerfile"
[8]: https://fly.io/docs/hands-on/install-flyctl/ "Install flyctl"
[9]: https://vercel.com/ "Vercel"
[10]: https://kit.svelte.dev/docs/adapter-vercel "Vercel deployment guide"