DEV Community

Cover image for Authentication system using rust (actix-web) and sveltekit - CORS and Frontend Integration
John Owolabi Idogun
John Owolabi Idogun

Posted on • Updated on

Authentication system using rust (actix-web) and sveltekit - CORS and Frontend Integration

Introduction

So far so good, we have spawned some amazing systems that, "fictitiously", can register, verify, log in and out such a user. As rightly pointed out, this claim is still fictitious since there is currently no automated test (which will be added at a later stage) or an application that truly uses it. This article is about the latter point. We will be building our frontend application to communicate with the backend services we had built so far. Let's incept!

Source code

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

GitHub logo Sirneij / rust-auth

A fullstack authentication system using rust, sveltekit, and Typescript

rust-auth

A full-stack secure and performant authentication system using rust's Actix web and JavaScript's SvelteKit.

This application resulted from this series of articles and it's currently live here(I have disabled the backend from running live).

Run locally

You can run the application locally by first cloning it:

~/$ git clone https://github.com/Sirneij/rust-auth.git
Enter fullscreen mode Exit fullscreen mode

After that, change directory into each subdirectory: backend and frontend in different terminals. Then following the instructions in each subdirectory to run them.




Implementation

You can get the overview of the code for this article on github.

Step 1: Install and Configure CORS

Right now, it's impossible to connect another standalone application, not running on the same address, to our current backend service successfully with some errors. The reason for that is what is termed CORS (Cross-Origin Resource Sharing). Simply put, it is a security system that prevents systems of different origins (addresses) from sharing resources without being specifically allowed to.

In our current application configuration, we have not whitelisted any other application to share its resources. Let's do that with yet another awesome crate in the Actix Web ecosystem, actix-cors:

~/rust-auth/backend$ cargo add actix-cors
Enter fullscreen mode Exit fullscreen mode

Next, let's whitelist our frontend application to share our backend resources:

// src/startup.rs
...
async fn run(
    listener: std::net::TcpListener,
    db_pool: sqlx::postgres::PgPool,
    settings: crate::settings::Settings,
) -> Result<actix_web::dev::Server, std::io::Error> {
    ...
    let server = actix_web::HttpServer::new(move || {
    actix_web::App::new()
        ...
        .wrap(
            actix_cors::Cors::default()
                .allowed_origin(&settings.frontend_url)
                .allowed_methods(vec!["GET", "POST", "PATCH", "DELETE"])
                .allowed_headers(vec![
                    actix_web::http::header::AUTHORIZATION,
                    actix_web::http::header::ACCEPT,
                ])
                .allowed_header(actix_web::http::header::CONTENT_TYPE)
                .expose_headers(&[actix_web::http::header::CONTENT_DISPOSITION])
                .supports_credentials()
                .max_age(3600),
            )
        ...
}
...
Enter fullscreen mode Exit fullscreen mode

We allowed requests from settings.frontend_url, one of the settings we created before now. Ensure you set the value to the address of your front-end application. Mine was set to https://localhost:3000. Notice that I used HTTPS protocol because if we don't, the session configuration made in the previous article will prevent it to work. We then allowed the allowed origins to make GET, POST, PATCH, and DELETE requests. actix-cors add OPTIONS automatically for us. We also supported sending authentication credentials such as cookies. That's the last bit we need to do at the backend for now.

Step 2: Install and setup TailwindCSS for SvelteKit

As stated, we will be using TailwindCSS v3.3 for our styling and we need to install and set it up. The steps to achieve that were enumerated here.

Ensure you get that done before proceeding so that we will be on the same page.

Having installed Tailwind, let's start making our front-end application.

Step 3: Serve SvelteKit dev with HTTPS

As earlier emphasized, we need our frontend application to be served via HTTPS protocol for our backend to recognize it. However, by default, vite serves apps in development via the HTTP protocol instead. We need to change this. Because Vite knew this scenario would happen, they made a package called plugin-basic-ssl. Install it as a dev package:

~/rust-auth/frontend$ npm i -D @vitejs/plugin-basic-ssl
Enter fullscreen mode Exit fullscreen mode

Then open up frontend/vite.config.ts:

// 
...
import basicSsl from '@vitejs/plugin-basic-ssl';
...
plugins: [basicSsl(), sveltekit()],
...
Enter fullscreen mode Exit fullscreen mode

Also, proceed to your package.json file and replace the dev under scripts with:

// frontend/package.json
{
...
    "dev": "vite dev --port 3000 --https",
...
}
Enter fullscreen mode Exit fullscreen mode

Now, whenever you run your application using npm run dev, it will be served via HTTPS!

Step 4: Build user registration UI and logic

Since that's done, let's make our user registration UI.

SvelteKit, like NextJS, uses filesystem-based routing. This means the path of any +page.svelte in the routes directory is automatically considered a route. If, for instance, you have src/auth/register/+page.svelte, your register URL in the browser will be https://localhost:3000/auth/register. This example is what we'll use. Open up src/auth/register/+page.svelte:

<!-- src/auth/register/+page.svelte -->
<script lang="ts">
    import { goto } from '$app/navigation';
    import { page } from '$app/stores';
    import { apiResponse } from '$lib/stores/api.response.store';
    import { loading } from '$lib/stores/loading.store';
    import { notification } from '$lib/stores/notification.store';
    import { BASE_API_URI, happyEmoji } from '$lib/utils/constant';
    import {
        isValidEmail,
        isValidPasswordMedium,
        isValidPasswordStrong
    } from '$lib/utils/helpers/input.validation';
    import { post } from '$lib/utils/requests/posts.requests';
    import type { ApiResponse, CustomError } from '$lib/utils/types';
    import { flip } from 'svelte/animate';
    import { scale } from 'svelte/transition';
    let email = '',
        password = '',
        first_name = '',
        last_name = '',
        confirmPassword = '',
        errors: Array<CustomError> = [],
        fieldsError = { email: '', password: '', first_name: '', last_name: '', confirmPassword: '' },
        isValid = false;
    const handleRegister = async () => {
        isValid = true;
        if (!isValidEmail(email)) {
            isValid = false;
            fieldsError.email = 'That email address is invalid.';
        } else {
            fieldsError.email = '';
        }
        if (!isValidPasswordMedium(password)) {
            isValid = false;
            fieldsError.password =
                'Password is not valid. Password must contain six characters or more and has at least one lowercase and one uppercase alphabetical character or has at least one lowercase and one numeric character or has at least one uppercase and one numeric character.';
        } else {
            fieldsError.password = '';
        }
        if (confirmPassword.trim() !== password.trim()) {
            isValid = false;
            fieldsError.confirmPassword = 'Password does not match.';
        } else {
            fieldsError.confirmPassword = '';
        }
        if (isValid) {
            loading.setLoading(true, 'Please wait while we register you...');
            const [res, err] = await post($page.data.fetch, `${BASE_API_URI}/users/register/`, {
                first_name: first_name,
                last_name: last_name,
                email: email,
                password: password
            });
            if (err.length > 0) {
                loading.setLoading(false);
                errors = err;
            } else {
                loading.setLoading(false);
                const response: ApiResponse = res;
                $notification = {
                    message: `You have successfully registered ${happyEmoji}...`,
                    borderColor: `border-green-300 bg-green-100`,
                    textTopColor: 'text-green-800',
                    textBottomColor: 'text-green-600'
                };
                $apiResponse = {
                    message: response.message ? response.message : '',
                    status: response.status ? response.status : ''
                };
                await goto('/auth/confirming');
            }
        }
    };
</script>

<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 bg-slate-800 py-4"
        on:submit|preventDefault={handleRegister}
    >
        <h1 class="text-center text-2xl font-bold text-sky-400 mb-6">Register</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
            />
            {#if fieldsError.email}
                <span class="text-center text-rose-600" transition:scale|local={{ start: 0.7 }}>
                    {fieldsError.email}
                </span>
            {/if}
        </div>

        <div class="w-3/4 mb-2">
            <input
                type="text"
                name="first_name"
                bind:value={first_name}
                id="first-name"
                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="First name"
                required
            />
        </div>
        <div class="w-3/4 mb-2">
            <input
                type="text"
                name="last_name"
                id="last-name"
                bind:value={last_name}
                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="Last name"
                required
            />
        </div>

        <div class="w-3/4 mb-2">
            <input
                type="password"
                name="password"
                bind:value={password}
                id="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
            />
            {#if fieldsError.password}
                <span class="text-center text-rose-600" transition:scale|local={{ start: 0.7 }}>
                    {fieldsError.password}
                </span>
            {/if}
        </div>

        <div class="w-3/4 mb-6">
            <input
                type="password"
                name="confirm-password"
                bind:value={confirmPassword}
                id="confirm-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="Confirm password"
                required
            />
            {#if fieldsError.confirmPassword}
                <span class="text-center text-sm text-rose-600" transition:scale|local={{ start: 0.7 }}>
                    {fieldsError.confirmPassword}
                </span>
            {/if}
        </div>

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

That's a lot! A closer look will simplify things as usual. We are starting with the script tag. We opted to use TypeScript using the lang='ts' attribute. We then imported goto to programmatically browse to another page. It was used to progress to auth/confirming route after a user successfully registers. src/routes/auth/confirming/+page.svelte is simple:

<!--src/routes/auth/confirming/+page.svelte-->
<div class="flex items-center justify-center h-[60vh]">
    <div
        class="w-11/12 md:w-2/3 lg:w-1/3 rounded-xl flex flex-col items-center divide-y bg-slate-800 py-4"
    >
        <h1 class="text-center text-2xl font-bold text-emerald-600 mb-2">Email sent!</h1>

        <p class="text-emerald-900 text-justify px-4 py-4">
            Lorem, ipsum dolor sit amet consectetur adipisicing elit. Accusamus autem quod minima deleniti
            esse ratione consectetur, aliquam commodi minus voluptates nobis, ipsam aperiam molestiae,
            quis laboriosam tempore corporis magni ad?
        </p>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

We will change the text in a later article.

We also imported page, a built-in store with all the available data about the current page. One of the data it made available was fetch, an extended version of the fetch API, provided by SvelteKit. I exposed it in src/routes/+layout.ts:

// src/routes/+layout.ts
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async ({ fetch, url }) => {
    return { fetch, url: url.pathname };
};
Enter fullscreen mode Exit fullscreen mode

Any data made available in layout.ts is available to all pages that are a superset of such a layout in SvelteKit. We also brought in three custom stores namely: apiResponse, loading and notification. Store, in Svelte, is a built-in equivalent of Redux in React. It is however simpler:

// frontend/src/lib/stores/api.response.store.ts
import { writable } from 'svelte/store';

export const apiResponse = writable({ message: '', status: '' });

// frontend/src/lib/stores/loading.store.ts

import type { Loading } from '$lib/utils/types';
import { writable, type Writable } from 'svelte/store';

const newLoading = () => {
    const { subscribe, update, set }: Writable<Loading> = writable({
        status: 'IDLE',
        message: ''
    });

    const setNavigate = (isNavigating: boolean) => {
        update(() => {
            return {
                status: isNavigating ? 'NAVIGATING' : 'IDLE',
                message: ''
            };
        });
    };

    const setLoading = (isLoading: boolean, message = '') => {
        update(() => {
            return {
                status: isLoading ? 'LOADING' : 'IDLE',
                message: isLoading ? message : ''
            };
        });
    };

    return { subscribe, update, set, setNavigate, setLoading };
};

export const loading = newLoading();

// frontend/src/lib/stores/notification.store.ts
import { writable } from 'svelte/store';

export const notification = writable({
    message: '',
    borderColor: '',
    textTopColor: '',
    textBottomColor: ''
});
Enter fullscreen mode Exit fullscreen mode

They are all writable stores. This means that we can set, get, and update them. There are other types of Svelte stores. Apart from loading, the stores were created using one-liners. As for loading, it appeared complex because we decided to make available custom methods to interact with the store.

Next, we imported some constants: BASE_API_URI and happyEmoji. BASE_API_URI is the URL of our backend application. It was introduced into our frontend using the environment variable:

// frontend/src/lib/utils/constant.ts
import type { Topic } from './types';

export const BASE_API_URI = import.meta.env.DEV
    ? import.meta.env.VITE_BASE_API_URI_DEV
    : import.meta.env.VITE_BASE_API_URI_PROD;

export const danceEmoji = '💃';
export const angryEmoji = '😠';
export const sadEmoji = '😔';
export const happyEmoji = '😊';
export const thinkingEmoji = '🤔';
export const eyesRoll = '🙄';

export const redColor = '#dc3545';
export const greenColor = '#198754';
export const yellowColor = '#ffc107';
export const cyanColor = '#0dcaf0';

export const topics: Array<Topic> = [
    {
        id: 1,
        title: 'Backend Introduction',
        url: 'https://dev.to/sirneij/full-stack-authentication-system-using-rust-actix-web-and-sveltekit-1cc6'
    },
    {
        id: 2,
        title: 'Database and Redis Configuration',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-db-and-redis-config-38fp'
    },
    {
        id: 3,
        title: 'User Registration',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-user-registration-580h'
    },
    {
        id: 4,
        title: 'User session, Login and Logout',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-login-and-logout-1eb9'
    },
    {
        id: 5,
        title: 'CORS and Frontend Integration',
        url: 'https://dev.to/sirneij/authentication-system-using-rust-actix-web-and-sveltekit-login-and-logout-1eb9'
    }
];
Enter fullscreen mode Exit fullscreen mode

At the top, we dynamically determine our current environment and appropriately set BASE_API_URI. In Vite, your environment variable must start with VITE_ to be recognized. I used this .env:

VITE_BASE_API_URI_DEV=http://127.0.0.1:5000
VITE_BASE_API_URI_PROD=
Enter fullscreen mode Exit fullscreen mode

There are also other constants there. Moving on, I also imported some custom data validation logic which was defined in $lib/utils/helpers/input.validation. Do you wonder where the $lib emanated from? I do too. Well, it turns out in SvelteKit, to prevent ugly import paths, whenever your file lives in src/lib/ directory, it automatically gets the $lib prefix. I use it a lot! The validation logic looks like this:

// frontend/src/lib/utils/helpers/input.validation.ts
/**
 * Validates an email field
 * @file lib/utils/helpers/input.validation.ts
 * @param {string} email - The email to validate
 */
export const isValidEmail = (email: string) => {
    const EMAIL_REGEX =
        /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
    return EMAIL_REGEX.test(email.trim());
};
/**
 * Validates a strong password field
 * @file lib/utils/helpers/input.validation.ts
 * @param {string} password - The password to validate
 */
export const isValidPasswordStrong = (password: string) => {
    const strongRegex = new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})');

    return strongRegex.test(password.trim());
};
/**
 * Validates a medium password field
 * @file lib/utils/helpers/input.validation.ts
 * @param {string} password - The password to validate
 */
export const isValidPasswordMedium = (password: string) => {
    const mediumRegex = new RegExp(
        '^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})'
    );

    return mediumRegex.test(password.trim());
};
Enter fullscreen mode Exit fullscreen mode

Just a file having a lot of regexes to validate the correct email address and detect whether your password is strong or medium in strength!!!

Next, we have a custom post function that abstracts away some stuff:

// frontend/src/lib/utils/requests/posts.requests.ts
import type {
    ApiResponse,
    CustomError,
    LoginRequestBody,
    PasswordChange,
    RegenerateTokenRequestBody,
    RegisterRequestBody,
    User
} from '../types';

/**
 * Handle all POST-related requests.
 * @file lib/utils/requests/post.requests.ts
 * @param {typeof fetch} sveltekitFetch - Fetch object from sveltekit
 * @param {typeof fetch} url - The URL whose resource will be fetched.
 * @param {LoginRequestBody |  RegisterRequestBody | RegenerateTokenRequestBody | FormData |undefined} body - Body of the POST request
 * @param {RequestCredentials} [credentials='omit'] - Request credential. Defaults to 'omit'.
 * @param {'POST' | 'PUT' | 'PATCH' | 'DELETE'} [method='POST'] - Request method. Defaults to 'POST'.
 */
export const post = async (
    sveltekitFetch: typeof fetch,
    url: string,
    body:
        | LoginRequestBody
        | RegisterRequestBody
        | RegenerateTokenRequestBody
        | FormData
        | PasswordChange
        | undefined,
    credentials: RequestCredentials = 'omit',
    method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'POST'
): Promise<[object, Array<CustomError>]> => {
    try {
        const headers = { 'Content-Type': '' };
        const requestInitOptions: RequestInit = {
            method: method,
            mode: 'cors',
            credentials: credentials
        };
        if (!(body instanceof FormData)) {
            headers['Content-Type'] = 'application/json';
            requestInitOptions['headers'] = headers;
            if (body !== undefined) {
                requestInitOptions.body = JSON.stringify(body);
            }
        } else if (body instanceof FormData) {
            headers['Content-Type'] = 'multipart/form-data';
            if (body !== undefined) {
                requestInitOptions['body'] = body;
            }
        } else if (body === undefined && method !== 'DELETE') {
            const errors: Array<CustomError> = [
                { error: 'Unless you are performing DELETE operation, you must have a body.', id: 0 }
            ];
            return [{}, errors];
        }

        const res = await sveltekitFetch(url, requestInitOptions);

        if (!res.ok) {
            const response = await res.json();
            const errors: Array<CustomError> = [];

            errors.push({ error: response.error, id: 0 });

            return [{}, errors];
        }

        const res_json = await res.json();

        let response: ApiResponse | User;

        if (res_json['message']) {
            response = { message: res_json['message'], status: res_json['status'] };
        } else {
            response = {
                id: res_json['id'],
                email: res_json['email'],
                first_name: res_json['first_name'],
                last_name: res_json['last_name'],
                is_staff: res_json['is_staff'],
                thumbnail: res_json['thumbnail'],
                is_superuser: res_json['is_superuser']
            };
        }

        return [response, []];
    } catch (error) {
        console.error(`Error outside: ${error}`);
        const err = `${error}`;
        const errors: Array<CustomError> = [
            { error: 'An unknown error occurred.', id: 0 },
            { error: err, id: 1 }
        ];
        return [{}, errors];
    }
};
Enter fullscreen mode Exit fullscreen mode

We are working with TYPEscript, so the types are expected and they all live in frontend/src/lib/utils/types.ts. Our post uses SvelteKit fetch to perform "data-sensitive" requests: 'POST' | 'PUT' | 'PATCH' | 'DELETE'. It detects the type of body you are sending and serializes them accordingly before performing them. We then use Svelte's built-in animate package to provide a better user experience to our users.

In svelte/sveltekit, you don't need ref, setState, or hooks to bind a variable to an input. You just need to declare the variable and "bind" it to such an input element. For instance, if you have a variable, email, and you want it bounded to an input, just do:

<script lang="ts">
  let email = '';
</script>

<input type="email" bind:value={email}>
Enter fullscreen mode Exit fullscreen mode

And whatever the value a user types in, your email variable holds it automatically!!!

We used this to build out our form and perform some validations. There is a function we need to talk about briefly before we draw the curtains for this article, handleRegister. It is responsible for actually initiating the registration request. It also does some client-side validations.

NOTE: Only client-side validation is a bad idea. Users can turn off JavaScript and they will fail. Ensure you complement such validations with server-side ones. It's important for best practices.

Notice that on the form tag, I put something like:

<form ... on:submit|preventDefault={handleRegister}>
Enter fullscreen mode Exit fullscreen mode

The |preventDefault helps ensure that submitting the form doesn't cause a browser refresh.

If your tailwind configuration was right and you make your routes/+layout.svelte look like the one in the repo, you should see this image:

Registration Page

Did you get here? Congratulations!!! You just made your first real request to the backend!! See you in the next article where we'll enable login and logout in the front-end.

Top comments (0)