Posted on: Written by: K-Sato
⚠️ This article was posted over a year go. The information might be outdated. ⚠️

Table of Contents

認証流れ整理

(1) クライアントでEmail, Passwrordを入力し、Firebaseでユーザー検証 (Email, Passwordが違ったらログインできない)
→
(2) Email, PasswordがOKならTokenがFirebaseから渡される。
(この時点でアプリケーションはユーザーが存在する事を保証)
→
(3) 受け取ったTokenと共にリクエストをRailsに送る。
→
(4) RailsでTokenの検証(受け取ったTokenが変更されてないか検証)
(JWTのヘッダー、ペイロードが改変されていると、発行元Firebaseの署名と不一致になる)
=
!!! Tokenが正しい秘密鍵から作られているか公開鍵で検証(この場合はFirebaseの秘密鍵から作られたTokenか、Firebaseの公開鍵から検証) !!

Firebase から受け取るトークンの検証

Ruby は Firebase の公式 AdminSDK が存在しないので、下記の記事内のような方法でトークンの検証を行う。 記事を見ればわかるが、トークンの検証方法自体はそこまで複雑ではない。

Ruby で Firebase の id トークンを認証に使ってみる - Qiita

ちなみに公式 AdminSDK(Node)の認証箇所は下記。

https://github.com/firebase/firebase-admin-node/blob/master/src/auth/auth.ts

require 'jwt'
require 'yaml'

module FirebaseUtils
  CONFIG = YAML.load_file("firebase_config.yml")

  module Auth
    ALGORITHM = 'RS256'
    ISSUER_BASE_URL = 'https://securetoken.google.com/'
    CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
    class << self
      def verify_id_token(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
      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

      def _fetch_public_keys
        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)
        }
        data = JSON.parse(res.body)

        if (data['error']) then
          msg = 'Error fetching public keys for Google certs: ' + data['error']
          msg += " (#{res['error_description']})" if (data['error_description'])

          raise msg
        end

        data
      end

      def _validate_jwt(json)
        project_id = FirebaseUtils::CONFIG[:project_info][: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
    end
  end
end

参考

About the author

I am a web-developer based somewhere on earth. I primarily code in TypeScript, Go and Ruby at work. React, RoR and Gin are my go-to Frameworks.