Skip to content

Firebase Security Crash Course in a Nutshell

Just recently, Firebase started their Firebase Pro Series on their YouTube channel.

“Because this is Google, we’re going to build a new chat app”

This one stood out to me. Mike McDonald covered what probably most of us (myself included) did not know about the Firestore Security Rules. It is however bundled in a 12-minute video and as you might be aware, that might be way too long for our lazy bums to sit down for.

So here it is in a nutshell! Mike McDonald went about the crash course by creating a simple group chat app and going through different security scenarios. I’m going to show you the most significant points, but I do still recommend you watch the video. I’m going to be quoting much of what Mike has said throughout it.

More than just Storage and Firestore protection

“As you may already know,  security rules protect your data from both Cloud Storage and Cloud Firestore from unauthorised access. What you may not know is it can do so much more”

  • Define new resource types
  • Validate incoming data
  • Execute business logic

Defining New Resource Types

Message.json

// Message.json
{
	"username": "Mike McDonald",
	"userId": "7199732f-ec7a-46bd-8949-71cd323a21fd",
	"createdAt": "2018-01-29T01:23:45Z",
	"messageText": "Hello, World!"
}

Being a group chat app, we’re going to have messages defined in a separate JSON file.

Validating Incoming Data

This one caught me by surprise. Mike performed a bunch of validation logic to validate the incoming message. He then put it all in a function.

Chat.rules

function isChatMessage(message) {
	return message.data.size() == 4
	&& message.data.username is string
	&& message.data.userId is string
	&& message.data.createdAt is timestamp
	&& message.data.messageText is string;
}

service cloud.firestore {
	...

	match /rooms/{roomId} {
		match/messages/{messageId} {
			allow create: if isChatMessage(request.resource.data);
		}
	}
}

Now now, I know putting things in functions is a SUPER basic fundamental practice, and I do feel stupid for not thinking about it. Though I know I’m not the only one.

“Using functions makes it easy to add more validation logic later”

Authentication Checks

Mike identified 2 important authentication checks that need to be done in this app:

  1. Verify the user is who they claim to be
  2. Members can either be a reader or an editor

The first problem is pretty easy to solve.

allow create: if isChatMessage(request.resource.data)
&& request.resource.data.userId == request.auth.uid;

This probably isn’t your first time seeing this scenario nor this logic. Just validate the incoming data userId to the currently authenticated userId provided by Firebase Authentication.

Now the second problem requires the user to either be a reader or editor. (Readers are only given read-access. Editors are also given write-access)

User.json

// User.json
{
	"role": "editor" // Or "reader"
}

What Mike did was create another resource type: user, and this only has one property which is its role.

Chat.rules

function isUser(user) {
	return user.size() == 1
	&& (user.role == 'editor' || user.role == 'reader')
}

service cloud.firestore {
	...

	match /rooms/{roomId} {

		match/messages/{messageId} {
			allow create: if isChatMessage(request.resource.data)
						&& request.resource.data.userId == request.auth.uid;
		}

		match /users/{userId} {
			allow create: if isUser(request.resource.data)
		}

	}
}

Before checking what particular role a user has, Mike first validated that the incoming data of the user is in its correct format just like he did with the messages earlier. He created a sibling document under {roomId} to validate this.

match /rooms/{roomId} {

	function getRoleForUser(userId) {
		return get(/databases/$(database)/documents/rooms/$(roomId)/users/$(userId)).data.role;
	}

	match/messages/{messageId} {
		allow create: if isChatMessage(request.resource.data)
				&& request.resource.data.userId == request.auth.uid
				&& getRoleForUser(request.auth.uid) == 'editor';
		allow get, list: if getRoleForUser(request.auth.uid) in ['editor', 'reader'];
	}

	...
}

Now to get the particular role of the user, Mike used the get function of Firebase to find the particular document in the database and get its role. He then modified the authentication checks under messageId to include the getRoleForUser function and give the corresponding permissions.

Creating a Room (Bootstrapping)

When a room is first created, the user creating the room should be given a new role: admin. He will then be able to set the permissions for new users joining the room.

function isAdminUser(user) {
	return user.size() == 1
	&& user.role == 'admin'
}

service cloud.firestore {

	function roomExists(roomId) {
		return exists(/databases/$(database)/documents/rooms/$(roomId));
	}
	
	...

	match /users/{userId} {
		allow create: if (!roomExists(roomId)
				&& request.auth.uid == userId
				&& isAdminUser(request.resource.data))
			|| (getRoleForUser(request.auth.uid) == 'admin'
				&& request.auth.uid != userId
				&& isUser(request.resource.data))
	}

}

First Mike created new functions isAdminUser and roomExists, both pretty self explanatory. He then modified the rules to create users to satisfy the following conditions:

If the room doesn’t exist:

  • It will be created with the user given admin permission
  • The user performing the action is making himself an admin
  • The user creating the room is doing it on behalf of himself (you can’t create a room and set somebody else as the admin)

If the room already exists:

  • The user performing the action is already an admin
  • The user is creating either a reader or an editor
  • The user performing the action is not making himself a reader or an editor

And that’s about everything Mike covered creating the security rules for a bare bones group chat app. If you want to see the finished product in action, check the video.

Disclaimer

All the content in this post is taken from the video mentioned.