— reactjs, firebase, remix — 1 min read
Tutorial on using Firebase to authorize server side access in a Remix app.
Remix has a good support for server side rendering. In this part I will be using Firebase to authenticate user on server side instead of client side authentication that I did in the previous one.
You need to know how to setup firebase admin sdk I missed out that part while recording but you need to download
service-account.json
We need a Login
form where user can enter username and password.
Once we have that we need to validate it using firebase authenticaton.
We do that using signInWithEmailAndPassword
. This method returns
idtoken which we pass to Remix
to create Cookie.
1// snipped imports and boilerplate2if (process.env.NODE_ENV === "development") {3 connectAuthEmulator(auth, "http://localhost:9099");4}5
6export default function LoginIndex() {7 const [username, setUsername] = useState("");8 const [password, setPassword] = useState("");9 function login() {10 signInWithEmailAndPassword(auth, username, password)11 .then((user: UserCredential) => {12 user.user13 .getIdToken()14 .then((idToken) => {15 return fetch("/session", {16 method: "POST",17 body: JSON.stringify({ idToken }),18 });19 })20 .then(() => {21 // @ts-ignore22 window.location = "/dashboard";23 });24 })25 .catch((err) => console.error({ err }));26 }27
28 return (29 <Layout>30 <h1 className="font-bold text-3xl">Login Form</h1>31 <div className="pb-5">32 <input33 placeholder="email"34 onChange={(e) => setUsername(e.currentTarget.value)}35 />36 </div>37 <div className="pb-5">38 <input39 placeholder="password"40 onChange={(e) => setPassword(e.currentTarget.value)}41 />42 </div>43 <button onClick={login}>Login</button>44 </Layout>45 );46}
Next up we have a utility file which we need on Server side to
1const expiresIn = 60 * 60 * 24 * 5 * 1000;2
3export async function createUserSession(idToken: string, cb: Function) {4 return admin5 .auth()6 .createSessionCookie(idToken, { expiresIn })7 .then((sessionCookie) => {8 return cb(sessionCookie);9 });10}11
12export async function getUserSession(request: Request) {13 return request.headers.get("Cookie");14}15
16export async function requireUserId(request: Request) {17 const sessionCookie = (await getUserSession(request)) || "";18 return admin19 .auth()20 .verifySessionCookie(sessionCookie, true)21 .then((decodedClaims) => {22 return decodedClaims;23 })24 .catch((err) => {25 throw redirect("/");26 });27}
Session.tsx this Remix Action function takes care of setting Cookie in the response header. This gets invoked once the user is logged in using Firebase Auth.
1import { ActionFunction, json } from "remix";2import { createUserSession } from "~/utils/session.server";3export const action: ActionFunction = async ({ request }) => {4 if (request.method === "POST") {5 const payload = await request.json();6 const { idToken } = payload;7 return createUserSession(idToken, (cookie: string) => {8 return json(9 { idToken },10 {11 headers: {12 "Set-Cookie": cookie,13 },14 }15 );16 });17 }18 return json({ message: "Method not allowed" }, 405);19};
Dashboard: Accessible only if session cookie is present Create a Remix Loader function that checks if request scope has session token and then renders the component else it redirects to login screen.
1import { LoaderFunction, useLoaderData } from "remix";2import { requireUserId } from "~/utils/session.server";3
4export const loader: LoaderFunction = async ({ request }) => {5 const decodedClaims = await requireUserId(request);6 return { decodedClaims };7};8
9export default function DashboardIndex() {10 const actionData = useLoaderData();11 return (12 <div className="container mx-auto p-10">13 <h1 className="text-2xl">PersonalDashboard</h1>14 <p>15 Logged in as{" "}16 <span className="bg-red-200 italic ml-2">17 {actionData.decodedClaims.email}18 </span>19 </p>20 <form action="/logout" method="post">21 <button type="submit" className="button">22 Logout23 </button>24 </form>25 </div>26 );27}