web

Remix

Remix

In this quickstart, you are going to build an application with Remix and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web.

Prerequisites

For this Quickstart, you’ll need:

General Architecture

While this sample application doesn't have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.

In that case, the system might look like this before FusionAuth is introduced.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

Getting Started

Start with getting FusionAuth up and running and creating a new Remix application.

Clone The Code

First, grab the code from the repository and change into that folder.

git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web.git
cd fusionauth-quickstart-javascript-remix-web

All shell commands in this guide can be entered in a terminal in this folder. On Windows, you need to replace forward slashes with backslashes in paths.

All the files you’ll create in this guide already exist in the complete-application subfolder, if you prefer to copy them.

Run FusionAuth Via Docker

You'll find a Docker Compose file (docker-compose.yml) and an environment variables configuration file (.env) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

docker compose up -d

Here you are using a bootstrapping feature of FusionAuth called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing docker compose down -v, then re-run docker compose up -d.

FusionAuth will be initially configured with these settings:

  • Your client Id is E9FDB985-9173-4E01-9D73-AC2D60D1DC8E.
  • Your client secret is super-secret-secret-that-should-be-regenerated-for-production.
  • Your example username is richard@example.com and the password is password.
  • Your admin username is admin@example.com and the password is password.
  • The base URL of FusionAuth is http://localhost:9011/.

You can log in to the FusionAuth admin UI and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app. The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the OAuth configuration section.

The .env file contains passwords. In a real application, always add this file to your .gitignore file and never commit secrets to version control.

Create A Basic Remix Application

Next, you’ll set up a basic Remix app. While this guide builds a new Remix project, you can use the same method to integrate your existing project with FusionAuth.

Create the default Remix starter project.

npx create-remix@latest mysite

If create-remix isn’t on your machine, npx will prompt you to install it. Confirm the installation with a y input.

Choose the following options:

  • Just the basics
  • Remix App Server
  • TypeScript
  • Yes (to running npm install)

Add additional npm packages the site will use using the commands below.

cd mysite
npm install dotenv remix-auth remix-auth-oauth2 jwt-decode

The two Remix authentication packages allow you to use FusionAuth without needing to know how OAuth works. The JWT package will provide your app with the user details returned from FusionAuth. The dotenv package allows the app to read configuration details from a .env environment file.

Now create the .env file in your mysite folder and add the following to it (note that this is a different environment file to the one in the root folder used by Docker for FusionAuth).

CLIENT_ID="E9FDB985-9173-4E01-9D73-AC2D60D1DC8E"
CLIENT_SECRET="super-secret-secret-that-should-be-regenerated-for-production"
AUTH_URL="http://localhost:9011/oauth2"
AUTH_CALLBACK_URL="http://localhost:3000/auth/callback"

Preliminary Setup

Before you configure authentication, you need to change the default files created by Remix.

Replace the contents of the file mysite/app/root.tsx with the following code.

import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction, V2_MetaFunction } from "@remix-run/node";
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import css from "~/css/changebank.css";

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  );
}

export const links: LinksFunction = () => [
  ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
  { rel: "stylesheet", href: css },
];

export const meta: V2_MetaFunction = () => {
  return [
    { title: "FusionAuth OpenID and PKCE example" },
  ];
};

This is what these changes do:

  • Add some styling with import css from "~/css/changebank.css"; at the top.
  • Specify the <head> content for the page (title and CSS link) in the two functions at the bottom.

At the top of the file mysite/app/entry.server.tsx, add the following line.

import 'dotenv/config';

This allows any other file run on the server to use settings from .env.

Authentication

Authentication in Remix requires two parts:

  • An Authenticator service to handle communication with FusionAuth.
  • Routes (login, logout, callback) that talk to the Authenticator.

Services

The app folder in the mysite project holds all your code. The routes folder is a reserved word in Remix, and contains your application’s pages. Let’s make a services folder to keep your authentication code separate.

mkdir app/services

In this services folder, add a file called session.server.ts and copy in the following code (this guide follows the example from the Remix Auth README).

import { createCookieSessionStorage } from "@remix-run/node";

// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "_session", // use any name you want here
    sameSite: "lax", // this helps with CSRF
    path: "/", // remember to add this so the cookie will work in all routes
    httpOnly: true, // for security reasons, make this cookie http only
    secrets: ["s3cr3t"], // replace this with an actual secret
    secure: process.env.NODE_ENV === "production", // enable this in prod only
  },
});

// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage;

This code provides a way for Remix to store cookies that maintain the user’s session. (If you’d prefer to use file-based sessions instead of cookie-based, you can read about it in an earlier blog post about using Remix.)

Next, create an authentication file in the services folder, auth.server.ts, that uses the session provider you just created.

import { jwtDecode } from "jwt-decode";
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";
import type { OAuth2StrategyOptions } from "remix-auth-oauth2";
import { OAuth2Strategy } from "remix-auth-oauth2";

type User = string;
export let authenticator = new Authenticator<User>(sessionStorage);

const authOptions: OAuth2StrategyOptions = {
    authorizationURL: `${process.env.AUTH_URL}/authorize`,
    tokenURL: `${process.env.AUTH_URL}/token`,
    clientID: process.env.CLIENT_ID!,
    clientSecret: process.env.CLIENT_SECRET!,
    callbackURL: process.env.AUTH_CALLBACK_URL!,
    scope: 'openid email profile offline_access',
    useBasicAuthenticationHeader: false, // defaults to false
};

const authStrategy = new OAuth2Strategy(
    authOptions,
    async ({accessToken, refreshToken, extraParams, profile, context, request}) => {
        const jwt = await jwtDecode<any>(extraParams?.id_token);
        return jwt?.email || 'missing email check scopes'
    }
);

authenticator.use(
    authStrategy,
    "FusionAuth"
);

In the line new Authenticator<User>, you can specify which user details you want to keep in the session. This example uses a single string containing the user’s email, but you could define an object with more details or just a user Id.

Note that the User must have a value. If the Authenticator returns nothing to store in the session, then a user will not stay logged in.

In authOptions, you specify the FusionAuth URLs stored in the .env file, which match those created by Kickstart when you ran the Docker command to start FusionAuth.

Finally, authStrategy returns the user’s email, decoded from the JWT returned by FusionAuth.

Routes

First replace the homepage of your app in mysite/app/routes/_index.tsx with the following code.

import { Link } from "@remix-run/react";
import type { LoaderFunction } from "@remix-run/node"
import { authenticator } from "~/services/auth.server";

export const loader: LoaderFunction = async ({request}) => {
  let user = await authenticator.isAuthenticated(request, {
      successRedirect: "/account",
  });
  return user;
}

export default function Index() {
  return (
    <div id="page-container">
      <div id="page-header">
        <div id="logo-header">
          <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
          <Link to="/login" className="button-lg">Login</Link>
        </div>

        <div id="menu-bar" className="menu-bar">
          <a className="menu-link">About</a>
          <a className="menu-link">Services</a>
          <a className="menu-link">Products</a>
          <a className="menu-link" style={{textDecorationLine: 'underline'}}>Home</a>
        </div>
      </div>

      <div style={{flex: '1'}}>
        <div className="column-container">
          <div className="content-container">
            <div style={{marginBottom: '100px'}}>
              <h1>Welcome to Changebank</h1>
              <p>To get started, <Link to="/login" >log in or create a new account</Link>.</p>
            </div>
          </div>
          <div style={{flex: '0'}}>
            <img src="/money.jpg" style={{maxWidth: '800px'}}/>
          </div>
        </div>
      </div>
    </div>
  );
}

Remix extends React with a few more custom elements that automatically hide the difference between client and server from the programmer. For example, the line <Link to="/login" className="button-lg">Login</Link> allows Remix to load the linked page in the background without a separate page refresh.

If you have not worked with React before, there are a few differences from normal HTML to note:

  • The class attribute is replaced by className, as class is a reserved word in JavaScript.
  • To add JavaScript values into your HTML, you wrap the value in { }.
  • The style values are not strings but object literals in braces, wrapped in more braces.

The _index.tsx file imports the Authenticator service you created in the previous section, checks if the user has a session, and redirects the user to their profile if they do. This isAuthenticated function does not call FusionAuth but checks the user’s cookie instead.

Note that this code runs in a Remix loader function on the server, not the client. Remember that Remix provides two functions that run only on the server: loader for GET requests and action for POST, PATCH, and UPDATE requests. These functions will always run before any HTML is returned to the client. By contrast, the default function (that returns HTML) is exported from a route and runs only in the browser (unless the user browses directly to the route without already being on the site). Since Remix mixes server-side and client-side code in the same file, it is essential not to put any authentication handling or secrets in the client-side code.

The link you created looks for a login route, so let’s code it. Create the file mysite/app/routes/login.tsx and add the following code to it.

import type { LoaderFunction } from "@remix-run/node"
import { authenticator } from "~/services/auth.server";

export let loader: LoaderFunction = async ({ request }) => {
  return await authenticator.authenticate("FusionAuth", request, {
    successRedirect: "/account",
    failureRedirect: "/",
  });
};

This route is purely server-side, as it exports no default function. Instead, the GET loader function redirects the user to FusionAuth. (The user will automatically be logged in without FusionAuth if they already have a session cookie in their browser).

Now create the file mysite/app/routes/logout.tsx and add the following code to it.

import type { LoaderFunction } from "@remix-run/node"
import { authenticator } from "~/services/auth.server";

export let loader: LoaderFunction = async ({ request }) => {
  await authenticator.logout(request, { redirectTo: "/" });
};

When a user browses to this route, they will be logged out by the Authenticator clearing their session, and redirected to the home page.

The final route to create is mysite/app/routes/auth.callback.tsx.

import type { LoaderFunction } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";

export const loader: LoaderFunction = async ({request}) => {
  await authenticator.authenticate("FusionAuth", request, {
    successRedirect: "/account",
    failureRedirect: "/",
  });
}

This is where FusionAuth is configured to direct the user after authentication. It may look like this code is doing yet another authentication with authenticator.authenticate, but the Authenticator will already have created a session for the user, so no call to FusionAuth will be made here. It is just a way to direct the user to the appropriate page after login.

If you are wondering why auth.callback.tsx has two dots, it’s because Remix routing treats the first dot as a slash in the URL, /auth/callback.

Customization

Now that authentication is done, the last task is to create two example pages that a user can access only when logged in.

Copy over some CSS and an image from the example app.

mkdir app/css
cp ../complete-application/app/css/changebank.css ./app/css/changebank.css
cp ../complete-application/public/money.jpg ./public/money.jpg

Add the account page mysite/app/routes/account.tsx with the following code.

import type { LoaderFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { authenticator } from "~/services/auth.server";

export const loader: LoaderFunction = async ({request}) => {
    const email = await authenticator.isAuthenticated(request, {
        failureRedirect: "/login",
    });
    return email;
}

export default function Account() {
    const email: string = useLoaderData<typeof loader>();
    return (
        <div id="page-container">
            <div id="page-header">
                <div id="logo-header">
                    <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
                    <div className="h-row">
                        <p className="header-email">{email}</p>
                        <Link to="/logout" className="button-lg">Logout</Link>
                    </div>
                </div>

                <div id="menu-bar" className="menu-bar">
                    <Link to="/change" className="menu-link inactive" >Make Change</Link>
                    <Link to="/account" className="menu-link" >Account</Link>
                </div>
            </div>

            <div style={{flex: '1'}}>
                <div className="column-container">
                    <div className="app-container">
                        <h3>Your balance</h3>
                        <div className="balance">$0.00</div>
                    </div>
                </div>
            </div>
        </div>
  );
}

The loader function at the top of the page prohibits the user from viewing this page if they are not authenticated. Any page you create that needs the user to be authenticated requires this function.

The line const email: string = useLoaderData<typeof loader>(); provides whatever data the loader function returns for use in the HTML. It’s used in the line <p className="header-email">{email}</p>.

The final page you need is mysite/app/routes/change.tsx, with the following code.

import type { LoaderFunction } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { useState, ChangeEvent } from "react";
import { authenticator } from "~/services/auth.server";

export const loader: LoaderFunction = async ({request}) => {
    const email = await authenticator.isAuthenticated(request, {
        failureRedirect: "/login",
    });
    return email;
}

export default function Change() {
    const email: string = useLoaderData<typeof loader>();
    const [state, setState] = useState({error: false, hasChange: false, total: '', nickels: '', pennies: ''});

    function onTotalChange(e: ChangeEvent<HTMLInputElement>): void {
        setState({ ...state, total: e.target.value, hasChange: false });
    }

    function makeChange() {
        const newState = { error: false, hasChange: true, total: '', nickels: '', pennies: ''};
        const total = Math.trunc(parseFloat(state.total)*100)/100;
        newState.total = isNaN(total) ? '' : total.toFixed(2);
        const nickels = Math.floor(total / 0.05);
        newState.nickels = nickels.toLocaleString();
        const pennies = ((total - (0.05 * nickels)) / 0.01);
        newState.pennies = Math.ceil((Math.trunc(pennies*100)/100)).toLocaleString();
        newState.error = ! /^(\d+(\.\d*)?|\.\d+)$/.test(state.total);
        setState(newState);
    }

    return (
        <div id="page-container">
            <div id="page-header">
                <div id="logo-header">
                    <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
                    <div className="h-row">
                        <p className="header-email">{email}</p>
                        <Link to="/logout" className="button-lg">Logout</Link>
                    </div>
                </div>

                <div id="menu-bar" className="menu-bar">
                    <Link to="/change" className="menu-link inactive" >Make Change</Link>
                    <Link to="/account" className="menu-link inactive" >Account</Link>
                </div>
            </div>

            <div style={{flex: '1'}}>
                <div className="column-container">
                    <div className="app-container change-container">
                        <h3>We Make Change</h3>

                        { state.error && state.hasChange &&
                            <div className="error-message"> Please enter a dollar amount </div>
                        }

                        { !state.hasChange &&
                            <div className="error-message"><br/> </div>
                        }

                        { !state.error && state.hasChange &&
                            <div className="change-message">
                                We can make change for ${ state.total } with { state.nickels } nickels and { state.pennies } pennies!
                            </div>
                        }

                        <div className="h-row">
                            <form onSubmit={(e) => { e.preventDefault(); makeChange(); }} >
                                <div className="change-label">Amount in USD: $</div>
                                <input className="change-input" name="amount" value={state.total} onChange={onTotalChange} />
                                <input className="change-submit" type="submit" value="Make Change" />
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

This page has identical Remix and authentication functionality to the Account page and some pure React code that updates the DOM whenever the state changes.

Run The Application

Start the Remix server from the root mysite directory.

npm run dev

You can now browse to the app.

Log in using richard@example.com and password. The change page will allow you to enter a number. Log out and verify that you can’t browse to the account page.

Made it this far? Want a free t-shirt? We got ya.

Thank you for spending some time getting familiar with FusionAuth.

*Offer only valid in the United States and Canada, while supplies last.

fusionauth tshirt

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.

FusionAuth Customization

FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:

Security

Tenant and Application Management

Remix Authentication

Troubleshooting

  • I get “This site can’t be reached localhost refused to connect” when I click the login button.

Ensure FusionAuth is running in the Docker container. You should be able to log in as the admin user admin@example.com with a password of password at http://localhost:9011/admin.

  • It still doesn’t work.

You can always pull down a complete running application and compare what’s different.

git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web.git
cd fusionauth-quickstart-javascript-remix-web
docker compose up -d
cd complete-application
npm install
npm run dev