Ruby Firebase ID Token verifier

Actions Status

A Ruby gem to verify the signature of Firebase ID Tokens (JWT). It uses ActiveSupport::Cache to store Google's x509 certificates and manage their expiration time, so you don't need to request Google's API in every execution and can access it as fast as reading from memory.

It also checks the JWT payload parameters as recommended here by Firebase official documentation.

Feel free to open any issue or to contact me directly. Any contribution is welcome.

Docs

Installing

gem install firebase_id_token

or in your Gemfile

gem 'firebase_id_token', '~> 4.0.0'

then

bundle install

Configuration

It's needed to set up your Firebase Project ID.

If you are using Rails, this should probably go into config/initializers/firebase_id_token.rb.

FirebaseIdToken.configure do |config|
  config.cache_store = ActiveSupport::Cache::RedisCacheStore.new
  config.project_ids = ['your-firebase-project-id']
end

You can use the old method of configuration as well. If you use this method, you'll have to proactively download certificates (see Downloading Certificates below)

FirebaseIdToken.configure do |config|
  config.redis = Redis.new
  config.project_ids = ['your-firebase-project-id']
end
  • A cache store instance inheriting from ActiveSupport::Cache::Store must be supplied.
    • Examples:
    • ActiveSupport::Cache::RedisCacheStore.new(Redis.new(host: '10.0.1.1', port: 6380, db: 15))
    • ActiveSupport::Cache::MemoryStore.new(namespace: "firebase_auth")
    • ActiveSupport::Cache::FileStore.new("cache", namespace: "firebase_auth")
    • Rails.cache
  • project_ids must be an Array.

If you want to verify signatures from more than one Firebase project, just add more Project IDs to the list.

Usage

You can get a glimpse of it by reading our RSpec output on your machine. It's really helpful. But here is a complete guide:

Downloading Certificates

Before verifying tokens, you need to download Google's x509 certificates.

To do it, simply:

FirebaseIdToken::Certificates.request

It will download the certificates and save it in cache, but only if the cache certificates database is empty. To force download and override of the cache entries, use:

FirebaseIdToken::Certificates.request!

Google give us information about the certificates' expiration time, it's used to set a cache TTL (Time-To-Live) when saving it. By doing so, the certificates will be automatically deleted after its expiration.

Certificates Info

Checks the presence of certificates in the cache.

FirebaseIdToken::Certificates.present?
=> true

How many seconds until the certificate's expiration. NOTE: Currently only functional when using Redis

FirebaseIdToken::Certificates.ttl
=> 22352

Lists all certificates in the database.

FirebaseIdToken::Certificates.all
=> [{"ec8f292sd30224afac5c55540df66d1f999d" => <OpenSSL::X509::Certificate: [...]]

Finds the respective certificate of a given Key ID (kid).

FirebaseIdToken::Certificates.find('ec8f292sd30224afac5c55540df66d1f999d')
=> <OpenSSL::X509::Certificate: subject=<OpenSSL::X509 [...]>

Downloading in Rails

If you pass in the cache_store configuration option (see configuration), the certificates will be requested at runtime when needed and you can ignore this section.

If you are using Rails, it's clever to download certificates in a cron task, you can use whenever.

Example

Read whenever's guide on how to set it up.

Create your task in lib/tasks/firebase.rake:

namespace :firebase do
  namespace :certificates do
    desc "Request Google's x509 certificates when the cache is empty"
    task request: :environment do
      FirebaseIdToken::Certificates.request
    end

    desc "Request Google's x509 certificates and override the cache"
    task force_request: :environment do
      FirebaseIdToken::Certificates.request!
    end
  end
end

And in your config/schedule.rb you might have:

every 1.hour do
  rake 'firebase:certificates:force_request'
end

Then:

$ whenever --update-crontab

I recommend running it once every hour or every 30 minutes, it's up to you. Normally the certificates expiration time is around 4 to 6 hours, but it's good to perform it in a small fraction of this time.

When developing, you should just run the task:

$ rake firebase:certificates:request

You need Redis to be running if you're using RedisCacheStore

Verifying Tokens

Pass the Firebase ID Token to FirebaseIdToken::Signature.verify and it will return the token payload if everything is ok:

FirebaseIdToken::Signature.verify(token)
=> {"iss"=>"https://securetoken.google.com/firebase-id-token", "name"=>"Bob Test", [...]}

When either the signature is false or the token is invalid, it will return nil:

FirebaseIdToken::Signature.verify(fake_token)
=> nil

FirebaseIdToken::Signature.verify('aaaaaa')
=> nil

Verifying Session Cookies

Firebase Session Cookies are issued by https://session.firebase.google.com and signed by a different certificate set than ID Tokens. Pass type: :session_cookie to verify one:

FirebaseIdToken::Signature.verify(session_cookie, type: :session_cookie)
=> {"iss"=>"https://session.firebase.google.com/firebase-id-token", "name"=>"Bob Test", [...]}

When using the cache_store configuration, the Session Cookie certificates are downloaded automatically on the first verification, just like the ID Token ones. If you are on the legacy Redis configuration and download certificates with a cron task, you also need to request the Session Cookie certificates:

FirebaseIdToken::Certificates.request(source: :session_cookie)

WARNING!

Expired tokens can point to long gone certificates

Notice that often when the token have expired, the Firebase certificate can be already missing from the Firebase servers. In these cases, verify will return nil.

If you want to take specific actions in such cases, here's a solution suggested by the user cfanpnk:

  1. Use verify! to raise an exception.
  2. Rescue FirebaseIdToken::Exceptions::CertificateNotFound and return 401.
  3. The client app will refresh the token if expired.

More details here.

Trying to verify tokens without downloaded certificates will raise an error

If you try to verify a signature without any certificates in the cache, it will raise a FirebaseIdToken::Exceptions::NoCertificatesError.

"I keep on getting nil on verify"

Poorly synchronized clocks will sometimes make the server think the token's iat is on the future, which will render the token as invalid. Make sure your server's or development system's clock is correctly set. On macOS, some people reported success by unchecking and checking the "Set date and time automatically" configuration checkbox. See here.

Payload Structure

In case you need, here's a example of the payload structure from a Google login in JSON.

{
   "iss":"https://securetoken.google.com/{{YOUR_FIREBASE_APP_ID}}",
   "name":"Ugly Bob",
   "picture":"https://someurl.com/photo.jpg",
   "aud":"{{YOUR_FIREBASE_APP_ID}}",
   "auth_time":1492981192,
   "user_id":"theUserID",
   "sub":"theUserID",
   "iat":1492981200, // needs to be in the past
   "exp":33029000017, // needs to be in the future
   "email":"uglybob@emailurl.com",
   "email_verified":true,
   "firebase":{
      "identities":{
         "google.com":[
            "1010101010101010101"
         ],
         "email":[
            "uglybob@emailurl.com"
         ]
      },
      "sign_in_provider":"google.com"
   }
}

If you're using this snippet for testing, make sure you check the comments in it.

Testing

bundle exec rake rspec

Testing Mode

Just run:

FirebaseIdToken.test!

Warning FirebaseIdToken.test! replaces signature verification with a fixture certificate whose private key is publicly available in this repository. Never call it outside of your test suite. An application running in test mode will accept forged tokens crafted by anyone.

By using this mode, the following methods become available.

# RSA PRIVATE KEY
FirebaseIdToken::Testing::Certificates.private_key

# CERTIFICATE
FirebaseIdToken::Testing::Certificates.certificate

certificate will always return the same value. No external HTTP call is performed.

Example: Testing in Rails

Describes the following in test_helper.rb.

class ActiveSupport::TestCase
  setup do
    FirebaseIdToken.test!
  end
end

Test example:

require 'test_helper'

module Api
  module V1
    module UsersControllerTest < ActionController::TestCase
      setup do
        @routes = Engine.routes
        @user = users(:one)
      end

      def create_token(sub: nil)
        _payload = payload.merge({sub: sub})
        JWT.encode _payload, OpenSSL::PKey::RSA.new(FirebaseIdToken::Testing::Certificates.private_key), 'RS256'
      end

      def payload
        # payload.json
      end

      test 'should success get api v1 users ' do
        get :show, headers: create_token(@user.id)
        assert_response :success
      end
    end
  end
end

License

The gem is available as open source under the terms of the MIT License.