Are you struggling with implementing single sign-on (SSO) authentication in Remix? No worries, I’ll guide you through the process of implementing SSO using various OAuth providers and identity providers (IdPs).
I’ve written about SSO in Remix using Clerk and GitHub. In that article, we covered what SSO is and how it can be beneficial to you and your software users. The whole user identity was managed by Clerk, and this is not the case for every kind of software. You may want to manage your users yourself, but still use an OAuth provider (e.g., GitHub) to authenticate your users.
In this article, I’ll skip explaining SSO and go straight to showing you how to use GitHub for authentication, while managing your user data yourself.
To follow along, you’ll need some familiarity with Remix (e.g., loaders, routes and actions) and an understanding of SSO.
Let’s get started!
Create a new Remix app using npx create-remix
. I’ll use JavaScript for this tutorial, but you’re free to create a TypeScript project.
After that’s done, run the following command to install the remix-auth packages.
npm install remix-auth remix-auth-github
Remix Auth is a strategy-based, server-side authentication solution for Remix applications. With TypeScript support, you can implement custom strategies, support persistent sessions and easily handle success and failure scenarios. There are several community-supported strategies for you to use. We’re going to make use of the GitHub Strategy in the preceding examples.
If you want to allow users to sign in using their GitHub profile, you’ll need to create a GitHub OAuth app. You can create and register a GitHub OAuth app under your personal account or under any organization you have administrative access to. Follow these instructions to create an OAuth app.
You will be redirected to the general information page for your OAuth app. Copy the Client ID for later use. You also need a client secret. Click the Generate a new client secret button to generate one.
Copy the secret to a text editor so that you can find it when you need it in the coming sections.
Remix Auth needs a session storage object to store the user’s session. It can be any object that implements the SessionStorage
interface in Remix. Session storage understands how to parse and generate cookies, and
how to store session data in a database or filesystem. Remix comes with several built-in session storage options for common scenarios, and one to create your own:
createCookieSessionStorage
createMemorySessionStorage
createFileSessionStorage
(node)createWorkersKVSessionStorage
(Cloudflare Workers)createArcTableSessionStorage
(architect, Amazon DynamoDB)createSessionStorage
For this example, I’m going to use the createCookieSessionStorage
function. This function is used for purely cookie-based sessions, where the session data itself is stored in the session cookie with the browser.
Create a new file named app/service/session.server.js and paste into the content below.
import { createCookieSessionStorage } from "@remix-run/node";
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
},
});
Now, create a file app/service/auth.server.js for the Remix Auth configuration.
import { GitHubStrategy } from "remix-auth-github";
import { Authenticator } from "remix-auth";
import { sessionStorage } from "./session.server";
let gitHubStrategy = new GitHubStrategy(
{
clientID: "CLIENT_ID",
clientSecret: "CLIENT_SECRET",
callbackURL: "http://localhost:3000/auth/github/callback",
},
async ({ accessToken, extraParams, profile }) => {
// Save/Get the user data from your DB or API using the tokens and profile
return profile;
}
);
export let authenticator = new Authenticator(sessionStorage);
authenticator.use(gitHubStrategy);
Here we import the Authenticator
class and the sessionStorage
object. We created a GitHub strategy and registered it with the authenticator.
The GitHubStrategy
constructor takes a couple of parameters. We pass it the GitHub client ID, secret and callback URL. We also gave it a callback function that will be executed after the user has been authenticated from
GitHub. That function has access to the returned accessToken, params and the user profile data. With such information, you can now create or retrieve user data from your database or wherever you choose to store and manage that data.
Don’t forget to replace the CLIENT_ID and CLIENT_SECRET placeholders with your GitHub OAuth app Client ID and Secret.
We tell the authenticator to use the gitHubStrategy
by calling authenticator.use(gitHubStrategy)
.
Now that we have the strategy registered, let’s move to create the routes.
Create the GitHub authorization callback route by creating a file named app/routes/auth.github.callback.jsx:
import { authenticator } from "../service/auth.server";
export async function loader({ request }) {
return authenticator.authenticate("github", request, {
successRedirect: "/protected",
failureRedirect: "/login",
});
}
We call the authenticator.authenticate()
method with the name of the strategy we want to use, the request
object, and an object with the URLs we want the user to be redirected to after
a success or a failure. The /protected
route will be a page that only authenticated users have access to. We will create that in a moment, but first, let’s handle the login page.
Create a new file routes/login.jsx with the following content:
import { Form } from "@remix-run/react";
export default function Login() {
return (
<Form action="/auth/github" method="post">
<button>Login with GitHub</button>
</Form>
);
}
This page has a basic form that posts to /auth/github
when the form is submitted. You can also create the action in the same /login
route, but I chose to have it separate for this example.
Next, let’s create the /auth/github
route. Create a new file routes/auth.github.jsx and paste the content below into it.
import { redirect } from "@remix-run/node";
import { authenticator } from "../service/auth.server";
export async function loader() {
return redirect("/login");
}
export async function action({ request }) {
return authenticator.authenticate("github", request, {
successRedirect: "/protected",
});
}
This is a resource route that redirects to the /login
route when loaded. The action authenticates the user. If the user is unauthenticated, they’re redirected to GitHub, otherwise, they’re taken to /protected
route.
Now let’s create the /protected
route. Create a new file named routes/protected.jsx, then copy and paste the snippet below into it.
import { json } from "@remix-run/node";
import { authenticator } from "../service/auth.server";
import { useLoaderData, Form } from "@remix-run/react";
export async function loader({ request }) {
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
return json(user);
}
export default function Index() {
const data = useLoaderData();
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome {data.displayName}</h1>
<ul>
<li>You have {data._json.followers} followers</li>
<li>You're following {data._json.following} people</li>
</ul>
<Form action="/logout" method="post">
<button>Logout? Click me</button>
</Form>
</div>
);
}
Here we used the authenticator.isAuthenticated()
method, with the option to redirect to /login
if the user is not authenticated. If they are, we get the user object, which in our case is
the GitHub profile data (recall we returned this data in auth.server.js). We return that data to the client and display some of the data to the user.
We added a form that posts to the /logout
route when it’s clicked. Let’s implement that route.
Create a new file routes/logout.jsx and paste the code snippet below into it.
import { authenticator } from "../service/auth.server";
export async function action({ request }) {
return await authenticator.logout(request, { redirectTo: "/login" });
}
This route logs the user out by calling authenticator.logout()
method. That method destroys the user session and redirects them to the /login
route.
We’re almost done. Let’s add a login button to the Home page. Open the _index.jsx file and add the following markup in between the header element and the unordered list (i.e., after line 10).
<div>
<Form action="/auth/github" method="post">
<button>Login with GitHub</button>
</Form>
</div>
Start up the application using npm run dev
and try it out in your browser.
There you have it. 🚀 In less than 30 minutes, you have a fully protected app with single sign-on done through GitHub as the OAuth server. We used Remix Auth, which provides a comprehensive API for dealing with any form of authentication in Remix. Although we used the GitHub strategy (through the remix-auth-github package), you can create your own strategy and combine multiple strategies in one application.
I hope that was useful for you. Leave a comment if you have any questions, and check out my other posts about SSO in Next.js application.
Here’s the complete code for this post on GitHub.
Peter is a software consultant, technical trainer and OSS contributor/maintainer with excellent interpersonal and motivational abilities to develop collaborative relationships among high-functioning teams. He focuses on cloud-native architectures, serverless, continuous deployment/delivery, and developer experience. You can follow him on Twitter.