module FirebaseIdtoken

Constants

ALGORITHM
CLIENT_CERT_URL
ISSUER_BASE_URL
VERSION

Attributes

configuration[RW]

Public Class Methods

configure() { |configuration| ... } click to toggle source
# File lib/firebase_idtoken.rb, line 27
def configure
  yield configuration
end
verify(token) click to toggle source
# File lib/firebase_idtoken.rb, line 35
def verify(token)
  raise 'id token must be a String' unless token.is_a?(String)

  full_decoded_token = decode_token(token)

  err_msg = validate_jwt(full_decoded_token)
  raise err_msg if err_msg

  public_key = fetch_public_keys[full_decoded_token[:header]['kid']]
  unless public_key
    raise 'Firebase ID token has "kid" claim which does not correspond to ' +
      'a known public key. Most likely the ID token is expired, so get a fresh token from your client ' +
      'app and try again.'
  end

  certificate = OpenSSL::X509::Certificate.new(public_key)
  decoded_token = decode_token(token, certificate.public_key, true, { algorithm: ALGORITHM, verify_iat: true })

  {
    'uid' => decoded_token[:payload]['sub'],
    'decoded_token' => decoded_token
  }
end

Private Class Methods

decode_token(token, key=nil, verify=false, options={}) click to toggle source
# File lib/firebase_idtoken.rb, line 62
def decode_token(token, key=nil, verify=false, options={})
  begin
    decoded_token = JWT.decode(token, key, verify, options)
  rescue JWT::ExpiredSignature => e
    raise 'Firebase ID token has expired. Get a fresh token from your client app and try again.'
  rescue => e
    raise "Firebase ID token has invalid signature. #{e.message}"
  end

  {
    payload: decoded_token[0],
    header: decoded_token[1]
  }
end
fetch_public_keys() click to toggle source
# File lib/firebase_idtoken.rb, line 77
def fetch_public_keys
  cache_path = configuration.cache_path
  cache_time = configuration.cache_time
  data = nil
  begin
    # Fetch from cache if within an hour (UTC timezone)
    s = File.stat(cache_path)
    File.delete(cache_path) if (Time.now.utc - s.mtime.utc) > cache_time
    data = JSON.parse(open(cache_path).read)
  rescue => error
    puts "#{error.message}: Fetching public key from Google..."

    # Fetch keys from Google if no cache or after an hour
    uri = URI.parse(CLIENT_CERT_URL)
    https = Net::HTTP.new(uri.host, uri.port)
    https.use_ssl = true

    res = https.start {
      https.get(uri.request_uri)
    }
    json_string = res.body
    data = JSON.parse(json_string)

    if (data['error']) then
      msg = 'Error fetching public keys for Google certs: ' + data['error']
      msg += " (#{res['error_description']})" if (data['error_description'])
      raise msg
    else
      save_cache_file(json_string)
    end
  end
  data
end
save_cache_file(json_string) click to toggle source
# File lib/firebase_idtoken.rb, line 111
def save_cache_file(json_string)
  cache_path = configuration.cache_path
  dirname = File.dirname(cache_path)
  unless File.directory?(dirname)
    puts "Creating a directory: #{dirname}"
    FileUtils.mkdir_p(dirname)
  end
  open cache_path, 'w' do |io|
    io.write json_string
  end
end
validate_jwt(json) click to toggle source
# File lib/firebase_idtoken.rb, line 123
def validate_jwt(json)
  project_id = configuration.project_id
  raise 'You need to set Firebase project ID' unless project_id

  payload = json[:payload]
  header = json[:header]

  return 'Firebase ID token has no "kid" claim.' unless header['kid']
  return "Firebase ID token has incorrect algorithm. Expected \"#{ALGORITHM}\" but got \"#{header['alg']}\"." unless header['alg'] == ALGORITHM
  return "Firebase ID token has incorrect \"aud\" (audience) claim. Expected \"#{project_id}\" but got \"#{payload['aud']}\"." unless payload['aud'] == project_id

  issuer = ISSUER_BASE_URL + project_id
  return "Firebase ID token has incorrect \"iss\" (issuer) claim. Expected \"#{issuer}\" but got \"#{payload['iss']}\"."  unless payload['iss'] == issuer

  return 'Firebase ID token has no "sub" (subject) claim.' unless payload['sub'].is_a?(String)
  return 'Firebase ID token has an empty string "sub" (subject) claim.'  if payload['sub'].empty?
  return 'Firebase ID token has "sub" (subject) claim longer than 128 characters.' if payload['sub'].size > 128

  nil
end