Single sign-on (SSO) is an authentication scheme that enables users to authenticate with multiple applications using just one set of credentials (e.g., email and password). You may be familiar with SSO if you use Google services like Docs, Sheets, Drive, etc. You log in to Google Docs and when you visit Drive or Sheets, you’re automatically logged in.
In order words, single sign-on allows a user to log in once and access services/applications without re-entering authentication credentials.
How Does This Work?
SSO is achieved by having a central identity/authentication server through which authentication is performed, and then the session is shared with other domains/services without repeatedly asking for login credentials. Each service/domain will get a separate token or cookie when the users are authenticated. The user won't need to re-enter their login credential as long as there's an authenticated session to the identity server.
In this tutorial, we will implement single sign-on using Clerk, and use GitHub as the OAuth server that'll verify the user’s credentials. We are going to modify an existing application to add protected routes and use the pre-built UI components.
SSO for Remix Apps Using Clerk
Clerk is an SSO provider with integrations that help you securely adopt and implement a single sign-on authentication scheme for your apps in a few minutes (or hours if you have a complex use case). It has integrations for GitHub, Twitter, Apple and many more.
While it's possible to use protocols like OpenID Connect, OAuth and SAML for SSO flow, Clerk uses only the OAuth protocol at the moment. They provide SDKs and UI components to speed up the development time.
Setting Up a Clerk Application
Let's begin by creating a Clerk application.
A Clerk app is a way to isolate the data and configuration for a specific application.
To create the application, go to the Clerk Dashboard and click on the Add application card. You'll be presented with a subset of the available authentication options (you can change them later):
Enter your application name and click the Show more button in the Social Connections section to see the other available SSO providers.
Unselect Google and select GitHub, then click the Finish button at the bottom of the page. The application is created and you're taken to its homepage.
At the top of the page, you should see a menu that reads “Instance,” which can be used to switch between the development and production environment for the application. We'll work on the development environment.
The next step is to connect Clerk with GitHub as an OAuth application. Clerk uses a pre-configured shared OAuth credential and callback URI in development mode, but that's optional. You'll have to add the OAuth credential and redirect URI in the production environment when you need to use it. You're going to use the development environment for this tutorial—therefore, you don't need to create a GitHub OAuth app.
Add the Clerk Client to Your Remix Application
You're going to clone a GitHub repo that'll be used as the boilerplate for our sample application. The repository can be found at https://github.com/pmbanugo/remix-sso-clerk.dev.
You can fork and open the code in GitHub Codespaces, or open your terminal and run the following commands:
git clone https://github.com/pmbanugo/remix-sso-clerk.dev.git
cd remix-sso-clerk.dev
npm install
If you use GitHub CLI, the command
gh repo clone pmbanugo/remix-sso-clerk.dev
would achieve the same for you.
Next, you need to install Clerk's Remix SDK. This gives you access to their pre-built components and React hooks, as well as helpers for Remix loaders. Run the command npm install @clerk/remix
to install the SDK.
Set Environment Variables
The next step is to add environment variables that the SDK will use. To do that, add a new file named .env to your project's root directory. Then add your Clerk keys to the file.
CLERK_PUBLISHABLE_KEY=your_publishable_key
CLERK_SECRET_KEY=your_secret_key
To get the respective secrets, go to the API Keys page in the Clerk dashboard. You'll find different sections for the varying secrets. Copy them and update the values in your .env file.
Configure Clerk Client
Now that you have the environment variables, the next step is to configure the application so that the authentication state can be accessed from any route.
Open the app/root.tsx file. Add the following import statement and a loader.
import type { LoaderFunction } from "@remix-run/node";
import { rootAuthLoader } from "@clerk/remix/ssr.server";
export const loader: LoaderFunction = (args) => rootAuthLoader(args);
Next, remove the export default
keywords from the App
function definition and wrap it with the ClerkApp
higher-order component.
//Add this import statement as well
import { ClerkApp } from "@clerk/remix";
function App() {
return (
<Document>
<Layout>
<Outlet />
</Layout>
</Document>
);
}
export default ClerkApp(App);
Clerk provides a ClerkApp
HOC that provides the authentication state to your React tree. Clerk uses short-lived stateless JWT tokens by default and Remix's CatchBoundary
in app/root.tsx
to refresh expired authentication tokens.
<h4>
Introduction to JSON Web Tokens (JWT)
</h4>
<p>
<a href="https://www.telerik.com/blogs/introduction-json-web-tokens-jwt">Learn more about using JWTs</a> to implement authorization in web applications.
</p>
Rename the existing CatchBoundary()
in app/root.tsx
to CatchAll()
and export a new CatchBoundary
function like so:
// Updated the import statement for ClerkCatchBoundary
import { ClerkApp, ClerkCatchBoundary } from "@clerk/remix";
// renamed to CatchAll
function CatchAll() {
const caught = useCatch();
let message;
//... the rest of the exiting function's logic
}
export const CatchBoundary = ClerkCatchBoundary(CatchAll);
Authenticating Users
Now we get to the interesting part, authenticating users 😎. We're going to use some of the UI components provided by Clerk. You can style them however you want, but for the sake of this tutorial, we will use the default style.
You're not limited to using the provided components. You can also build your own UI components and use the hooks provided in the SDK. You can read more about using hooks in the documentation.
Let's get started with authenticating users 👩🏽💻🔐
We're going to add the <SignInButton />
and <SignOutButton />
components to the navigation bar, which will be used to sign in and sign out the user. While you still have the root.tsx
file open, add the following import statement:
import {
ClerkApp,
ClerkCatchBoundary,
SignedIn,
SignedOut,
SignInButton,
SignOutButton,
} from "@clerk/remix";
Scroll to line 118 where you have list items for the navigation bar, then add the following markup to the list.
<li>
<SignedOut>
<SignInButton mode="modal" />
</SignedOut>
<SignedIn>
<SignOutButton />
</SignedIn>
</li>
The <SignedOut />
component will display its children if the user is signed out, and the <SignedIn />
component does the same but only for logged-in users. When the <SignInButton />
is clicked, it will display a sign-in modal.
Those components will render a button, so you can style it by adding the following CSS to app/styles/global.css
:
button {
background-color: var(--color-links);
border: none;
color: white;
padding: 15px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
Protected Routes
So far, we have used components to show or hide certain UI elements based on the logged-in state. There are cases where we'd like to guard certain routes and redirect unauthenticated users to the sign-in page. You can use the getAuth()
function in your loader or action to check if the returned user is null. If it is, it means the user is signed out.
We're going to add a new /demos/protected
route, which will redirect the user to the sign-in page if they're not already signed in. To implement this, create a new file app/routes/demos/signin.tsx and paste the code below in it.
import { SignIn } from "@clerk/remix";
export default function LogIn() {
return <SignIn />;
}
This will render the <SignIn />
component from Clerk.
Add a new file routes/demos/protected.tsx and paste the following code in it:
import { getAuth } from "@clerk/remix/ssr.server";
import type { LoaderFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const loader: LoaderFunction = async (args) => {
const { userId } = await getAuth(args);
if (!userId) {
return redirect("/demos/signin");
}
return json({ userId });
};
export default function Protected() {
const data = useLoaderData<typeof loader>();
return <h1>You're in! UserId is {data.userId}</h1>;
}
The loader function checks if there's a userId
. If there is, it returns the userId and displays the message. If not, it redirects to the /demos/signin
route.
Let's modify the Home page to include a link to this page. Open app/routes/index.tsx and add the code snippet below, on line 42.
{ to: "demos/protected", name: "Protected route" },
Application Demo
At this point our application is ready and we can try it out. To test it yourself, open the terminal and run npm run dev
. This should start up the dev server. Open localhost:3000 in your browser and try to log in or visit the protected route.
Wrap-up
This post covered what single sign-on is and how to implement it using GitHub and Clerk. Clerk is an SSO provider with integrations that help you securely adopt and implement a single sign-on authentication scheme for your apps in less time.
We used the UI components in this tutorial but you can do the same with your custom UI components and the React hooks from the Clerk SDK. You can read more about using hooks in the SDK reference documentation.
You can find the completed code for this demo on GitHub. If you have any questions, please comment here or reach out to me on Twitter.
Originally posted on Telerik's Blog