これはなに?
モバイル端末のapp側で Sign in with Apple を実施、ここで入手したトークンをサーバ側で検証する方法についてのメモ。
環境
- 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
を使用して対応するレコードを絞り込む。
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
を取り出す。
これを使用して、公開鍵を取得。
この公開鍵を使用して、検証しながらデコードする。
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
で例外が発生する。
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 に等しいことを検査。