Skip to content

Instantly share code, notes, and snippets.

@serradura
Last active August 22, 2024 02:52
Show Gist options
  • Save serradura/40a4f05f424262a94f44997681f02d26 to your computer and use it in GitHub Desktop.
Save serradura/40a4f05f424262a94f44997681f02d26 to your computer and use it in GitHub Desktop.
FirebaseAdmin::Auth.verify_id_token | Ruby solution for https://firebase.google.com/docs/auth/admin/verify-id-tokens
# Usage:
# ========
# FirebaseAdmin::Auth.verify_id_token(your_id_token)
#
# The method call follows the same API of the Node.js, JAVA and Python SDKs.
# See https://firebase.google.com/docs/auth/admin/verify-id-tokens#verify_id_tokens_using_the_firebase_admin_sdk
# Dependencies:
# ---------------
# gem 'activesupport'
# gem 'httparty', '~> 0.14.0'
# gem 'jwt', '~> 1.5', '>= 1.5.6'
# require 'jwt'
# require 'httparty'
# require 'active_support/core_ext/module/delegation'
#
# require 'openssl'
# require 'singleton'
# require 'ostruct'
module FirebaseAdmin
class PublicKeys
URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
EXPIRES_HEADER = 'expires'
attr_reader :response, :data
delegate :keys, :values, to: :data
def initialize
@response = fetch
end
def valid?
Time.now.utc < time_to_expire
end
def data
@response.as_json
end
private
def time_to_expire
@time_to_expire ||= Time.parse(
response.headers[EXPIRES_HEADER]
)
end
def fetch
HTTParty.get(URL)
end
end
class IDTokenVerifier
JWT_OPTIONS = { algorithm: 'RS256', verify_iat: true }
attr_reader :certificates
def initialize(public_keys)
@public_keys = public_keys
@certificates = map_certificates
end
def verify(id_token)
result = nil
certificates.each do |x509|
result = decode_jwt(id_token, x509)
break if result
end
result
end
private
def decode_jwt(id_token, x509)
JWT.decode(id_token, x509.public_key, true, JWT_OPTIONS)
rescue JWT::VerificationError
nil
end
def map_certificates
@public_keys.values.map do |credential|
OpenSSL::X509::Certificate.new(credential)
end
end
end
class Auth
include Singleton
def initialize
refresh
end
def public_keys
resolve { @public_keys }
end
def verify_id_token(id_token)
result = resolve { @id_token_verifier.verify(id_token) }
if result
payload, header = result
[ OpenStruct.new(payload), OpenStruct.new(header) ]
end
end
class << self
delegate :verify_id_token, :public_keys, to: :instance
end
private
def refresh
@public_keys = PublicKeys.new
@id_token_verifier = IDTokenVerifier.new(@public_keys)
end
def resolve
refresh unless @public_keys.valid?
yield
end
end
end
@midnight-wonderer
Copy link

FYI certificate map can be looked up with kid from the JWT header.
To utilize the optimization you have to decode the token without verification once for the kid.

Here my enhanced version
https://gist.github.com/MidnightWonderer/c40b8c46dc42cb560ccbdcd4a79f52c9

@sabiou
Copy link

sabiou commented Aug 21, 2024

Any update on this??

@midnight-wonderer
Copy link

Even after all those years, not much has changed. My link is dead, but the original example is still solid.
This example has a workable solution but is not complete.

To complete your implementation, you may want to read the official document. It describes all fields that need validation and the kid trick I mentioned.

This gist is a stripped-down version for newcomers to JWT, so it is easier to grasp. It is a good start, but more needs to be added to the production code. You can't be wrong with the official document.

P.S. I still use Firebase Auth in Ruby projects. It is one of the most trusted, free solutions around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment