Integrate Hanko with Remix
Learn how to quickly add authentication and user profile in your Remix app using Hanko.
Install @teamhanko/hanko-elements
Once you’ve initialized your Remix app, installing hanko-elements provides you with access to the prebuilt components: hanko-auth
and hanko-profile
.
Add the Hanko API URL
Retrieve the API URL from the Hanko console and place it in your .env
file.
HANKO_API_URL=https://f4****-4802-49ad-8e0b-3d3****ab32.hanko.io
If you are self-hosting you need to provide the URL of your running Hanko backend.
Importing and exporting Hanko functions in a .client.ts
file
Since the @teamhanko/hanko-elements
package is designed to work on the client side, you’ll need to import the Hanko functions into a .client.ts
file. This file will then export them, making them available for use throughout your application.
import { register, Hanko } from "@teamhanko/hanko-elements";
export { register, Hanko };
Add <hanko-auth>
component
The <hanko-auth>
web component adds a login interface to your app. The following code example defines a HankoAuth
component in the login
route.
It uses the register function from hanko.client.ts
to register <hanko-auth>
component with the brower’s CustomElementRegistry. When the authentication flow is completed, a callback is set up to redirect the user to the dashboard.
import { useNavigate, useLoaderData } from "@remix-run/react";
import { Suspense, useCallback, useEffect, useState } from "react";
import { register, type Hanko } from "~/utils/hanko.client";
export const loader = () => {
return { hankoUrl: process.env.HANKO_API_URL };
};
const HankoAuth = () => {
const [hanko, setHanko] = useState<Hanko>();
const navigate = useNavigate();
const data = useLoaderData<typeof loader>();
const hankoUrl = data.hankoUrl || '';
const redirectAfterLogin = useCallback(() => {
navigate("/dashboard");
}, [navigate]);
useEffect(() => {
if (hanko) {
hanko.onSessionCreated(() => {
redirectAfterLogin();
});
}
}, [hanko, redirectAfterLogin]);
useEffect(() => {
register(hankoUrl)
.catch((error: Error) => {
console.error(error.message);
})
.then((result) => {
if (result) {
setHanko(result.hanko);
}
});
}, [hankoUrl]);
return (
<div className="">
<Suspense fallback={"Loading..."}>
<hanko-auth />
</Suspense>
</div>
);
};
const Login = () => {
return (
<div>
<HankoAuth />
</div>
)
}
export default Login
By now, your sign-up and sign-in features should be working. You should see an interface similar to this 👇
Add <hanko-profile>
component
The <hanko-profile>
component provides an interface, where users can manage their email addresses and passkeys.
import { useLoaderData, useNavigate } from "@remix-run/react";
import { Suspense, useEffect, useState } from "react";
import { type Hanko, register } from "~/utils/hanko.client";
export const loader = () => {
return { hankoUrl: process.env.HANKO_API_URL };
};
function HankoProfile() {
let data = useLoaderData<typeof loader>();
let hankoUrl = data.hankoUrl || '';
const [hanko, setHanko] = useState<Hanko>();
const navigate = useNavigate();
useEffect(() => {
register(hankoUrl)
.catch((error: Error) => {
console.error(error.message);
})
.then((result) => {
if (result) {
setHanko(result.hanko);
}
});
}, [hankoUrl]);
return (
<div className="">
<Suspense fallback={"Loading..."}>
<hanko-profile />
</Suspense>
</div>
)
}
const Dashboard = () => {
return (
<div>
<HankoProfile />
</div>
)
}
export default Dashboard
It should look like this 👇
Implement logout functionality
You can use @teamhanko/hanko-elements
to easily manage user logouts. Below we add a logout button to the dashboard page.
import { useLoaderData, useNavigate } from "@remix-run/react";
import { Suspense, useEffect, useState } from "react";
import { type Hanko, register } from "~/utils/hanko.client";
export const loader = () => {
return { hankoUrl: process.env.HANKO_API_URL };
};
function HankoProfile() {
let data = useLoaderData<typeof loader>();
let hankoUrl = data.hankoUrl || '';
const [hanko, setHanko] = useState<Hanko>();
const navigate = useNavigate();
useEffect(() => {
register(hankoUrl)
.catch((error: Error) => {
console.error(error.message);
})
.then((result) => {
if (result) {
setHanko(result.hanko);
}
});
}, [hankoUrl]);
// here we define the logout function
const logout = async () => {
try {
await hanko?.user.logout();
navigate("/login");
return;
} catch (error) {
console.error("Error during logout:", error);
}
};
return (
<div className="">
<Suspense fallback={"Loading..."}>
<hanko-profile />
</Suspense>
<div>
<button onClick={logout}>Logout</button>
</div>
</div>
)
}
const Dashboard = () => {
return (
<div>
<HankoProfile />
</div>
)
}
export default Dashboard
Customize component styles
You can customize the appearance of hanko-auth
and hanko-profile
components using CSS variables and parts. Refer to our customization guide.
Securing routes
To add JWT verification with Hanko in your Remix application, we’re using the jose library. However, you’re free to choose any other suitable library.
Below we define a validateJwtAndFetchUserId
function which validates the JWT and fetches the user ID. This will also be used to protect routes and ensure only authenticated users can access certain parts of the application.
import { parse } from "cookie";
import { jwtVerify, createRemoteJWKSet } from "jose";
const hankoApiUrl = process.env.HANKO_API_URL;
export async function validateJwtAndFetchUserId(request: Request) {
const cookies = parse(request.headers.get("Cookie") || "");
const token = cookies.hanko;
if (!token) {
return null;
}
const JWKS = createRemoteJWKSet(
new URL(`${hankoApiUrl}/.well-known/jwks.json`)
);
let payload;
try {
const verifiedJWT = await jwtVerify(token, JWKS);
payload = verifiedJWT.payload;
} catch {
return null;
}
const userID = payload.sub;
if (!userID) {
return null;
}
return userID;
}
This is how you can use it in your routes:
import { type LoaderFunction, json, redirect } from '@remix-run/node';
import { validateJwtAndFetchUserId } from '../services/auth.server';
export let loader: LoaderFunction = async ({ request }) => {
const userID = await validateJwtAndFetchUserId(request);
if (!userID) {
return redirect('/login');
}
return json({ userID });
};
export default function Protected() {
return (
<div>
<h1>Protected</h1>
</div>
);
}
Try it yourself
Remix example
Full source code available on our GitHub