20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Sign in with AppleのJWTをサーバーサイドで検証する

Last updated at Posted at 2020-01-06

これはなに?

モバイル端末のapp側で Sign in with Apple を実施、ここで入手したトークンをサーバ側で検証する方法についてのメモ。
スクリーンショット 2020-01-06 15.42.25.png

環境

  • ruby: 2.6
  • rails: 5.3
  • jwt: 2.1

概要

サーバ側での実施項目は以下。

  • 公開鍵を取得、OpenSSL::PKey::RSAを作成する
  • JWTをデコードする
  • 内容を検証する

公開鍵を取得、OpenSSL::PKey::RSAを作成する

https://appleid.apple.com/auth/keysから公開かぎを取得、作成する。
このURLをGETすると、複数レコードが返信される。
ここから、クライアントから送られてくる kid を使用して対応するレコードを絞り込む。

publick_key.rb
module Apple
  class PublicKey
    def create_key(kid)
      OpenSSL::PKey::RSA.new(sequence(kid).to_der)
    end

    private

    def sequence(kid)
      OpenSSL::ASN1::Sequence.new([
        OpenSSL::ASN1::Integer.new(openssl_bn(modules(kid))),
        OpenSSL::ASN1::Integer.new(openssl_bn(exponent(kid)))
      ])
    end

    def openssl_bn(n)
      n = n + '=' * (4 - n.size % 4) if n.size % 4 != 0
      decoded = Base64.urlsafe_decode64 n
      unpacked = decoded.unpack('H*').first
      OpenSSL::BN.new unpacked, 16
    end

    def modules(kid)
      public_key(kid)&.[]("n")
    end

    def exponent(kid)
      public_key(kid)&.[]("e")
    end

    def public_key(kid)
      public_keys_json["keys"].each do |record|
        return record if record["kid"] === kid
      end
    end

    def public_keys_json
      @public_keys_json ||= JSON.parse(public_keys)
    end

    def public_keys
      @public_keys ||= HTTPClient.new.get(Settings.idps.apple.public_key.url).body
      # url: https://appleid.apple.com/auth/keys
    end
  end
end

JWTをデコードする

クライアント側から送られる JWT デコードする。
まず、検証なしでデコードし kid を取り出す。
これを使用して、公開鍵を取得。
この公開鍵を使用して、検証しながらデコードする。

access_token_analyser.rb
module Apple
  class AccessTokenAnalyser
    def initialize(access_token)
      @access_token = access_token
    end

    def decode_token
      JWT.decode(@access_token, rsa_public_key, true, { algorithm: 'RS256' })
    rescue => e
      Rails.logger.info %(exception occured. #{e.message})
      nil
    end

    private

    def kid_from_token
      decode_token_without_verify&.[](1)&.[]("kid")
    end

    def decode_token_without_verify
      @decode_token_without_verify ||= JWT.decode(@access_token, nil, false)
    rescue => e
      Rails.logger.info %(exception occured. #{e.message})
      nil
    end

    def rsa_public_key
      Apple::PublicKey.new.create_key(kid_from_token)
    end
  end
end

検証する

appleのdocumentによると、以下の項目を検証すべしとある。
https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user

To verify the identity token, your app server must:

  • Verify the JWS E256 signature using the server’s public key
  • Verify the nonce for the authentication
  • Verify that the iss field contains https://appleid.apple.com
  • Verify that the aud field is the developer’s client_id
  • Verify that the time is earlier than the exp value of the token

今回、nonceは使用していないので、それ以外を検証する。
期限切れの場合、JWT.decode で例外が発生する。

apple_login_service.rb
module Apple
  class AppleLoginService
    def verify_token(params)
      verify_access_token_local(params[:access_token])
    rescue => e
      Rails.logger.info %(exception occured. #{e.message})
      false
    end

    private

    def verify_access_token_local(access_token)
      access_token_analyser = Apple::AccessTokenAnalyser.new(access_token)
      unless token_json = access_token_analyser.decode_token
        return false
      end

      unless verify_iss(token_json.first['iss'])
        return false
      end

      unless verify_aud(token_json.first['aud'])
        return false
      end
      token_json.first['sub']
    end

    def verify_iss(iss)
      iss == 'https://appleid.apple.com'
    end

    def verify_aud(aud)
      aud == aud_value
    end

    def aud_value
      (Rails.env.development?)? 'host.exp.Exponent' : 'myapp'  # myapp value
    end
  end
end

audの検査

今回、クライアントの開発に expo.io を使用している。
この場合、開発環境とその他の環境(expo client を使用するか否か)で、aud の値が異なる。
開発環境は expo client で host.exp.Exponent
それ以外の場合は、Apple developer console の Certificates, Identifiers & Profiles > Identifiers で定義した IDENTIFIER に等しいことを検査。

20
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?