Before I work on creating game data, I first need to figure out how that looks in the database, and in doing so, I've encountered an architectural conundrum around it. Here's what I know:
- I know users can start a game by forking the repo (this creates some game data in the database)
- I know users will be able to log in to the website to look at their stats and maybe do more advanced things to their game (this creates user data in the database)
- Users could start a game and then log in; or users could log in first, and then start a game
How do I store game data in relation to user data, and establish ownership between the two? This actually poses a slight challenge due to the use of serverless because I don't quite trust the player data provided. Let me explain:
When a user starts a new game, they simple fork a repo, which we detect using the github webhook listener set up last time. This act gives us three pieces of identifying information:
- The repo's URL (which includes the username)
- The username
- The user ID
For all intents and purposes, we can trust this data, as it comes from GitHub's webhook API, and is secured with that secret. So we can store this as patr of game data.
When a user logs in using GitHub Oauth, we get the following pieces of identifying information:
- A Firebase UID
- The user's email
- The username
- The user ID
- A gitHub access token
The Firebase UID is a unique ID in Firebase's Auth system, it doesn't match any IDs in github, so we can't use it for matching the game data.
The user email is not useful either as we don't receive that with the game data. Also, I can't turn this off, it seems to be an integral part of Firebase GitHub OAuth.
The the other three are all received during OAuth, and we would need to store it in firestore. But any of this data could be spoofed - since the login is handled by the frontend, the firestore database has to allow write permissions to it for the logged-in user. Which means a user could in theory change this data if they bypass the frontend logic (which is trivial to do). For example, the user could change their user ID to a different user's ID; if we trusted user IDs, it would allow that user to take over another user's data. We don't want this.
The only two guarantees we have are that the Firebase UID and the user email belong to the GitHub account who logged in. The rest we can't trust because they are handled by the frontend, and therefore by the time we receive them at the database, could have been tampered with.
Normally, the easy way around this would be to store the game data under the user's document. If we did this, then while a user could tamper with their own data, it wouldn't grant them access to anything else, and they ruin their own app experience. The problem is localized.
However, to do this, the user data would have had to have been created first, and then the game. This is fine if we make the user sign up before starting a game, but this doesn't work if we allow the game to start first, and a user to sign up/log in for the first time later. We could still do this and copy the game data to the user's account, but this adds to complexity.
Another way to do this (which is closer to the traditional way before serverless came along) is to roll our own OAuth, so that OAuth is being handled by the backend, and we can trust the data can't be tampered with. This would mean we can disallow write access from the frontend to a user's profile, depriving them of any opportunity to tamper with their IDs.
However, I'm allergic to hard work, so we won't be doing this. Firebase's built-in Oauth is a pleasure to use, as it's all handled from the frontend, without a need to write your own Oauth handlers (which wouldn't be too hard, juts annoying).
Fortunately there remains one option for us to still accurately link up user data and game data despite Firebase Auth's trusted identifiers not being useful to us, and auth data from the frontend not being trusted - that GitHub access token.
Even though we can't trust that the token given to us wasn't manipulated on it's way over, we can still use the token and check with GitHub API to make sure that taken belongs to the user we think it does. Since users can't easily guess other user's tokens, and if we assume the bearer of the token is who they say they are if the token's details match, then we can use this to establish the logging-in user's true identity. Since we do trust Firebase Auth's UID (it just didn't have enough information for us to tie it to a specific game), we can use this token to tie together an Auth UID and a game!
If this is the solution we're going for, then that would imply that when frontend does registration, instead of sending the data directly into Firestore, it should go via a firebase function which validates the service token, and does the remainder of connecting the account to any matching games.
I only slightly regret this solution as it does add one firebase function, which to be honest, rolling our own OAuth would also need. But I feel this is still neater, as we need to be able to ask the GitHub API for things anyway.