Ben Gorman

Ben Gorman

Life's a garden. Dig it.

Here's a little animation showcasing the signup process as we know it thus far.

  1. The user signs up with an email and password.
  2. On form submit, the client sends the email and password to the Firebase Authentication backend.
  3. The Firebase Authentication backend checks the data to make sure everything's kosher. If so, it transmits a response that includes an ID Token in the form of a JWT.
  4. The client parses that response and stores the data in an indexed database.
  5. When the client requests data from third-party services and APIs like Firestore, Cloud Storage, Realtime Database, Vercel, AWS, etc. it may send the user's ID Token.
  6. Third parties that receive the ID token can validate its authenticity and respond accordingly.

What happens if one of those third-party services (e.g. Firestore) get hacked? Couldn't a malicious user steal an ID Token and use it to request other resources?

This would be problem. 😬

Token expiration

To mitigate risk of an ID token being stolen by a malicious user, ID tokens are usually short-lived, meaning, they expire quickly. For example, here's an ID token that Firebase issued to me.

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

How long did this token live before expiring?

1 hour.

  1. Copy and paste the token into the debugger window at jwt.io

  2. Note these two fields in the decoded payload

    payload
    {
      ...
     
      "iat": 1724029859,
      "exp": 1724033459,
     
      ...
    }

    iat is the issued at timestamp in Unix time.
    exp is the expiration timestamp in Unix time.

  3. exp minus iat gives the token's lifespan in seconds.

    So, 3,600 seconds = 1 hour.

So, if an attacker steals this token, they'll only have a short time window to do anything with it.

How is token expiration enforced?
Those third-party APIs and services are supposed to check the token to see if it expired.

However, this presents a new challenge..

If a token expires every hour, wouldn't this mean the user needs to re-log in every hour to get a new, valid token?
Yes! But fortunately, there's a workaround for this..

Refresh tokens

Earlier, when I signed up for an account, the Firebase Authentication server sent me this response.

{
    "kind": "identitytoolkit#SignupNewUserResponse",
    "localId": "rw989AAMzeEiULUS0guvTbL4uaLH",
    "email": "julian@gmail.com",
    "idToken": "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6Imp1bGlhbkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImF1dGhfdGltZSI6MTcyMzg2MDM0OSwidXNlcl9pZCI6InJ3OTg5QUFNemVFaVVMVVMwZ3V2VGJMNHVhTEgiLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbImp1bGlhbkBnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9LCJpYXQiOjE3MjM4NjAzNDksImV4cCI6MTcyMzg2Mzk0OSwiYXVkIjoiZmlyZWF1dGg1NSIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9maXJlYXV0aDU1Iiwic3ViIjoicnc5ODlBQU16ZUVpVUxVUzBndXZUYkw0dWFMSCJ9.",
    "refreshToken": "eyJfQXV0aEVtdWxhdG9yUmVmcmVzaFRva2VuIjoiRE8gTk9UIE1PRElGWSIsImxvY2FsSWQiOiJydzk4OUFBTXplRWlVTFVTMGd1dlRiTDR1YUxIIiwicHJvdmlkZXIiOiJwYXNzd29yZCIsImV4dHJhQ2xhaW1zIjp7fSwicHJvamVjdElkIjoiZmlyZWF1dGg1NSJ9",
    "expiresIn": "3600"
}

Notice the refreshToken.

Like the ID token, the refresh token is base 64 URL encoded. We can easily decode it by dumping into the debugger at jwt.io (even though this token is NOT a JWT).

Refresh token (decoded)
{
  "_AuthEmulatorRefreshToken": "DO NOT MODIFY",
  "localId": "rRy73n1O7AKc8gtbPOc18YBOIx0m",
  "provider": "password",
  "extraClaims": {},
  "projectId": "fireauth55"
}

What is the refresh token for?
When the ID Token expires, the refresh token can be sent to the Firebase Authentication server to get a fresh, new ID Token. In other words, the refresh token lets you refresh the ID token.

Why is this helpful?
Observe the expiration on this refresh token. Unlike the ID token, which expires in an hour, the refresh token expires in two weeks. I.e. the refresh token is a long-lived token. So, the long-lasting refresh token prevents the user from having to re-log in every hour, when the ID token expires.

How is this secure?
Think about where the ID token travels. It moves between the authentication server, the client, Firestore, Cloud Storage, Vercel, and potentially many other third party services. By contrast, the refresh token ONLY travels back and forth between the authentication server and the client. So, it's much less likely to get stolen.