Skip to content

Instantly share code, notes, and snippets.

@serradura
Last active February 10, 2022 18:45
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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

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