other ways).
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 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.
Start with getting FusionAuth up and running and creating a new Remix application.
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.
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:
E9FDB985-9173-4E01-9D73-AC2D60D1DC8E
.super-secret-secret-that-should-be-regenerated-for-production
.richard@example.com
and the password is password
.admin@example.com
and the password is password
.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.
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:
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"
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:
import css from "~/css/changebank.css";
at the top.<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 in Remix requires two parts:
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.
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:
class
attribute is replaced by className
, as class
is a reserved word in JavaScript.{ }
.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
.
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.
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.
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 gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:
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.
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