Verify Firebase auth JWT with Ruby

May 15, 2021 FirebaseRuby

Firebase Authentication is an easy-to-use service to authenticate users to your app. It comes with a bunch of auth providers from traditional email/password authentication to password-less mobile number authentication. Once a user is authenticated, Firebase will give us a JWT which contains the user info. Firebase provides SDKs for verious languages which can be used for verifying the JWT on your backend server, however, there is no official SDK for Ruby. In this post, I will show you how to decode and verify Firebase JWT with Ruby.

Overall flow

  1. The client app retrieves the Firebase ID Token (JWT) and send it to the backend server through HTTP header
  2. The backend server decodes and verifies the JWT following this guide

I'm not going to cover how to retrieve the token on the client side since it's very straightforward.

I created FirebaseToken class with verify() method which decodes and verifies the JWT and then returns the docoded payload which contains the authenticated user data. You can find the full codebase here.

Verify the signing algorithm in the header

First, we are going to verify the signing algorithm which is specified in the JWT header. Firebase uses RS256 hash algorithm for signing. We need to decode the header to retrieve the algorithm used for the JWT.

def decode_header
  encoded_header = @token.split('.').first
  JSON.parse(Base64.decode64(encoded_header))
end

Now, we can retrieve the alg from the header and check if it's expected algorithm.

JWT_ALGORITHM = 'RS256'.freeze
header = decode_header
alg = header['alg']

if alg != JWT_ALGORITHM
  raise "Invalid token 'alg' header (#{alg}). Must be '#{JWT_ALGORITHM}'."
end

Download the public key for verifying the JWT

Next, we will fetch the public key to verify that the token is signed by the right private key. Grab the public keys from here and find the one which corresponds to the kid in the JWT header.

def get_public_key(kid)
  response = HTTParty.get(PUBLIC_KEY_URL)

  unless response.success?
    raise "Failed to fetch JWT public keys from Google."
  end

  public_keys = response.parsed_response

  # TODO: cache public_keys to avoid downloading it every time.
  # Use the cache-control header for TTL.

  unless public_keys.include?(kid)
    raise "Invalid token 'kid' header, do not correspond to valid public keys."
  end

  OpenSSL::X509::Certificate.new(public_keys[kid]).public_key
end

As commented, it would be a good practice to cache the public keys to avoid downloading them everytime. For the cache TTL, you can use the max-age value in the Cache-Control header of the response. It's up to you how to implement the caching logic. e.g. caching them in Redis

Verify the JWT with the public key

Finally, we can verify the token using the public key. We will use ruby-jwt gem for this. We can pass a bunch of verification options to JWT.decode method. We will specify those options according to Firebase docs.

def verify_jwt(token, public_key, firebase_project_id)
  options = {
    algorithm: JWT_ALGORITHM,
    verify_iat: true,
    verify_aud: true,
    aud: firebase_project_id,
    verify_iss: true,
    iss: "https://securetoken.google.com/#{firebase_project_id}"
  }

  JWT.decode(token, public_key, true, options)
end

You can find the full codebase here. If you've got any questions or comments, please comment in this Twitter thread!

avatar

About Me

I am a software engineer from Japan and currently based in Singapore. Over the past 5 years, I've been creating e-commerce and FinTech web applications using Laravel, Angular, and Vue.js. Making clean, maintainable, and scalable software with agile methodology is one of my biggest passions. Im my spare time, I play the bass and enjoy DIY.