Ben Gorman

Ben Gorman

Life's a garden. Dig it.

Earlier, we created a user with the createUserWithEmailAndPassword() function. This triggered a POST request that returned the following response 👇

{
    "kind": "identitytoolkit#SignupNewUserResponse",
    "localId": "rw989AAMzeEiULUS0guvTbL4uaLH",
    "email": "[email protected]",
    "idToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6Imp1bGlhbkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTcyMzg2MDM0OSwidXNlcl9pZCI6InJ3OTg5QUFNemVFaVVMVVMwZ3V2VGJMNHVhTEgiLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImp1bGlhbkBnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MjM4NjAzNDksImV4cCI6MTcyMzg2Mzk0OSwiYXVkIjoiZmlyZWF1dGg1NSIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXJlYXV0aDU1Iiwic3ViIjoicnc5ODlBQU16ZUVpVUxVUzBndXZUYkw0dWFMSCJ9.",
    "refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJydzk4OUFBTXplRWlVTFVTMGd1dlRiTDR1YUxIIiwicHJvdmlkZXIiOiJwYXNzd29yZCIsImV4dHJhQ2xhaW1zIjp7fSwicHJvamVjdElkIjoiZmlyZWF1dGg1NSJ9",
    "expiresIn": "3600"
}

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 👇

JWT Client Server Relationship

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 👇

Authentication logic on the server
inputted_username = "bobno1"
inputted_password = "hunter1"
 
inputted_password_hashed = hash(inputted_password)
database_password = get_password_for_user(inputted_username)
 
if database_password == inputted_password_hashed:
   // yep, it's really Bob
else:
  // it's an imposter! ..or maybe Bob mistyped his password

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..

  1. Bob submits his username and password to log in

  2. The server verifies Bob's credentials. Bob is now authenticated.

  3. The server generates a big random token like SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

  4. 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
  5. The server sends the token back to Bob in the response of his login request as a cookie.

  6. Bob tries to access a protected resource like /find-donuts/, but when he makes this request, he also provides the token.

  7. 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?

Cookies

When Bob authenticated, the server sent him the access token in the form of a cookie 🍪.

What's a cookie?
A cookie is a small piece of data that travels back and fourth between a server and client.

Where do cookies come from?
Cookies may originate from the server or the client. The server can set a cookie via Set-Cookie header field in an HTTP response. The client can set a cookie via document.cookie property.

Where are cookies stored?
Cookies are stored on the client (i.e. in your browser). In Chrome, you can view your cookies under the Application tab of the DevTools window.

cookies

How are cookies sent back to the server?
On every subsequent request that the client makes to the server, the cookies are included in a Cookie HTTP header. This handled automatically by the browser.

When & how do cookies expire?
When the server creates a cookie, it may give it a Max-Age or Expires attribute. This tells the client when to delete the cookie.

Session Cookie

Cookies without a without a Max-Age or Expires attribute are called session cookies. The client deletes these cookies when the "current browser session" ends.

How are cookies protected from tampering?
Generally speaking, cookies are visible to, and can be changed by the end user. However..

  • A cookie with the Secure attribute is only sent to the server with an encrypted request over the HTTPS protocol. It's never sent with unsecured HTTP (except on localhost).
  • A cookie with the HttpOnly attribute can't be modified by JavaScript; it can only be modified when it reaches the server.
  • The Domain attribute specifies which server can receive a cookie.
  • The Path attribute indicates a URL path that must exist in the requested URL.

What about Bob?
By sending Bob an access token in the form of a cookie, there's no manual work necessary for Bob to send it back to the server on subsequent requests. This process is handled automatically by the server.

Furthermore, Bob wouldn't want to store his username and password in a cookie because there's a small chance someone may steal his cookie. And, losing an access token is less dangerous than losing a username and password because access tokens are typically set to expire every few hours or days.

Cool! But there are a couple drawbacks to this approach..

Drawbacks

  1. 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.

  2. 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..

    1. Bob logs into server 1, so server 1 stores his access token.
    2. Bob then makes a request to server 2, attempting to access /find-donuts/.
    3. 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...

  1. The server defines a header as

    header
    {
      "alg": "RS256",
      "typ": "JWT"
    }
  2. The server defines a payload as

    payload
    {
      "username": "bobno1",
      "name": "Bob",
      "expires": 2021-12-30_10:30:05
    }
  3. 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:

  1. A header (as shown above)
  2. A payload (as shown above)
  3. 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?

  1. The server generates a big, random secret key.
  2. The header, payload, and secret key are concatenated into a long string.
  3. 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,

header
{
  "alg": "RS256", 
  "typ": "JWT"
}

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

    1. Sign the JWT and
    2. 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

payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

into a nice compact string like this

Base64 URL encoded payload
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Exercises

When I created a new user in my fireauth application, the server gave me this reponse 👇

{
    "kind": "identitytoolkit#SignupNewUserResponse",
    "localId": "rw989AAMzeEiULUS0guvTbL4uaLH",
    "email": "[email protected]",
    "idToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6Imp1bGlhbkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTcyMzg2MDM0OSwidXNlcl9pZCI6InJ3OTg5QUFNemVFaVVMVVMwZ3V2VGJMNHVhTEgiLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImp1bGlhbkBnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MjM4NjAzNDksImV4cCI6MTcyMzg2Mzk0OSwiYXVkIjoiZmlyZWF1dGg1NSIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXJlYXV0aDU1Iiwic3ViIjoicnc5ODlBQU16ZUVpVUxVUzBndXZUYkw0dWFMSCJ9.",
    "refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJydzk4OUFBTXplRWlVTFVTMGd1dlRiTDR1YUxIIiwicHJvdmlkZXIiOiJwYXNzd29yZCIsImV4dHJhQ2xhaW1zIjp7fSwicHJvamVjdElkIjoiZmlyZWF1dGg1NSJ9",
    "expiresIn": "3600"
}

How might you recognize that the idToken is in fact, a JWT?

There are a few giveaways..

  1. Most JWTs start with ey because that's the base64 encoded form of an open curly brace {
  2. The fact that it's an id token is a hint
  3. 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

Copy the idToken. Then paste it into the token debugger at jwt.io.

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.

eyJhbGciOiJSUzI1NiIsImtpZCI6ImQ0MjY5YTE3MzBlNTA3MTllNmIxNjA2ZTQyYzNhYjMyYjEyODA0NDkiLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiTXlzdGVyaW91cyBVc2VyIiwic3RyaXBlIjp7ImNvbm5lY3QiOnsiZmVlX3BjdCI6MC4xLCJmZWVfYW10IjowLjN9LCJjdXN0b21lciI6eyJzdWJzY3JpcHRpb25zIjpbXSwicHVyY2hhc2VzIjpbXX19LCJxdW90YXMiOnsibWF4X3Byb2R1Y3RzIjoxMDAsIm1heF9wb3N0cyI6MzAwLCJtYXhfZGVwdGgiOjYsIm1heF9maWxlcyI6MTAwMCwibWF4X2ZpbGVfYnl0ZXMiOjUwMDAwMDAsInN0b3JhZ2VfYnl0ZXMiOjEwMDAwMDAwMH0sImlzQWRtaW4iOmZhbHNlLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vc2NpcHJlc3MtcHJvZCIsImF1ZCI6InNjaXByZXNzLXByb2QiLCJhdXRoX3RpbWUiOjE3MjQwMjk4NDksInVzZXJfaWQiOiJKZUdPMm8yeFlGaE4xa044Zm1mWU56MzFXdjYyIiwic3ViIjoiSmVHTzJvMnhZRmhOMWtOOGZtZllOejMxV3Y2MiIsImlhdCI6MTcyNDAyOTg1OSwiZXhwIjoxNzI0MDMzNDU5LCJlbWFpbCI6ImJ1YmJsZXNAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImJ1YmJsZXNAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.IYv3dyLia6cRLub_3iSp3x25LY0NwP7qy8YTSKcU12Q26PJpPxDlkAbIp6OD28Z91G229hUpQH6SmluAoo23L9Svf0t6Cr0wjfnwfJ_Oy0PRSS-h2sEmig31qi-QTinjcCR5FAyPMYURxe5_SIZPtltx2XvpzpVlH_6hfLLIeXiJfO9JGbJ21dbH24jHaiaKU6ws9yxi9Za0phAxi4I3IVs6NyGsYRcEoH6O_iwDMlUoE4ZmVXy_ZQmPN5KAFGQSVKlhtDwa_w_Yvtzj7TrxbuoJjknQBZ-BlxO5a_EIgfig3MdLArzyFceyJ3p-TAdSp4JLOD4P4MDgs6Kokm7B7w

Is it valid or has it been tampered with?

Public Keys

At the time this token was issued, Google provided the following public keys:

{
  "d4269a1730e50719e6b1606e42c3ab32b1280449": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIBAgIIBTK+rT5K9S0wDQYJKoZIhvcNAQEFBQAwMTEvMC0GA1UE\nAwwmc2VjdXJldG9rZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wHhcNMjQw\nODEyMDczMjM0WhcNMjQwODI4MTk0NzM0WjAxMS8wLQYDVQQDDCZzZWN1cmV0b2tl\nbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD\nggEPADCCAQoCggEBAL+hW7yxUOvBp3bI/qeH2gUQjplcNSWiEazsfddaQaq8SxU0\nShgY0n4t4aFnx+e6tdyrSunLJv0jNOQcn27RhjfEWzUT3hTK8iLz8fIRQqflAsUQ\n91iWbOKS1s0yBffd10qvdlABTa1n4o4uSbD2veRDDFcIx9TBM58my2YGJ3i/HG8B\nRVHkkiBqMh4wmGAUEFMrrzB7MdpsZ/aqqFq/69UXhP8/LZFPWWlVCoqzZc5MJuHV\nEDyqp718f2c2m3KjMLuJF074vgxQKu6/F5+dqJz79s5R7S/vq8yPNdiYb3v8H9yt\nO43XOxalc+pSNwxyzLZXgAJpXL62vgZwYTCHnU0CAwEAAaM4MDYwDAYDVR0TAQH/\nBAIwADAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwIwDQYJ\nKoZIhvcNAQEFBQADggEBAG4vYBrYrmH8zn4g+p5s5QEPCce2UKvjt0zDBHmLjp+T\nVOBvYE3TcsHHcCq6WDGIUt20hUNHIgAP/CC7QI7AXy5nZVgpfno7BsqTLmb0LH8Q\n1KGTh+wiepLveuXxEvXhqMvLn48uNXlmI5JiMF7WLnPlD0OsKCTIVZFsJnPg+Ui1\nVlV3BwQf7cGbYasg9RB0PSNaBHXZS6pZyX0Wf5gFmWz9lFchU9VG00i0i0wKwqNY\nI7yrkX2NXm2f+VqpgR3t29YzDu8ySD8osoLf9+sHVrC6VK0PO7yfO+Dd0mKKmsJV\nObsDnhAM4686jOJhCJkX0kWYfFGVP3Yl3vJmk0Rh6UE=\n-----END CERTIFICATE-----\n",
  "ce371730ef86eba29a5212d9b96f3675504f62bc": "-----BEGIN CERTIFICATE-----\nMIIDHTCCAgWgAwIBAgIJALF8j0ucEuz0MA0GCSqGSIb3DQEBBQUAMDExLzAtBgNV\nBAMMJnNlY3VyZXRva2VuLnN5c3RlbS5nc2VydmljZWFjY291bnQuY29tMB4XDTI0\nMDgwNDA3MzIzM1oXDTI0MDgyMDE5NDczM1owMTEvMC0GA1UEAwwmc2VjdXJldG9r\nZW4uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqGSIb3DQEBAQUA\nA4IBDwAwggEKAoIBAQCysG+iTfMSX0fC2tUkvgGUEJQYfvMo4IbyDYF4AzsW92eX\nQpFfL9asYVBY7OTpeV8v0g9+7AxMf2P9tAVxrefM0//+3zBxjthiMRQ/UCyL4FRq\nroDPfsjkkFobTm97A71yD+fyiKunGzg8RfDcTlzvE01YA9uGqhLcynkgodDdkMFu\n89qBgoCjka5zKg8ol47uUo68FaRNEfcOXQ8BLMFDoikYmT1QabzF8RbnUkZpbAf6\nRtTG6ucAyhUqtxjAbLBO28u1bUGLAlMcA4nvcrsDUO/EFxOrm/7rHKm37qkqld1/\nSwNNpIPCCZPV6kXs0btDnR3/VWLey9pHGGg7PnHtAgMBAAGjODA2MAwGA1UdEwEB\n/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMA0G\nCSqGSIb3DQEBBQUAA4IBAQBbAzxOQbBZ9iVPav5duMPqCujLzAt4ombLx7aL6YQJ\n8JfupxHKs917zXhsh0BfJ0II8sM+lKnBnsimQpzIkCXw/rqyBYFkNaF0rpJQ/PdR\n5Uv0hcsvepD8bcI2Aki5tLRbs+0jQev71bCMpKUHKUldddaNz41lvth/k7AGDLic\nILq6LEDBnne2aXkLKDVMMgDFhUm1AT2Sd3hfXWUpfpS89eDD1NsP9RMN40LqXdVI\nWbFa6oLClJhr7w2gB78EL945kkEb7SKFcJ4/iobFAqYJbXaeo8KSGpbBKj2lkzUe\n6G5PUVFGyYtynYjyhB1AK5fI+LLI8aKeYWp5eWufOQ7D\n-----END CERTIFICATE-----\n"
}
  1. Copy and paste the token into the debugger at jwt.io. Note that the decoded header looks like this

    {
      "alg": "RS256",
      "kid": "d4269a1730e50719e6b1606e42c3ab32b1280449",
      "typ": "JWT"
    }
  2. 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 😃

  3. 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!

Verify JWT