For the first lab in this series, let's examine authorization. Firebase handles all of the authentication. But say, we only want certain Google-signed-in people to use our app.
As a quick refresher:
-
Authentication
is the confirmation of who your user says s/he is. -
Authorization
is then the next step: Determination of what your authenticated user has access to.
For today's lab, let's pretend we're building a new "blogging" website. You've looked at WordPress, Blogger, and Forem, but are deeply unimpressed. You want a free solution that you can host on your own GitHub pages (or Firebase hosting) but you want the backend data to be housed in Firestore. Again, the key here is free. You want to share your very important thoughts with the world but only if you can do so without spending a penny.
This is a very glorious goal-- but before we get to the fun part (working on the editor), we need to figure out an authorization scheme. Like all projects, let's start with something super-simple and then slowly build on top of it.
Today's Goal:
To begin, let's say you simply want to set up an authorization scheme that lets some people read your content (eg. you, your SO) and bans everyone else. For this first lab, let's build private blog.
What are some ways we could accomplish this? Let's dive in!
(First: A quick note about how I decided to write this series. Online, I've generally found many "how-to" guides that are plenty informative and spectacular. In particular, the formal documentation and videos that the official Firebase team regularly puts out is probably among the best I've ever seen.
However, I know when I was a beginner and first learning a new platform or technology, often times I went down many rabbit holes and often did things the wrong way. I'm guessing I'm not unique but online, I've generally found that people only share "the right way" to do things.
So, what I think is edifying, is demonstrating "the wrong way" to do things. (And in programming, as an aside, people often call these "antipatterns.")
Anyway, if you're uninterested in learning what not to do, you can simply skip to Part II.
Contents:
Part I: What to not do.
Alright, to get started, go ahead and clone this firebase-learning
repo to your local working directory:
$> git clone git@github.com:r002/firebase-learning.git
Build the project (gotta compile all that dynamic code!) and then start the emulators. Make sure to import the mock data I've set up for lab01. Ie.
$> firebase emulators:start --import=data/lab01
Once the emulator GUI pops up, you can navigate to it and poke around. For those just following along though in bed or waiting in the IKEA checkout line, here are the screens you'd see:
In our fictional universe here, John McClane is married to Diana Prince and they're trying to set up a private blog for themselves using Firebase that only the two of them can access. Ie. If Batman tried to sign in, he should get the boot.
Authorization by checking emails in an array
Instinctively, you might be thinking about setting up a simple check like this:
And your client-side JavaScript would look like this:
async function isAuthorized (user) {
try {
const auRef = firebase.firestore().collection('array_example')
const qs = await auRef.where('writers', 'array-contains', user.email).get()
if (qs.size === 1) {
console.log('>> authorization by email passed!', user.email)
return true
} else {
...
Source:
lab01__array-auth-emails.js
Let's give it a whirl:
As expected, Diana logs in fine:
But Bruce gets rejected:
And we're finished! Mission accomplished! This authorization scheme technically works and while it may not win the Nobel Prize or impress anyone at Black Hat, it does get the job done. And intuitively, if you're 100% new to programming. I think this is probably be the first approach you might be tempted to take. And if you're just working on a simple, personal weekend project, you might even call it a day. Things to do! People to meet! MVP! Etc. I totally get it.
But let's consider some pitfalls with this design. There are several, but I think the most intuitive one is:
What if Diana or John changes their email addresses?
Authorization by checking uids in an array
Remember, the way we have it set up is keeping a list "authorized emails". And while it does happen once in a blue moon (seriously, when was the last time you changed your personal email address?), it is possible. Thus, your next gut-instinct move may be to maintain an array of uids. This totally works! To save you some time, I've gone ahead and set that up quickly too. It looks like:
Now when John logs in, it looks like:
So, this is better. Additionally, with this design-- you've just accidentally implemented what people call "security by obfuscation". That uid that Firebase generates is opaque. Meaning: It means nothing and is in no way connected to the underlying user (John McClane). In other words: It's not an email address and can't be spammed.
One advantage of this design is that if your Firestore data (ie. the authorized_uids
document of array_example
collection) is compromised (more on this shortly), the hackers only steal a list of uids. And while that's bad, these opaque uids can simply be discarded and re-generated. Whereas if those hackers made away with raw email addresses-- well, that's going to be a much tougher conversation with your users! âšī¸
Generally, even in the most secure of systems, hacks still happen. Therefore, whenever possible, if you ever have a choice like we do here: "emails" or "uids"? Always pick the more secure one, especially if it costs you nothing.
Finally, let's examine some other pitfalls of this system:
First, what does your Firestore rules look like? To my knowledge, you only have
get(âĻ)
andexists(âĻ)
that gets specific attributes of a document or checks if a document exists. I don't think you can check array membership with Firestore rules. (If I'm mistaken, please let me know!) Consequently, you're going to need to allow global read or authenticated read of your"array_example"
collection. Again, maybe you don't care about everyone on the internet being able to access your authorized email addresses, but that's generally frowned upon.Documents in Firestore are limited to one mb. How about if your blogging system blows up in popularity and you get billions of writers? Will all those uids fit in that puny array in a single document? Right now the project may only be for Diana and John. But maybe Diana shares it with the rest of the Justice League one weekend. And then before you know it, entire nation states are on your humble, homegrown blogging platform and you're shaping public opinion across continents and winning/losing presidential elections. Who knows? It happens.
One reason more senior developers get paid more than junior developers is because more senior folks have been around the block a few times and this isn't their first rodeo. To this end, thanks to (mostly) experience, they're able to anticipate when a client will likely give them future asks and are able to architect their design to be more "future-proof". Of course we don't want to over-engineer things either (it's easy to go down that rabbit hole where you're suddenly solving all sorts of "imaginary problems") but obviating potential pitfalls is one reason senior devs make the big bucks. This is a trivial example but I think it's illustrative. Maybe, one day you want to keep track of additional user info like a personal bio, their favorite food/song/movie. Etc. In this case, just having one "uids" in a single authorized array just isn't going to cut it.
Part II: What to actually do.
Alright, now we've spent all of that time describing what not to do, let's go ahead and take a look at two examples that could serve you well:
Option One: Check to see if the user exists in an authorized
collection.
Use "exists(...)"
to see if a user is a member of an authorized
collection. If the user's uid
exists in that collection, authorize access. If not, give the boot. It looks like this:
And your firestore.rules
looks like:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
// For lab01__roles-auth-ex01
allow read, write: if exists(/databases/$(database)/documents/authorized/$(request.auth.uid));
}
}
}
And here's the relevant JavaScript snippet:
async function isAuthorized (user) {
try {
const docRef = firebase.firestore().collection('authorized').doc(user.uid)
const doc = await docRef.get()
if (doc.exists) {
...
Source:
lab01__roles-auth-ex01.js
So this approach is simplistic and definitely gets the job done. Our data is now also structured in such a way that allows us actually write Firestore rules for them.
For our example, this binary authorization scheme (in or out?) is perfectly adequate. But let's now take it one step father.
Option Two: Check the user's role
in the authorized
collection.
Consider a traditional blogging app which often needs more granular authorization schemes. Often times, you'll have roles
in your app. In a blogging app, it could be reader
, writer
, and admin
:
- A
reader
can see all articles. - A
writer
can author new articles but only edit their own. - An
admin
can edit everyone's articles.
In the official Firebase documentation, they call this Role-based access and it basically explains everything you need to know.
Just for the sake of completeness though (OCDs gotta OCD!) let's go ahead and take a look at one right way of doing it with role-based authorization
:
Notice we have now given Diana a role of "writer"
as an attribute.
Likewise, our Firestore rules now look like:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
// For lab01__roles-auth-ex02
allow read, write: if get(/databases/$(database)/documents/authorized/$(request.auth.uid)).data.role=='writer';
}
}
}
And here's the relevant JavaScript snippet:
async function isAuthorized (user) {
try {
const docRef = firebase.firestore().collection('authorized').doc(user.uid)
const doc = await docRef.get()
if (doc.exists && doc.data().role === 'writer') {
...
Source:
lab01__roles-auth-ex02.js
Which on the UX yields, as expected:
And that's it! Role-based authorization
implemented! If you have any comments or feedback, please let me know in the comments below. Again, I'm just learning Firebase myself. And also this is my first tutorial that I've ever posted. Hopefully, you found it useful! If there's anything you're specifically curious about, let me know and I can possibly investigate and write about it next. (Haha, after I learn about it myself!) Must admit, writing this all up was strangely satisfying. Until next time! Happy coding! â¤ī¸đđĨī¸
Top comments (0)