Continuing my series on blogging about every line of code I write, today’s topic is one that is pretty critical to creating a working auth system that I don’t have to code. If I were rolling my own auth, I wouldn’t have to do a lot of this since most of this process could happen on the server when the user logs in, but because I opted to return to the hard-won bosom of Firebase for my auth needs, I’ve added a step in my process.
As Firebase is an “off-site” authentication system, I don’t have a simple way of introducing my Firebase return data to my game service back-end, so I basically have to take Firebase’s data and send it over to my custom back-end manually. Since sending plain text is garbage, I rely on “JSON web tokens”, or JWT. All the kids love JWT these days.
A JWT is not a secure mode of transport; it’s only encoded, not encrypted, which makes it good for verification, but not super good for security on its own. Because of the way data is encrypted when using JWTs, the destination can decode it and verify that the data that was sent wasn’t messed with between the time it was generated and the time someone reads it. You know, at least on paper.
Here’s the flow of the test I conducted this evening.
Step One: Logging In using Firebase.



There’s nothing fancy here. I am using the useAuth hook I created to access Firebase for my “Adventure Outliner” project which allows me to log in a user using email and password, Google, Twitter, or Facebook. I’ve tested with Google because it’s automatically verified my email and non-verified registration/login is a different beast of a system.
Once the Promise returns with a valid user payload, I redirect to a “session” component on the client.
Step Dos: Process Firebase JWT on the Server
After a user authenticates, they’ll need a session record on the game server side. This is handled on the Node server which now poses a problem: although Firebase logged ’em in, Node doesn’t know what the hell Firebase knows. This is where the JWT comes into play, because Firebase’ JWT carries a user ID that we’ll use to help track a game session.
import React, { useEffect } from 'react';
import { useAuth } from '../Hooks/useAuth';
const axios = require('axios');
const Session = (props) => {
const { user } = useAuth();
useEffect(()=>{
if (user){
const token = user.accessToken;
const axiosBase = axios.create({
baseURL:'http://SERVER_URL:PORT/account',
headers: {'Authorization' : `Bearer ${token}`}
})
axiosBase.post('/new-session')
.then(results=>{
if (results.status === 200){
console.log(results);
}
})
.catch(err=>{
console.log(err);
})
}
},[])
return <div></div>
}
export default Session;
This is the “session.js” component that we redirect to after a user authenticates successfully. We pull the accessToken from the user exposed by useAuth. Using Axios, we set the Bearer header to carry our token, and then POST to the endpoint that will take the JWT.
Keep in mind that we’re going to be sending a JWT with every request, and will parse it as part of the game server authentication flow. Not only will this verify that the user who they claim to be (allegedly), but it will allow us to access data we need for processes related to the game state.
Step The Third: Receiving the JWT on the Server
I’m using Express, as one does, to handle routing on the Node server, so this next code block shows exported function that is intended to act as middleware. Middleware is like a filter for inbound and outbound traffic sent to and from a web server. If you look at the original code block above, you’ll see that we are telling Axios to call http://SERVER_URL:PORT/account/new-session. When Express encounters a request to http://SERVER_URL:PORT/account, it will route the request through a higher-level middleware that handles all account related activity.
require('dotenv').config()
import http from 'http';
import express from 'express';
var cors = require('cors');
import { Server } from "@colyseus/core";
import { WebSocketTransport } from "@colyseus/ws-transport"
import { DataAccess } from './Database/MSSQL';
import { BasicRoom } from './rooms/basic';
const accountRouter = require('./Routing/account');
const port = process.env.PORT || 3553;
const app = express();
app.use(express.json());
app.use(cors())
const server = http.createServer(app);
const gameServer = new Server({
transport: new WebSocketTransport({
server: server
})
});
gameServer.define('basic', BasicRoom);
//All traffic to /account will be sent to the function accountRouter
app.use('/account', accountRouter);
server.listen(port);
console.log(`Listening on http://localhost:${ port }`)
The above code is the entrance point for the Node/Express server. It’s fairly boilerplate, but the magic happens by importing accountRouter and using it to handle all traffic intended for baseURL/account by sending it to middleware called accountRouter.
accountRouter, right now, is a stub which accepts the request from the client when the destination includes /account after the base URL.
const express = require('express');
const accountRouter = express.Router();
const fbJWTAuthRouter = require('../Database/Helpers/auth');
accountRouter.use(fbJWTAuthRouter);;
accountRouter.post('/new-session/', (req: any, res: any)=>{
const what = req.user;
res.send(what.uid).status(200);
})
module.exports = accountRouter;
Every request to /account passes sequentially through any handlers set up until it reaches a more specific destination (like /new-session) or we purposefully stop the request from continuing. We can take advantage of this by throwing in “universal middleware” that all requests to /account must pass through. That universal filter is a function fbJWTAuthRouter.
const express = require('express');
const fbJWTAuthRouter = express.Router();
const admin = require('firebase-admin');
const serviceAccount = require("../../../serviceAccountKey.json")
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
module.exports = fbJWTAuthRouter.use((req: any, res: any, next: any)=>{
let idToken;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
console.log('Found "Authorization" header');
// Read the ID Token from the Authorization header.
idToken = req.headers.authorization.split('Bearer ')[1];
try {
admin.auth().verifyIdToken(idToken)
.then((decodedIdToken:any)=>{
console.log('ID Token correctly decoded', decodedIdToken);
req.user = decodedIdToken;
next();
})
.catch((err:any)=>{
res.status(500).send(err);
})
} catch (error) {
console.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
}
}else if (!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) {
console.error('No Auth Headers Found')
res.status(403).send('Unauthorized');
}
})
In a nutshell, if we have data in the Bearer header, we’ll send it through Firebase’s own JWT decoding API. Since they encoded it in the first place, we’ll let them decode it (we could use third part JWT libraries, which we will get to eventually). Assuming everything susses out, we set req.user to the value of the decoded token and tell the middleware to pass the data to the next step in the process. Any errors will short-circuit the request, and we’ll issue a response which fires immediately back to the client with any error information we might pass.
Basically, the only way to reach /new-session is to successfully parse a Firebase JWT.
Step The Third: Dealing with the User ID
Let’s look at the accountRouter code again, because our data just fell through the fbJWTAuthRouter and successfully passed through, picking up req.user along the way.
const express = require('express');
const accountRouter = express.Router();
const fbJWTAuthRouter = require('../Database/Helpers/auth');
accountRouter.use(fbJWTAuthRouter);;
accountRouter.post('/new-session/', (req: any, res: any)=>{
const decodedToken = req.user;
res.send(decodedToken.uid).status(200);
})
module.exports = accountRouter;
Next, the data will encounter the accountRouter.post() which includes our familiar route ending, “/new-session”. Here we can parse the req variable which has all of our original request data from the client as well as the newly added req.user info which contains the decoded Firebase JWT data.
At this point we will use the value of decodedToken.uid — the Firebase User ID — to generate or recover a game-session-record. Once that is done, successfully or not, we will return an appropriate status to the client which can finish its processing.