タケユー・ウェブ日報

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