React/Rust + Axum Multi-Tenant Starter App

React/Rust + Axum Multi-Tenant Starter App

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

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

The code is available on GitHub in these two Github Repos

Setting Up Authentication Pages

Before creating our application, we'll first be setting up our project in PropelAuth. Projects in PropelAuth provide all of the necessary components of authentication for your applications, including hosted login pages, which we'll be using for this walkthrough. You can also set up additional features such as SAML, social logins/SSO, and more. For more information on how to add these options, be sure to check out our documentation.

The first step is to create a project in PropelAuth.

Once a project has been created, you will be directed to the dashboard for your project, outlining the steps to complete adding authentication into your application.

The first step is to customize the look of your hosted authentication pages. By clicking "View," you are redirected to this page:

https://cdn.getmidnight.com/a1241f0fcb8d83a4c0387f234e241914/2023/01/image-2.png

Here you can adjust the look of all hosted authentication pages to best match your preferred branding and style guidelines. Feel free to make any changes, click "Save," and navigate back to the dashboard using the left sidebar. Now that we've changed the look of our pages, click "Mark As Done," and move on to the next step.

From here, you can configure other aspects of your end-users auth experience, including:

  • Adding "Login in with Google" or other SSO providers
  • Collecting additional metadata on signup - like username or first name/last name
  • Allowing your users to upload their own profile picture
  • Letting your end-users create organizations and invite their coworkers (called B2B support)

For now, we are going to click "Mark As Done" on step 2, "Add Social Logins," and move on to step 3.

Sign Up As A Test User

Under step 3, click "View," and a new tab should open with the authentication page you configured above. Sign up as if you are a user, exit the tab, and mark the step as done.

https://cdn.getmidnight.com/a1241f0fcb8d83a4c0387f234e241914/2023/01/image-3.png

Create a React Project

If you don't have a project already, you can create one with:

$ npx create-react-app frontend

Next, install the @propelauth/react package. It provides an easy interface to access your users information. It will manage auth tokens for you and has nice features like refreshing auth information when the user reconnects to the internet or switches back to your tab.

$ npm install @propelauth/react

Finally, we’ll install react-router to use switching between pages in our application.

$ npm install react-router-dom@6

Frontend

In this section, we will be setting up the frontend sections of our React application, walking through how to integrate PropelAuth into your components as we go.

Set Up Auth Provider

First, navigate to the index.js file in your application. Here we are going to add our AuthProvider and BrowserRouter.

import { AuthProvider } from '@propelauth/react';
import {BrowserRouter} from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AuthProvider authUrl={process.env.REACT_APP_PROPELAUTH_AUTH_URL}>
      <BrowserRouter>
        <App/>
      </BrowserRouter>
    </AuthProvider>
  </React.StrictMode>
);

The authUrl is available on the Frontend Integration section of your PropelAuth project, or step 4 in your dashboard.

BrowserRouter will manage our URL history while switching pages in our application. AuthProvider is the provider of a React context that manages the current user’s access token and metadata, and all other components pull information from it. We are placing it at the top level of our application so that it never unmounts.

Get Logged In Status

Next, in the App.js, we'll bring in the withAuthInfo function, which injects a user's information into a React component. In our project, we will be displaying different components depending on whether or not the user is logged in.

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

const App = withAuthInfo(({isLoggedIn}) => {
  if (isLoggedIn) {
      return <div>
          The User is logged in
      </div>
  } else {
      return <div>
          The User is logged out
      </div>
  }
})

export default App;

Using the code above, if the user is logged in they should see the following:

Create Sign In/SignOut Buttons

@propelauth/react also provides React hooks for redirecting your users to the hosted login/signup/account pages you created in your PropelAuth project, or logging your users out. Let's add them to our App.js file.

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

const App = withAuthInfo(({isLoggedIn}) => {
  const logoutFn = useLogoutFunction()
  const {redirectToSignupPage, redirectToLoginPage} = useRedirectFunctions();
    
  if (isLoggedIn) {
      return <div>
          <p>The User is logged in</p>
          <button onClick={() => logoutFn(true)}>
              Click here to log out
          </button>
      </div>
  } else {
      return <div>
          To get started, please log in as test user.
          <br/>
          <button onClick={() => redirectToSignupPage()}>
              Sign up
          </button>
          <button onClick={() => redirectToLoginPage()}>
              Log in
          </button>
      </div>
  }
})

Now, if a user is logged out, they will see the following:

And if they are logged in, they will see:

Display User Information

The next step is to build out our routes, and create a route and component that will display the currently logged in user’s information. First, import Route and Routes from react-router-dom into App.js. Next, create components/Home.jsx and components/UserInfo.jsx, which will serve as our home page component and our user information route, respectively.

In Home.jsx, we’ll import withRequiredAuthInfo, which is identical to withAuthInfo but the component will not be rendered if the user is not logged in, and instead will default to redirecting to the hosted sign up page unless otherwise specified. We’ll also import Link from react-router-dom. Finally, build out the rest of the component using Link:

import {withRequiredAuthInfo} from "@propelauth/react";
import {Link} from "react-router-dom";

function Home(props) {
   return <div>
        <Link to="/user_info">
            Click Here to see user info
        </Link>
   </div>
}

export default withRequiredAuthInfo(Home);

In UserInfo.jsx, we'll create a component to display user information that pulls the user object that is injected automatically from withAuthInfo.

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

function UserInfo({user}) {

    return <span>
        <h2>User Info</h2>
        {user && user.pictureUrl && <img src={user.pictureUrl} alt={"profile"} className="pictureUrl" />}
        <pre>user: {JSON.stringify(user, null, 2)}</pre>
    </span>
}

export default withRequiredAuthInfo(UserInfo);

Finally, in App.js, we’ll set up our routes for the Home and User Info componentsin the isLoggedIn check:

if (isLoggedIn) {
      return <div>
          <p>The User is logged in</p>
          <button onClick={() => logoutFn(true)}>
              Click here to log out
          </button>
          <Routes>
            <Route exact path="/" element={<Home/>}/>
            <Route path="/user_info" element={<UserInfo/>}/>
          </Routes>
      </div>
  }

User’s should now be able to view their information if they are logged in and click the link on the Home component.

Sending Requests from Frontend to Backend

Up to this point, we have stayed on the frontend components of our application, but next, we will be making requests to a protected backend.

Making Authenticated Requests

To make an authenticated request on behalf of your user, you’ll need to provide an access token. Just like isLoggedIn and user, the access token is available from withAuthInfo. You provide it in the request in the Authorization header, like so:

Authorization: Bearer ACCESS_TOKEN

With fetch, this looks like:

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

We can add this to a new components/AuthenticatedRequest.jsx component using React's useEffect hook.

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

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

function AuthenticatedRequest({accessToken}) {
    const [response, setResponse] = useState(null);
    useEffect(() => {
        fetchWhoAmI(accessToken).then(setResponse)
    }, [accessToken])

    return <span>
        <h2>Server Response</h2>
        <pre>{response ? JSON.stringify(response, null, 2) : "Loading..."}</pre>
    </span>
}

export default withRequiredAuthInfo(AuthenticatedRequest);

A Quick Note On CORS

Our React application runs on port 3000, so we’ll need to run our backend on a different port (in this tutorial, we use 3001). For security reasons, browsers will not allow you to make requests from one domain to another, and http://localhost:3000 and http://localhost:3001 are considered different domains.

A simple way to fix this issue is to add the following to your package.json:

"proxy": "http://127.0.0.1:3001/"

This will automatically proxy certain requests (like JSON requests) to http://localhost:3001. For more information, see the official React docs.

Great, we can now make authenticated requests to any backend we want. The only problem? We don't have a backend yet - let's fix that.

Authentication in Rust/Axum

Setting Up the Project

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

$ cargo new

Afterwards, we need to set up our dependencies, so in our Cargo.toml we will include axum, tokio, dotenv, serde, and propelauth. Make sure to enable the axum feature for the propelauth crate, which provides us with utilities specific to axum.

[dependencies]
axum = "0.6.2"
dotenv = "0.15.0"
propelauth = { version = "0.1.0", features = ["axum"] }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.25.0", features = ["full"] }

Creating our protected route

First, we need to initialize the 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.

#[tokio::main]
async fn main() {
    // Load variables from .env
    dotenv::dotenv().ok();
    let auth_url = std::env::var("PROPELAUTH_AUTH_URL")
        .expect("Couldn't find env variable PROPELAUTH_AUTH_URL");
    let api_key =
        std::env::var("PROPELAUTH_API_KEY").expect("Couldn't find env variable PROPELAUTH_API_KEY");

    // Initialize our crate. This performs a one time fetch to get the public key, which
    //   the library uses to validate tokens
    let auth = PropelAuth::fetch_and_init(AuthOptions { auth_url, api_key })
        .await
        .expect("Unable to initialize authentication");
}

Next, we’ll add PropelAuthLayer to your 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, as we’ll see in the next code snippet.

let auth_layer = PropelAuthLayer::new(auth);

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

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.

// User will automatically return a 401 (Unauthorized) if a valid access token wasn't provided
async fn whoami(user: User) -> Json<User> {
    Json(user)
}

You can run the code with:

$ cargo run

Your requests from the browser should succeed when you are logged in and 401 when you are logged out.

Now that we have a backend running, we can now include our components/AuthenticatedRequest.jsx in a new Route on App.js:

<Route path="/auth" element={<AuthenticatedRequest/>}/>

And if we add a new link to components/Home.jsx:

<Link to="/auth">
    Click Here to see an authenticated request to the backend
</Link>

If the user is logged in, they should see the server response from our authenticated request:

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 components/ListOfOrgs.jsx 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.

import {useRedirectFunctions, withRequiredAuthInfo} from "@propelauth/react";
import {Link} from "react-router-dom";

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}) {
    return <>
        <h3>Your organizations</h3>
        <ul>
            {orgs.map(org => {
                return <li key={org.orgId}>
                    <Link to={`/org/${org.urlSafeOrgName}`}>
                        {org.orgName}
                    </Link>
                </li>
            })}
        </ul>
    </>
}

function ListOfOrgs(props) {
    const orgs = props.orgHelper.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);

orgHelper 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.

Next, we’ll create components/OrgInfo.jsx and, following the same pattern we did with AuthenticatedRequest.jsx, we’ll use React's useEffect hook to send a fecth to a new route, org/orgId.

import {withAuthInfo} from '@propelauth/react';
import {useParams} from "react-router-dom";
import { useEffect, useState } from "react";

function fetchOrgInfo(orgId, accessToken) {
    return fetch(`/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({ orgHelper, accessToken }) {
    const {orgName} = useParams();
    const orgId = orgHelper.getOrgByName(orgName).orgId

    const [response, setResponse] = useState(null)
    
    useEffect(() => {
        fetchOrgInfo(orgId, accessToken).then(setResponse)
    }, [orgId, accessToken])

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

export default withAuthInfo(OrgInfo);

Axum Route

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

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

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.

// If the user isn't in the provided organization, a 403 is returned
async fn org_whoami(
    user: User,
    Path(org_id): Path<String>,
) -> Result<Json<OrgMemberInfo>, UnauthorizedOrForbiddenError> {
    let org =
        user.validate_org_membership(RequiredOrg::OrgId(&org_id), UserRequirementsInOrg::None)?;
    Ok(Json(org))
}

This will validate whether a given user is in an organization specified by the given org_id.

Finally we need to display the new routes in our application. In App.js we need to import and create new Route paths for ListOfOrgs.jsx and OrgInfo.jsx

<Route path="/orgs" element={<ListOfOrgs/>}/>
<Route path="/org/:orgName" element={<OrgInfo/>}/>

And on Home.jsx we need to add a new Link to the path /orgs to display ListOfOrgs.jsx

<Link to="/orgs">
    Click Here to see org info
</Link>

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

Wrapping Up

This guide provides 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.