I am so much comfortable with using firebase client side authentication and doing things on the client side basically because i haven’t tried much on the server side. My server side is very simple. After a while, when looking at the code, i found out, there are a few disadvantages i have found in having code at the Firebase client-side,
All the application logic is in the client side and its public
Even the templating is being done in client side because its possible with handlebars or pure javascript but too much coding but got used with it.
Here, i will be building a simple firebase app which will be doing majority of work at the server side like SSR(Server Side Rendering) and SSA(Server Side Authentication) and below are some of the items, i am planning to explore,
Setup firebase authentication.
Email and Password
Google
Public and Authenticated Routes
Authenticated routes (If not logged in, page will be redirected to login page)
Public routes available to all users(unregistered users as well)
Premium content will be showed only to certain registered users this is using custom claims.
# 1. Setup firebase authentication.
Here is the flow,
In the client side javascript add the below. This indicates that the state will only be stored in memory and will be cleared when the window or activity is refreshed. We are going to be using httpOnly cookies which do not persist any state client side.
User signs-in and once the authentication is successful, the auth state will change and this function firebase.auth().onAuthStateChanged gets triggered, here we will be able to generate the ID Tokens via firebaseUser.getIdToken()
Pass the generated ID Token in the body of the POST method to route /sessionLogin
In the route, generate the session cookie __session via admin.auth().createSessionCookie
When using Firebase Hosting together with Cloud Functions or Cloud Run, cookies are generally stripped from incoming requests.
Only the specially-named __session cookie is permitted to pass through to the execution of your app.
Going forward on each request to authenticated route, cookie __session verified and appropriate content will be rendered.
Once we are successfully logged-in via Firebase Client side authentication, we generate the ID Token as highlighted beblow and make a POST request to server to route /sessionLogin with ID Token in the body.
In the server, we receive the ID Token in the request body and we create a firebase __session cookie and set it.
In the below picture, you can see a check mark under httpOnly for __session.
When the HttpOnly flag is used, JavaScript will not be able to read the cookie in case of XSS exploitation
In the above image, we see 3 cookies and only __session has httpOnly attribute checked. When we try to display all the cookies below, you can notice only __session is not displayed.
# 2. Public and Authenticated Routes
Handling Authenticated routes
The function isAuthenticated checks whether the user is authenticated or not by retreiving the __session cookie. This function behaves as a middleware but in ExpressJS, its a route handler which is executed before the route.
One additional thing being done in the below code is res.locals.fbUser = fbUser. By doing this we are able to pass the fbUser data to the route.
In routes, isAuthenticated is called like below
Handling Public routes
Here actually, there is nothing being done. When this route is called public.ejs will be rendered and shown.
# 3. Premium content
Here, basically we are going to use custom claims which are attributes that can be added to user accounts in Firebase. So, whenever we enquire about user account details from Firebase, along with user details, you will also get these custom claims details. You might think instead of creating a new code called users in firebase, we can use custom claims to add all the necessary user data like name, address, mobile-no, email, image-url, etc… But, you see custom claims data cannot exceed 1k bytes. So, its recommended to use it for user access controls. Also firebase security rules can access these custom claims, so by using the combination of firebase security rules and custom claims, you can restrict specific content to users.
In my example, i am adding two custom attributes having boolean values and in the route, i check the custom claims, what role user has and render page based on that.
admin : True, means user has admin rights
premium : True, means user can view premium content
In this example, i am not doing anything in the firebase security rules
Here, i am setting up the initial values to work on(one time run). Its just a simple route by calling it, it updates custom claims to the user details.
Next time, when you refresh any authenticated page or viewing user details, you can see the custom claim values. Here i have added below line in fn_isAuthenticated.js to see the user details. So, when isAuthenticated() middleware function is executed, it shows the user details in the console.
To work custom claims in managing users, i have created a page like below and here using the toggle button we can grant and revoke access to users.
Below is the route for the user management page. Function getAllUsers() gets all the users, their details with claims and displayed as in above image. Later user you see the route /usermanagement which calls this function.
This is the client side script of the user management page.
Below is the route that handles the POST request and updates the firebase custom claims, when switch is toggled,
One of the main things to remember here is, there is a relationship between Firebase ID Token and Custom claims. So when you are signing-in to the app, a ID Token is generated via firebaseUser.getIdToken() and using the token __session cookie is created and for all authenticated routes, this cookie is used for verification. ID Token and __session has the same value.
When your custom claims change, it doesn’t get reflected/propagated right away as this ID Tokens and claims are connected. So to propagate the change, user needs to refresh the token, which can be done by one of the following methods,
Logout and Login (reAuthenticate)
Refresh the ID Token forcefully by currentUser.getIdToken(true) in the client side
In my case, this is not possible as i am signing-out after successful login from the client side.
Option left is to reAuthenticate
Main code here is just the highlighted one.
So far what we have seen is,
How to setup custom claims ?
How to refresh the ID Token to propagate the claim change ?
Now, on displaying the premium content, all one have to do is this,
Non-Premium user view
Premium user view
Here we are using simple approach to revoke the token asking user to reAuthenticate themselves by click logout. We can do this automatically as well, by setting up some logics like,
Having a node user node in firebase and update it whenever a claim is updated.
Have a listener at client which listens to user node or in middleware like isAuthenticated() which check if the ID token is revoked and redirect the user to login screen or open up a modal box asking userid and password again to reAuthenticate. Its all upto to the implementer.