Firebase Authentication enables you to really easily build an authentication flow within your application, however in most scenarios you'll likely be performing this logic using the client SDK where the users authentication state is stored on the browser.
Remix is a full stack web framework which allows you to build out your application, however is primarily focused on using a server to serve your pages. When it comes to authentication, you'll want to be preventing the user from accessing the page until they're authenticated via the server - rather letting the client check whether they can access it.
So, how do we get Remix & Firebase Authentication to play nicely together?
Setting up a project #
If you haven't already done so, create a new Remix project:
Next, lets go ahead and install the Firebase client SDK & the Firebase Admin SDK:
Setting up Firebase #
We'll be using both the Firebase client SDK and admin SDK for this, so first lets create a two new files which will act as our entry point to the SDKs.
First, create the file for the client sdk under app/firebase.client.ts
. Grab your configuration from the Firebase Console:
You'll notice here that we're setting the authentication persistence to "in memory". This basically tells Firebase Auth to only store the authentication state in memory - so if you refresh the page you'll lose that state. We'll be storing the authentication state on the server, so this is exactly what we want.
Next, create the file for the admin sdk under app/firebase.server.ts
. For this, you'll need to reference a service account file from the Firebase Console.
The admin SDK doesn't allow initialization of the same app more than once - since Remix provides some hot-reloading on file changes this will trigger initialization more than once, so we first check if a Firebase App instance has been initialized and return it if it already has been.
Authenticating the user #
Now we have our Firebase setup configured, let's go ahead and authenticate a user!
Let's assume we already have a user account on our Firebase project as an email/password user, create a new login page with a basic form:
We've got a basic form here, however we don't actually want to submit the form via a standard form submission. Instead,
we want to intercept the "submit" button and attempt to log the user in. Lets go ahead and create the handleSubmit
function:
Ok, so when our form is submitted, we grab the email & password values and attempt to log the user in (using signInWithEmailAndPassword
). Upon
a successful login, we'll then get the user's idToken.
However, since we enabled in-memory persistence the authentication state will be lost if we reload the page.
Creating a session cookie #
With a users ID Token, we can use the Firebase Admin SDK to call the createSessionCookie
function, which generates a JWT token. This token can be stored
in a cookie - which we'll later use for authentication.
To create this cookie, we first need to create an action:
Since we previously handled the form submission ourselves, we need to programmatically trigger the action with the form data (which should include the idToken
).
To do this, we can make use of the useFetcher
hook:
To summarize the flow:
- The user enters their email & password into the form.
- The user presses the "submit" button.
- The submission is intercepted via the
handleSubmit
function. - The email & password is extracted the form event.
- The
signInWithEmailAndPassword
function is called to attempt to authenticate the user. - On success, the
getIdToken
function is called to get the user's idToken. - The
idToken
is sent via a POST request to our action. - The action validates the
idToken
is valid & creates a JWT (with a custom expiry date).
Storing the JWT in a cookie #
Now we have a user JWT token, we need to store this as a cookie in a session... luckily Remix makes this super easy.
First, create a app/cookies.ts
file:
This cookie will be used to store the JWT token - one detail to be aware of if you need to ensure the expiry time you defined when creating
the JWT token (via the expiresIn
property) matches up with the cookie expire date. If these don't match, you'll end up with the JWT expiring
before the cookie does, or the cookie being deleted before the JWT expires.
Back within the action, you can now redirect (or whatever you want to do) with a Set-Cookie
header which will inform the browser to store the cookie
in the browser's session:
On success, the action will redirect the user to the /
route and inform the browser to save our cookie in the browser's session!
If you open the browser's developer console, you'll see the cookie is now stored with an expiry date. If you refresh the page it should
still be there (if it isn't, make sure you set an expiry date to sometime in the future!).
Authenticating the user #
Now we've got the users JWT token stored in a cookie, we can use it to ensure they're authenticated. For example, let's say we want to display the
users profile information on the /profile
route:
In this logic, we first attempt to see if there is a valid session cookie store on the users browser (which is sent to the Request). If not, redirect them away. If there is, ensure the returned JWT is both valid and not expired. Once confirmed it's valid, use the users id to perform some logic (e.g. getting a user profile).
This logic would best be extracted into a utility function so you can reuse it across other pages & even actions.
Logging out #
To log a user out, it's as simple as informing the browser it needs to delete the cookie. In Remix, this is really straightforward - create a
app/routes/logout.ts
file with a loader function:
Now to log a user out, simply link them to the /logout
route - they will be redirect to the /
and the session cookie will be removed (since the date is in the past, the
browser will remove the cookie from its session).
Wrapping up #
There we have it - simple authentication with minimal fuss.
The great thing about this approach is that you can implement types of provider login, for example if you wanted to use GitHub, Google, Facebook, Twitter, etc login, just pass the returned
UserCredential
to your login action!