web
Remix
In this quickstart, you will build an application with Remix and integrate it with FusionAuth. The application is for ChangeBank, a global leader in converting dollars into coins. It will have areas reserved for logged in users and public-facing sections.
Find the Docker Compose file and source code for the complete application at https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web.
Prerequisites
For this Quickstart, you’ll need:
- Node 18 or later.
- Docker, which is the quickest way to start FusionAuth. (There are other ways).
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.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you would introduce FusionAuth to normalize and consolidate user data, making it consistent and up-to-date and 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 folder of the repository.
Assuming you have Docker installed on your machine, you can start FusionAuth on your machine with the following.
docker compose up -d
This will start three containers, one each for FusionAuth, Postgres, and Elasticsearch.
Here you are using a bootstrapping feature of FusionAuth, called Kickstart. When FusionAuth starts 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 system, delete the volumes created by docker compose
by executing docker compose down -v
, then rerun docker compose up -d
.
FusionAuth will be 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 ispassword
. - Your admin username is
admin@example.com
and the password ispassword
. - 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.
The .env
and kickstart.json
files contain passwords. In a real application, always add these files 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 jwt_decode from "jwt-decode";
import { Authenticator } from "remix-auth";
import { sessionStorage } from "~/services/session.server";
import { OAuth2Strategy, OAuth2StrategyOptions } 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!,
useBasicAuthenticationHeader: false, // defaults to false
};
const authStrategy = new OAuth2Strategy(
authOptions,
async ({accessToken, refreshToken, extraParams, profile, context, request}) => {
type Token = { email: string }
const token: Token = jwt_decode(accessToken);
return token.email;
}
);
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/assets/img/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 byclassName
, asclass
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/assets/img/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/assets/img/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.
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:
- Hosted pages such as login, registration, email verification, and many more.
- Email templates.
- User data and custom claims in access token JWTs.
Security
- You may want to customize the token expiration times and policies in FusionAuth.
- Choose password rules and a hashing algorithm that meet your security needs.
Tenant And Application Management
- Model your application topology using Applications, Roles, Groups, Entities, and more.
- Set up MFA, Social login, or SAML integrations.
- Integrate with external systems using Webhooks, SCIM, and Lambdas.
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