タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

RubyでFirebase Authenticationトークンを検証するサンプル

認証をFirebase Authenticationに丸投げし、そちらで作ったトークンをサーバーに送信、認可を行う時に有効

FIREBASE_PROJECT_ID = "FIREBASEPROJECTID"
CIRTIFICATE_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
EXP_LEEWAY = 30.seconds
VALID_ISS = "https://securetoken.google.com/#{FIREBASE_PROJECT_ID}"

CERTIFICATE_MAP = JSON.parse(Net::HTTP.get_response(URI.parse(CIRTIFICATE_URL)).body)

TokenVerifyFailed = Class.new(StandardError)
InvalidAuthTime = Class.new(TokenVerifyFailed)

def from_firebase(id_token)
  # https://firebase.google.com/docs/auth/admin/verify-id-tokens?hl=ja
  a, decoded_token_header = JWT.decode(id_token, nil, false)

  uri = URI.parse(CIRTIFICATE_URL)
  certificate = CERTIFICATE_MAP.fetch(decoded_token_header["kid"])

  public_key = OpenSSL::X509::Certificate.new(certificate).public_key
  decoded_token_payload, _ = JWT.decode(
    id_token,
    public_key,
    true,
    exp_leeway: EXP_LEEWAY,   # 有効期限の検証をするが、ゆるめに。 EXP_LEEWAY 秒は大目に見る。
    verify_iat: true,         # 発行時の検証をする
    aud: FIREBASE_PROJECT_ID,
    verify_aud: true,         # 対象の検証をする
    iss: VALID_ISS,
    verify_iss: true,         # 発行元の検証をする
    verify_sub: true,         # 件名の存在を検証する
    algorithm: decoded_token_header["alg"]
  )

  raise InvalidAuthTime.new('Invalid auth_time') unless Time.zone.at(decoded_token_payload['auth_time']).past?

  # decoded_token_payload = {
  #   "name"=>"Takeuchi Yuichi",
  #   "picture"=>"https://lh3.googleusercontent.com/a-/AAuE7mAZU7Rh7lIFStzfWGe3tC24qDIX4UIoEWR8426flA",
  #   "iss"=>"https://securetoken.google.com/rails-firebase-sample",
  #   "aud"=>"rails-firebase-sample",
  #   "auth_time"=>1580712233,
  #   "user_id"=>"Qgk3sd1HgoPLVbSy8uXAWnRmWmx1",
  #   "sub"=>"Qgk3sd1HgoPLVbSy8uXAWnRmWmx1",
  #   "iat"=>1580712233,
  #   "exp"=>1580715833,
  #   "email"=>"yuichi.takeuchi@takeyuweb.co.jp",
  #   "email_verified"=>true,
  #   "firebase"=>{
  #     "identities"=>{
  #       "google.com"=>["100008179958237311525"],
  #       "email"=>["yuichi.takeuchi@takeyuweb.co.jp"]
  #     },
  #     "sign_in_provider"=>"google.com"
  #   }
  # }

  decoded_token_payload
end

Railsにおける利用例

例えばこんな感じで、

  • Bearer トークンとして受け取って、対応する User モデルを作る/返す
  • セッションに記録されたものがあればそれを使う

みたいなことができる

  def current_user
    return @current_user if defined?(@current_user)

    # fallback
    if session[:authorization].present? && request.headers['Authorization'].blank?
      request.headers['Authorization'] = "Bearer #{session[:authorization]}";
    end

    user = authenticate_with_http_token do |id_token, options|
      User.from_firebase(id_token)
    rescue JWT::DecodeError, User::TokenVerifyFailed => e
      Rails.logger.error(e)
      nil
    end
    if user&.persisted?
      @current_user = user
    else
      @current_user = nil
    end
  end