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

# これはなに？
モバイル端末のapp側で Sign in with Apple を実施、ここで入手したトークンをサーバ側で検証する方法についてのメモ。
![スクリーンショット 2020-01-06 15.42.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86923/5184e0d5-8d86-9a0d-ec77-8feb83d6c289.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` を使用して対応するレコードを絞り込む。

```ruby: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` で例外が発生する。

```ruby: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 に等しいことを検査。