JEFF HULEATT: Hi, everyone.
Hi.
My name is Jeff, and more important
than my name is a really important question for you.
Who here loves figuring out JWTs?
Anybody?
AUDIENCE: Yeah.
JEFF HULEATT: Yeah?
Someone from the Authentication Engineering Team sneak in here?
Well, I don't either, which is why I wrote this talk.
I don't like figuring out JWTs.
I don't like remembering what HTTP
headers to attach to requests.
I don't like figuring out password salts.
And that's why I really like Firebase Authentication.
Instead of worrying about all these details,
Firebase Authentication gives me what I really need--
a login function where I can sign in a user, a hook where
I can listen to see if a user signed in or not so
that I can update my app state accordingly,
and security rules so that I can still control access
to my database safely.
And this model is really nice because instead
of worrying about so many small details,
I just think, OK, I have an app.
It's going to talk to a database.
And I have an app that's going to talk to Cloud Storage.
And this really simple model, with the help
of App Check and security rules, means
that I can write my app quickly and just
have this really simple model instead
of worrying about API layers, and JWTs,
and passing the right auth state around between services.
And inside this world protected by App Check and security
rules, I can also trigger background Cloud Functions.
Like, based on a database write or a upload to storage,
you can do things like copy image
metadata from a new upload to storage into Firestore.
And this world is so comfortable for client apps
because Firebase owns the server,
and Firebase gives me some magic client SDKs
that handle passing the right authentication
tokens back and forth.
But today we're going to venture a little bit
outside this comfortable world and, instead,
talk about doing a little bit of authentication ourselves
on the server when we need it.
So first, let's talk about when we might need a server.
I already mentioned background tasks, for example,
uploading an image and then copying some of that metadata
into storage.
And that's kind of covered because we
can trigger our functions behind App Check and security rules.
But there are other use cases too.
For example, if I'm building a game that
has some pretty complicated game logic that I want
to run on a server to make sure people aren't cheating,
I might need an API that my app can
call to verify if a new move or turn is valid.
I also might want to let people build their own clients so they
can access my app services in creative new ways
and support platforms that maybe I haven't built an app for yet.
And finally, maybe I'm trying to really increase my page render
speed and get my website to render more quickly for users.
In that case, I might want to server render my website,
and that has its own considerations outside
of the comfortable client-oriented Firebase
authentication world.
And when I'm doing stuff on the server,
I need a powerful SDK to help me out,
and that's the Firebase Admin SDK.
Since it's designed to be run in a trusted environment instead
of a client app that I have no control over,
I can do some extra stuff that I can't otherwise.
I can use a whole bunch of common server languages
to connect to Firebase services without worrying
about security rules.
And this is really powerful and great because a lot of times
on the server we might want to do bulk reads or book rights
and not worry about impersonating a specific user
to access specific data.
The Admin SDK also lets me generate and verify
Auth tokens, which we'll come back to later.
So if we have cloud function for Firebase
and we have an Admin SDK where we can do stuff,
why is this a problem?
Well, let's work through it.
If I have an HTTP endpoint, like a REST API,
it has to be open to the world so that people
can access it and call it.
And this endpoint that's open to the world
and takes whatever data people send to it
is using the Firebase Admin SDK because it's
running on the server.
And this Admin SDK can't impersonate a user,
and it has access to all my Firebase services.
Can anyone see why this might be a problem?
All right.
So it's a little scary, but we're
going to work through some common approaches
to solve use cases.
Let's start off with that example
I mentioned about complex game logic,
where instead of my app talking directly to Realtime Database
or Firestore, first it calls an API that
verifies if a move is valid.
In that case, it's actually pretty easy.
We can use callable functions.
These are an extra layer on top of HTTP functions
that make it easy to pass data back
and forth between the client app and the server.
The reason this is easy is because we're still kind
of within that Firebase box.
We know that our app that we control
is using the Firebase SDK, and the Firebase SDK
gives us the standardized interface
where we can pass data back and forth.
And on every request, it also attaches the Auth context,
so if somebody logged in and who are they--
and also the App Check status.
Is this app a verified app that I
know that I control and not just somebody trying to spoof me?
And so if this is all we need to do,
it's actually pretty simple.
And this is what the code looks like.
Beyond the boilerplate on the top and bottom for setting up
a Cloud Function, a callable function,
I can check on the request object and say, hey,
is there an App Check-- is there any App Check information?
If not, I can fail early and reject the request.
Same with authentication.
The authentication status is baked right
into the request object, and so I
can check if somebody is logged in at all.
I could check who the user is, and maybe I
have some metadata stored in the Firestore
where I need to look up a little bit more about them.
But in general, I have all this information,
and that way I can safely execute whatever code I need to
in my function.
But there is a more complicated use case,
and that's when it's not just my app calling the API.
It's also third-party apps, so other developers
that want to build their own clients to talk to my services.
And since we don't control their apps,
we don't know if they're using a Firebase SDK.
We have to rely on standards instead.
And for that, we'll use HTTP Authorization headers.
The idea here is that a developer at some point
registers with us as a developer that
wants to talk to our services, and we grant them
an authentication ID token.
And then this secret value, they need
to send with every request.
So that's what this diagram is showing.
The API consumer, a developer, gets an ID token
from Firebase Auth at some point,
and then adds it to their app.
And every request they make to our function
has this bearer token, and this bearer token is then checked.
This bearer token is then checked
by our function against Firebase Authentication
to make sure that this user is valid.
And it also contains information about this user
that we could look up in Firebase
if we have any extra metadata about banned API users
or something like that.
And here's an idea of what that looks like in code.
So this first snippet is what the third-party developer
would do.
So they would fetch against any URL in our server
that we have open and simply just attach the bearer token
as an authorization header.
On our end, when we receive a request,
we look in the headers, make sure there is a bearer token.
And if there is, we can call the Admin SDK's verifyIdToken
method to get information about this user
and decode the ID token.
And that's pretty much it for APIs with our functions.
So let's talk about something that's kind of like an API,
but also very different.
Like an API, server-rendered websites
are some HTTP endpoint we leave open to the world
so that people can access it.
But could you imagine if your browser, every time
you tried to go to a website, said, hey,
can you copy and paste your ID token
so that I can send it as an authorization header
to the server?
I don't think any of us would use the internet
if that's the way it worked.
So instead, we have to use a different standard
to get server-rendered websites to work.
And we'll use cookies for that.
So the way it works is on their first request
to a protected route in our site,
the browser will just send a GET request.
The server will say, hey, there are no cookies here.
Go sign in and come back later when you are assigned in.
When this user is signing in, they'll
sign in client side against Firebase Authentication.
And similar to our bearer token flow earlier,
they'll send a request to our server
with an authorization header and their Firebase ID token.
Just like before, the server will verify this bearer token
against Firebase Authentication to make sure it's real.
But the difference here is that when it sends a response back
to the browser, this response will contain a cookie.
And this cookie contains a top-secret code
that the browser will then automatically
attach to every new request so that it can send along
authentication context about the user.
Now, when the user tries to access the protected route
again, the browser sends along that cookie.
The server checks and sees it is valid
thanks to Firebase Authentication,
and it can actually return the web page.
And now, instead of a user going in there
and typing in an ID token, the browser
handles all of this automatically for us.
And here's a quick look at how it
would look to implement this.
So when the user first signs in on the sign-in page
in the browser, we'll call the Firebase client
SDK's getIdToken function.
Then, just like before with our bearer token flow,
we'll fetch against a special URL on our server,
getCookieURL in my example, and send a request
with an authorization header.
The server will respond with a cookie
by calling the Admin SDK's createSessionCookie method,
and then attach that cookie to its response.
Then, on every subsequent page request
when the browser does send along a cookie,
we can parse the cookie and call the Admin SDK's
verifySessionCookie function to make sure the cookie is real
and get information about the user.
So overall, it's not the most complicated flow in the world,
but there are some pitfalls to worry about.
First off, our cookie settings-- we
want to make sure that people aren't
creating their own cookies to try and trick us.
We want to make sure cookies expire
if we want people to not be logged in forever on our site,
and we want to make sure that everything stays secure.
It's also a bit tricky with these server-rendered
frameworks where they create some HTML in the server,
send it to the client, and then spin up
the client SDKs in the client for more reactivity.
It can be hard to pass the auth state that we've already
verified on the server into the client,
so that's something else to think about.
And finally, since we're using the server,
we're still using the Admin SDK.
And that means we can't impersonate a user
to have security rules apply to their requests.
So we have to implement our own logic to protect our data.
But overall, I think we found a few techniques
to make sure that we can do auth on the server
without it being too scary.
We can step a little bit outside of Firebase's
comfortable, client-oriented world
and still be able to keep our data secure.
With background triggers, we can make sure our functions only
run when a server sits behind security rules
in App Check has been triggered.
We can use callable functions to automatically send along
Authentication state and App Check state.
We can create authenticated HTTPS endpoints
with authorization headers, and we can do cookie-based off
with a combination of authorization
headers and a cookie flow.
Now, as an added bonus, the newly-announced dynamic
framework support for hosting is working
on making this cookie flow a lot more simple.
So in the future, you might see things
like being able to use the client
SDKs on the server or even this cookie auth being
handled automatically for you.
So please check out the new dynamic framework support
if you're interested in that.
Thanks for spending this time nerding out
on functions with me.
[APPLAUSE]
[MUSIC PLAYING]