React/Rust + Axum Multi-Tenant Starter App

React Rust/Axum Starter Guide

In this guide, we’ll build an example B2B application in a React-based framework where users can sign up, login, manage their accounts, and view organization and member information using PropelAuth, Next.js, Rust, and Axum.

Example app demonstration

We're going to use the following technologies for this blog post:

Setting up our Authentication UIs

PropelAuth provides UIs for signup, login, and account management, so we don’t need to build them ourselves.

To get started, let’s create a project:

Create Project Menu

We now actually have all the UIs we need to fully test the experience of our users. The preview button lets you view different pages, like the signup page:

Login Menu

For now, though, let’s customize these UIs, which we can do on the Look & Feel section of the dashboard followed by Open Editor:

PropelAuth Customization Menu

There’s a lot more to discover in the PropelAuth dashboard. We can set up social logins, like Login with Google, and enable passwordless logins via Magic Links. We can collect more information on signup like first/last name or accepting terms of service. We can also create and manage organizations, if our users need to work together in groups. But, for now, let’s jump right in to building our frontend.

Authentication in React

Prereqs

  • An application using a React-based framework. We use Next.js Pages Router and Typescript for this guide. If you'd prefer to use Vanilla React or Next.js with App Router, check out our quickstart guide.

Next.js Installation

Start by creating a new Next.js project:

npx create-next-app@latest

For this guide we'll be using TypeScript as well as Pages Router, so make sure to set the following when prompted:

✔ What is your project named? `my-app`
✔ Would you like to use TypeScript? `Yes`
✔ Would you like to use App Router? (recommended) `No`

PropelAuth Installation

We’re going to use PropelAuth's @propelauth/react library which allows us to manage our user’s information. Change directory to your Next.js app and run this command:

npm install @propelauth/react

Run it and update the Dashboard

We are now ready to run the application:

npm run dev

Note down which port the application is running on, and then update the PropelAuth dashboard. We need to configure PropelAuth so that it knows both where the application is running and where to redirect the user back to.

Enter the URL the frontend is running on in the Primary Frontend Location section under Frontend Integration. For our example, this is http://localhost:3000

Frontend Location Menu

You’ll also want to copy the Auth URL, as we’ll use it in the next step.

authURL example

Set up the AuthProvider

The AuthProvider is responsible for checking if the current user is logged in and fetching information about them. You should add it to the top level of your application so it never unmounts. In Next.js, this is the _app.tsx file:

_app.tsx

import type {AppProps} from 'next/app'
import {AuthProvider} from "@propelauth/react";

export default function App({Component, pageProps}: AppProps) {
    return <AuthProvider authUrl="ENTER_YOUR_AUTH_URL_HERE">
        <Component {...pageProps} />
    </AuthProvider>
}

Displaying user information

And now, the moment you’ve been waiting for! Let’s display the user’s email address via the React hook useAuthInfo . We’ll replace our index.tsx file with this:

index.tsx

import {useAuthInfo} from "@propelauth/react";

export default function Home() {
    const {loading, isLoggedIn, user} = useAuthInfo()
    if (loading) {
        return <div>Loading...</div>
    } else if (isLoggedIn) {
        return <div>Logged in as {user.email}</div>
    } else {
        return <div>Not logged in</div>
    }
}

And…

Not logged in example

Oh, we’re not logged in yet. We could navigate directly to our Auth URL and login, but let’s instead add a few links to our application.

Small note: withAuthInfo and withRequiredAuthInfo

useAuthInfo is great, but it does mean you have to check the loading state often. PropelAuth also provides a higher order component withAuthInfo which won’t render the underlying component until loading is finishing, making the code a bit cleaner:

import {withAuthInfo} from "@propelauth/react";

export default withAuthInfo(function Home({isLoggedIn, user}) {
    if (isLoggedIn) {
        return <div>Logged in as {user.email}</div>
    } else {
        return <div>Not logged in</div>
    }
})

Similarly, if you know the user is logged in, you can use withRequiredAuthInfo which can make it even cleaner:

import {withRequiredAuthInfo} from "@propelauth/react";

// By default, will redirect the user to the login page if they aren't logged in
export default withRequiredAuthInfo(function Home({user}) {
    return <div>Logged in as {user.email}</div>
})

You can read more about this here.

Adding Login/Account/Logout Buttons to our application

The @propelauth/react library also provides React hooks useRedirectFunctions and useLogoutFunction which provide easy ways for us to send the user to the hosted pages (like login, signup, account, etc) or log the user out entirely. Let’s update our index.tsx file:

index.tsx

import {withAuthInfo, useLogoutFunction, useRedirectFunctions} from "@propelauth/react";

export default withAuthInfo(function Home({isLoggedIn, user}) {
    const {redirectToLoginPage, redirectToAccountPage} = useRedirectFunctions()
    const logoutFn = useLogoutFunction()

    if (isLoggedIn) {
        return <>
            <div>Logged in as {user.email}</div>
            <div>
                <button onClick={() => redirectToAccountPage()}>Account</button>
                <button onClick={() => logoutFn(false)}>Logout</button>
            </div>
        </>
    } else {
        return <>
            <div>Not logged in</div>
            <button onClick={() => redirectToLoginPage()}>Login</button>
        </>
    }
})

Login button when not logged in

And now our users have an easy way to login, view their account information, and logout.

Account and logout button when logged in

Sending Requests from Frontend to Backend

Prereqs:

  • To make a request from Next.js to a backend running on a different localhost port, you’ll need to handle CORS issues.

For Next.js, we handle CORS locally by adding a rewrite for all /api/* routes to our backend on port 3001. This is our next.config.mjs:

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://127.0.0.1:3001/api/:path*'
      }
    ]
  }
}

export default nextConfig;

Start with an unauthenticated HTTP request

In our Next.js application, let's make a new file requestdemo.tsx that uses useEffect to make an HTTP request to our backend:

requestdemo.tsx

import {useEffect, useState} from "react";
import {withAuthInfo} from "@propelauth/react";

const fetchFromApi = async () => {
    const response = await fetch("/api/whoami", {
        headers: {
            "Content-Type": "application/json"
        }
    });
    if (response.ok) {
        return response.json();
    } else {
        return {status: response.status};
    }
}

export default withAuthInfo(function RequestDemo() {
    const [response, setResponse] = useState(null);

    useEffect(() => {
        fetchFromApi().then((data) => setResponse(data));
    }, [])

    if (response) {
        return <pre>{JSON.stringify(response, null, 2)}</pre>
    } else {
        return <div>Loading...</div>
    }
})

When we navigate to the /requestdemo page we get a 500 error! The reason? We don't have a backend yet - let's fix that.

500 error

Authentication in Rust/Axum

Setting Up the Project

If you don’t have a project yet, you can create one with:

cargo new

We then need to set up our dependencies in our Cargo.toml file to include axum, tokio, serde_json, and PropelAuth. We'll also enable the axum07 feature for the propelauth create, which provides us with utilities specific to axum.

Cargo.toml

[dependencies]
propelauth = { version = "^0.14.0", features = ["axum07"] }
serde_json = "^1.0"
axum = { version = "0.7.5" }
tokio = { version = "1.38.1" }

Creating an unprotected route

Let’s now create the route that our frontend is trying to make a request to:

main.rs

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let app = Router::new()
        .route("/api/whoami", get(whoami))

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn whoami() -> Json<Value> {
    let response = json!({ "Hello": "World" });
    Json(response)
}

If we run the application:

cargo run

And then navigate back to the /requestdemo page:

Hello world response

We have now made a request to our backend! The problem? whoami is currently accessible to anyone. Let's protect the endpoint using PropelAuth.

Protecting our unprotected route

To protect our route, we first need to Initialize our PropelAuth crate. We’ll perform a one time fetch to grab our PropelAuth Authentication URL and API Key (found in the Backend Integration section of your dashboard), which we will use to validate tokens from the frontend.

We’ll also add PropelAuthLayer to our Router. Layers in Axum are similar to middleware in other frameworks. The PropelAuth crate provides a Layer which allows you to validate access tokens sent from our frontend.

main.rs

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let auth_url = "YOUR_AUTH_URL".to_string()
    let api_key = "YOUR_API_KEY".to_string()

    let auth = PropelAuth::fetch_and_init(AuthOptions { auth_url, api_key })
        .await
        .expect("Unable to initialize authentication");

    let auth_layer = PropelAuthLayer::new(auth);

    let app = Router::new()
        .route("/api/whoami", get(whoami))
        .layer(auth_layer);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

async fn whoami(user: User) -> Json<Value> {
    let response = json!({ "user_id": user.user_id });
    Json(response)
}

We can now take User in as an argument, which will look for an access token in the Authorization header. If one isn’t provided, or if it is invalid/expired, the request is automatically rejected.

And now when we visit the /requestdemo page again, we instead get a 401 Unauthorized error!

401 error

Why did this happen? Well, we didn’t pass in any information from the frontend to indicate who we are. Let’s go back to the frontend, and see how to make an authenticated HTTP request.

Making an Authenticated HTTP request

Similar to user or isLoggedIn , one of the other values that we receive from useAuthInfo, withAuthInfo, and withRequiredAuthInfo is an accessToken.

This accessToken is unique to our user, and is what our backend is checking when it says we are authorized or unauthorized. We need to pass this in to our backend via an Authorization header, like so:

Authorization: Bearer ${accessToken}

Here’s that same page from the last section, but this time it will pass the access token to the backend:

requestdemo.tsx

import {useEffect, useState} from "react";
import {withAuthInfo} from "@propelauth/react";

const fetchFromApi = async (accessToken: string | null) => {
    const response = await fetch("/api/whoami", {
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${accessToken}`
        }
    });
    if (response.ok) {
        return response.json();
    } else {
        return {status: response.status};
    }
}

export default withAuthInfo(function RequestDemo({accessToken}) {
    const [response, setResponse] = useState(null);

    useEffect(() => {
        fetchFromApi(accessToken).then((data) => setResponse(data));
    }, [])

    if (response) {
        return <pre>{JSON.stringify(response, null, 2)}</pre>
    } else {
        return <div>Loading...</div>
    }
})

And now, as long as we are logged in, we see:

Success Message

Organization Information

A common use case for B2B applications is the ability to separate users into organizations or teams. For the purposes of this application build, we will add functionality to view the list of organizations that the current user is a part of, and to view other members of that organization.

Display Organizations

First, we’ll create a orgs.tsx page that will list all of the organizations a user is a part of, or display a button to redirect to the organization create/invitation page if they are not a part of any organizations.

orgs.tsx

import {useRedirectFunctions, WithLoggedInAuthInfoProps, withRequiredAuthInfo, OrgMemberInfoClass} from "@propelauth/react";
import Link from 'next/link';

interface OrgProps {
    orgs: OrgMemberInfoClass[]
}

function NoOrganizations() {
    const {redirectToCreateOrgPage} = useRedirectFunctions()
    return <div>
        You aren't a member of any organizations.<br/>
        You can either create one below, or ask for an invitation.<br/>
        <button onClick={() => redirectToCreateOrgPage()}>
            Create an organization
        </button>
    </div>
}

function ListOrganizations({orgs}: OrgProps) {
    return <>
        <h3>Your organizations</h3>
        <ul>
            {orgs.map(org => {
                return <li key={org.orgId}>
                    <Link href={`/org/${org.orgId}`}>
                        {org.orgName}
                    </Link>
                </li>
            })}
        </ul>
    </>
}

function ListOfOrgs(props: WithLoggedInAuthInfoProps) {
    const orgs = props.userClass.getOrgs()
    if (orgs.length === 0) {
        return <NoOrganizations />
    } else {
        return <ListOrganizations orgs={orgs}/>
    }
}

// By default, if the user is not logged in they are redirected to the login page
export default withRequiredAuthInfo(ListOfOrgs);

The UserClass allows us to easily get all the organizations that the user is a member of, and by passing this array into our helper component ListOrganizations, we can map over each organization and create a new Link and dynamic route to display relevant information.

Let's add a button to our index.tsx page so we can easily navigate to our new page.

index.tsx

<Link href="/orgs">
    <button>Org List</button>
</Link>

When we navigate to the new page, we'll either see a button to redirect the user to create an org or a list of orgs that the user belongs to.

org list

Next, create a new folder called org and create a file called [orgId].tsx inside of it. We'll follow the same pattern we did with requestdemo.tsx, using React's useEffect hook to send a fetch to a new route, org/orgId. Since the orgs.tsx page links to /org/{orgId}, we'll use useRouter to get the orgId from the URL.

/org/[orgId].tsx

import { withRequiredAuthInfo, WithLoggedInAuthInfoProps } from '@propelauth/react';
import { useRouter } from 'next/router';
import { useEffect, useState } from "react";

function fetchOrgInfo(orgId: String, accessToken: String) {
  return fetch(`/api/org/${orgId}`, {
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${accessToken}`
    }
  }).then(response => {
    if (response.ok) {
      return response.json()
    } else {
      return { status: response.status }
    }
  })
}

function OrgInfo({ accessToken }: WithLoggedInAuthInfoProps) {
  const router = useRouter();
  const orgId = router.query.orgId;
  const [response, setResponse] = useState(null)

  useEffect(() => {
    fetchOrgInfo(orgId? orgId.toString() : "", accessToken).then(setResponse)
  }, [orgId, accessToken])

  return <div>
    <p>{response ? JSON.stringify(response) : "Loading..."}</p>
  </div>
}

export default withRequiredAuthInfo(OrgInfo);

We then need to create the Axum route to return an authenticated response, so in our main.rs file we’ll add a new route:

main.rs

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let auth_url = "YOUR_AUTH_URL".to_string()
    let api_key = "YOUR_API_KEY".to_string()

    let auth = PropelAuth::fetch_and_init(AuthOptions { auth_url, api_key })
        .await
        .expect("Unable to initialize authentication");

    let auth_layer = PropelAuthLayer::new(auth.clone());

    let app = Router::new()
        .route("/api/whoami", get(whoami))
        .route("/api/org/:org_id", get(org_members))
        .layer(auth_layer)
        .layer(auth_layer)
        .layer(Extension(auth.clone()));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Next, we’ll create a handler. This handler will specifically check that the user is in the org_id in the path. We also don’t have any requirements for the user within this organization, although you can also check that the user has a specific role/permission.

main.rs

async fn org_members(
    user: User,
    Extension(auth): Extension<Arc<PropelAuth>>,
    Path(org_id): Path<String>,
) -> Result<Json<UserPagedResponse>, UnauthorizedOrForbiddenError> {
    let org =
        user.validate_org_membership(RequiredOrg::OrgId(&org_id), UserRequirementsInOrg::None)?;
    let org_members = &auth.org().fetch_users_in_org(
        FetchUsersInOrgParams  {
            org_id: org_id.to_string(),
            include_orgs: Some(false),
            role: None,
            page_size: Some(5),
            page_number: Some(0)
        }
    ).await
    .expect("Failed to fetch org members");
    
    Ok(Json(org_members.clone()))
}

When we click on one of our org links, we'll now see a JSON blob of the users who belong to that org.

users in org

We’ll leave it up to you to make it look good :)

Wrapping Up

This guide provides a comprehensive look at an authentication application framework you can use to get started. By using PropelAuth for authentication, we were able to skip building any auth UIs, transactional emails, invitation flows, and more.

Our frontend made an authenticated request to our backend, and our backend was able to identify the user that made the request. You can use this for things like saving information in a database per user_id.

If you have any questions, please reach out at support@propelauth.com.