Earlier, we created a user with the createUserWithEmailAndPassword()
function. This triggered a POST
request that returned the following response 👇
Some of you might recognize the idToken
as a JSON Web Token (JWT). JWTs are an internet-wide standard for storing and transferring data that is cryptographically signed.
Premise¶
Suppose we're building a web application called Hot Donuts Near Me. Picture a server with our code and database, and a client who wants to interact with our server over HTTP. In this example, we'll pretend the client is some guy named Bob who's accessing our web app from Chrome. The picture in your head should be something like this 👇
Some weeks ago, Bob registered an account for our application. He signed up with the username bobno1
and password hunter1
. When he submitted his password, we hashed it before storing it into our database, in table of users. So, somewhere on the server we have a table of users like this 👇
username | hashed_password |
---|---|
bobno1 | hVIg6D0oiE |
jamiegrl | 5gHi3pfusq |
freddyboy | 33b2yzPBtE |
yolo42 | J9fmJLUkhF |
Fast-forward to today. Bob goes to our web app, hungry for hot donuts. He tries to access the /find-donuts/
API, but this is a restricted endpoint for registered users only. Of course, Bob is a registered user - he just isn't signed in.
So, he signs in. He fills out a form with his username and password and POSTs it to the server. The server then implements some logic like this 👇
Assuming Bob enters the correct username and password, the server can easily confirm that “yes, the person making the request really is Bob”. This process is known as authentication.
So now what? Bob wants to access some protected resources like that /find-donuts/
endpoint he tried earlier. But we shouldn't need him to re-enter his username and password every single time he makes a request. What can we give to Bob so that he can easily and securely access the protected resources of our app?
Server Side Sessions¶
Our goal is to come up with an authorization mechanism. That is, we want an easy way for Bob to access our protected resources after he authenticates.
Authentication vs Authorization
Many people confuse these.
- Authentication is the process of verifying who someone is.
- Authorization is the process of verifying whether they have access to something.
One authorization mechanism we can set up involves server side sessions. The process goes like this..
-
Bob submits his username and password to log in
-
The server verifies Bob's credentials. Bob is now authenticated.
-
The server generates a big random token like
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
-
The server adds this token to a sessions table which looks like this
username session_expires_at token bobno1 2021-12-30_10:30:05 SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
yolo42 2021-12-25_09:45:52 4fwpMeJf36POk6yJV_adQssw5cSflKxwRJSMeKKF2QT
jamiegrl 2021-12-22_22:03:41 Ok6yJV_adQssw5cSflKxwRJSMeKKF2QT4fwpMeJf36P
-
The server sends the token back to Bob in the response of his login request as a cookie.
-
Bob tries to access a protected resource like
/find-donuts/
, but when he makes this request, he also provides the token. -
The server sees that someone made a request to
/find-donuts/
and provided a token. The server looks up the token in the sessions table, sees that it exists and that it hasn't expired, and then sends back the location of hot donuts. In this step, the server authorizes Bob to access the resource/find-donuts/
.
How does Bob send the access token back to the server when he wants to request a resource? Furthermore, how is this easier than just submitting his username and password with every request?
Drawbacks¶
-
The server has to do a decent bit of work. It has to create access tokens and insert them into a database. And with every request to a protected resource, it has to search the database to see if the provided access token exists and what its corresponding expiration time is.
-
Suppose we store these access tokens directly on the server. If we add a second server to handle more user requests, then the following problem could occur..
- Bob logs into server 1, so server 1 stores his access token.
- Bob then makes a request to server 2, attempting to access
/find-donuts/
. - Server 2 doesn't know that Bob is logged in because only server 1 was storing his access token. So, server 2 rejects Bob's request and prompts him to log in.
JWT solves these issues 🙌
JWT¶
Suppose, after Bob authenticates...
-
The server defines a header as
-
The server defines a payload as
-
The server wraps this information into a cookie and sends it to Bob.
Now, when Bob makes a subsequent request to /find-donuts/
, the server will receive this cookie and it can just read it to know that the person making the request is Bob and that he's currently logged in because the session hasn't expired. No need to look up anything in a database. If we have multiple servers this design still works. Sweet!
..but, if Bob figures this out, he can do something very shady. He can modify the username
portion of the payload to be jamiegrl
a user who he knows pays for premium access.
If Bob does this, how can the server know that Bob tampered with the data?
Signature¶
A JWT is made up of three components:
- A header (as shown above)
- A payload (as shown above)
- A signature
The signature is the interesting, cryptographic part of a JWT. It verifies that a JWT has not been modified; that it exists as it was defined by its creator.
How is a signature created?
- The server generates a big, random secret key.
- The header, payload, and secret key are concatenated into a long string.
- A cryptographic algorithm is used to encrypt that string.
The resulting value is the signature.
Note
The cryptographic algorithm used to create the signature should be specified in the JWT header, in the alg
property. For example,
Together, the header, payload, and signature make up a complete JWT, all of which is delivered to the client.
How is a signature verified?
Assuming you have the private key, you can use the header, payload, and private key to calculate the signature. If the signature you calculated matches the signature on the JWT, you can trust that it hasn't been tampered with.
But this isn't the only way to verify a JWT as I'll explain in a minute..
Why can't Bob modify the JWT payload
Bob can modify the JWT payload, but not without the server knowing about it.
Suppose Server A creates the JWT, but Server B needs to verify it. Does Server B need a copy of the private key?
Amazingly, no!
Thanks to asymmetric cryptography, Server A can create a public key that can be used to verify the signature of a JWT.
-
A private key is still needed to
- Sign the JWT and
- Create the public key
-
The public key can be exposed to the world
-
Anyone can use the public key to verify the signature of a JWT that was signed with the corresponding private key
-
Knowing the public key does not help you guess the private key
-
Knowing the public key does not give you the ability to forge a new JWT
Summary¶
JWT is a method to send and store data on a client such that, if the client tampers with the data and sends it back to the server, the server will know that the data has been tampered with.
JWT is not a method to encrypt data. If you give a client a JWT, know that the client can see everything inside the JWT. The only thing they can't see is the secret key used to sign the JWT - the one that's stored on the server.
Additional Details¶
In practice, there are a few extra steps to creating a JWT.
Base64 URL encoding¶
The header and payload are base64 url encoded. The purpose of this is to turn JSON data like this
into a nice compact string like this
The compact string is more suitable for sending in the header of HTTP requests. Although it looks cryptic, it's not. Anyone can easily decode it.
Concatenation¶
JWT specifies that we should concatenate the base64 url encoded header with the base64 url encoded payload with the signature, separated by periods. So at the end of the day, the token we send and receive looks something like this
Exercises¶
When I created a new user in my fireauth application, the server gave me this reponse 👇
How might you recognize that the idToken is in fact, a JWT?
There are a few giveaways..
- Most JWTs start with
ey
because that's the base64 encoded form of an open curly brace{
- The fact that it's an id token is a hint
- It includes two periods
.
. Remember, JWTs concatenate a header, payload, and signature, delimited by periods. (Oddly, there's no trailing string in this JWT - an indication that there's no signature.. Hmmm 🤔)
When did/does the idToken
expire?
Hint
The token expired at 1723863949 unix time, or Sat Aug 17 2024 03:05:49 GMT+0000.
The easy way to see this is to copy the idToken
and paste it into the debugger window at jwt.io.
Something's seriously wrong with this JWT. What is it, and why?
This JWT doesn't have a signature. Nor does it specify alg
in its header. That's because this JWT was created from the Firebase Emulator - a development environment that doesn't require the same level of security as production.
Here's a real JWT issued to me from a live Firebase app.
Is it valid or has it been tampered with?
Public Keys
At the time this token was issued, Google provided the following public keys:
Hint
-
Copy and paste the token into the debugger at jwt.io. Note that the decoded header looks like this
-
The Firebase docs on verifying ID Tokens tell us to get the public key from this URL endpoint 👇
https://www.googleapis.com/robot/v1/metadata/x509/[email protected]
That endpoint returns two public keys, each with a
kid
.Our token specifies
"kid": "d4269a1730e50719e6b1606e42c3ab32b1280449"
, so we need to get the corresponding public key.Key rotations
The public keys rotate every few hours. The public key you need to verify the token only existed for a few hours around the same time the token was created.
That's why I gave you the data in the question 😃
-
Copy and paste the public key into jwt.io, in the public key field.
Replace all \n
characters in the public key with actual line breaks!
If you did everything correctly, you'll see that the token is indeed valid!