diff --git a/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb b/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb index aaf0386ff7f..8e557f8cd86 100644 --- a/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +++ b/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb @@ -56,7 +56,8 @@ def self.available_options description: "The key ID"), FastlaneCore::ConfigItem.new(key: :issuer_id, env_name: "APP_STORE_CONNECT_API_KEY_ISSUER_ID", - description: "The issuer ID"), + description: "The issuer ID. It can be nil if the key is individual API key", + optional: true), FastlaneCore::ConfigItem.new(key: :key_filepath, env_name: "APP_STORE_CONNECT_API_KEY_KEY_FILEPATH", description: "The path to the key p8 file", diff --git a/spaceship/lib/spaceship/connect_api/token.rb b/spaceship/lib/spaceship/connect_api/token.rb index e0e1d369763..b67f677be3b 100644 --- a/spaceship/lib/spaceship/connect_api/token.rb +++ b/spaceship/lib/spaceship/connect_api/token.rb @@ -39,7 +39,6 @@ def self.from_json_file(filepath) missing_keys = [] missing_keys << 'key_id' unless json.key?(:key_id) - missing_keys << 'issuer_id' unless json.key?(:issuer_id) missing_keys << 'key' unless json.key?(:key) unless missing_keys.empty? @@ -99,12 +98,18 @@ def refresh! } payload = { - iss: issuer_id, # Reduce the issued-at-time in case our time is slighly ahead of Apple's servers, which causes the token to be rejected. iat: now.to_i - 60, exp: @expiration.to_i, aud: 'appstoreconnect-v1' } + if issuer_id + payload[:iss] = issuer_id + else + # Consider the key as individual key. + # https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#4313913 + payload[:sub] = 'user' + end @text = JWT.encode(payload, @key, 'ES256', header) end diff --git a/spaceship/spec/connect_api/token_spec.rb b/spaceship/spec/connect_api/token_spec.rb index 946f0d74c9d..9a2c10abab3 100644 --- a/spaceship/spec/connect_api/token_spec.rb +++ b/spaceship/spec/connect_api/token_spec.rb @@ -68,7 +68,7 @@ file.close expect do Spaceship::ConnectAPI::Token.from_json_file(file.path) - end.to raise_error("App Store Connect API key JSON is missing field(s): key_id, issuer_id, key") + end.to raise_error("App Store Connect API key JSON is missing field(s): key_id, key") end it 'raises error with missing key' do @@ -151,6 +151,22 @@ expect(token.duration).to eq(200) expect(token.in_house).to eq(true) end + + it "without issuer_id" do + expect(File).to receive(:binread).with('/path/to/file').and_return(private_key) + token = Spaceship::ConnectAPI::Token.create( + key_id: "key_id", + filepath: "/path/to/file", + duration: 200, + in_house: true + ) + + expect(token.key_id).to eq('key_id') + expect(token.issuer_id).to be_nil + expect(token.text).not_to(be_nil) + expect(token.duration).to eq(200) + expect(token.in_house).to eq(true) + end end describe 'with environment variables' do @@ -196,7 +212,7 @@ end context 'init' do - it 'generates proper token' do + it 'generates proper team token' do key = OpenSSL::PKey::EC.generate('prime256v1') token = Spaceship::ConnectAPI::Token.new(key_id: key_id, issuer_id: issuer_id, key: key) expect(token.key_id).to eq(key_id) @@ -205,6 +221,27 @@ payload, header = JWT.decode(token.text, key, true, { algorithm: 'ES256' }) expect(payload['iss']).to eq(issuer_id) + expect(payload['sub']).to be_nil + + expect(payload['iat']).to be < Time.now.to_i + expect(payload['aud']).to eq('appstoreconnect-v1') + expect(payload['exp']).to be > Time.now.to_i + + expect(header['kid']).to eq(key_id) + expect(header['typ']).to eq('JWT') + end + + it 'generates proper individual token' do + key = OpenSSL::PKey::EC.generate('prime256v1') + token = Spaceship::ConnectAPI::Token.new(key_id: key_id, key: key) + expect(token.key_id).to eq(key_id) + expect(token.issuer_id).to be_nil + + payload, header = JWT.decode(token.text, key, true, { algorithm: 'ES256' }) + + expect(payload['sub']).to eq('user') + expect(payload['iss']).to be_nil + expect(payload['iat']).to be < Time.now.to_i expect(payload['aud']).to eq('appstoreconnect-v1') expect(payload['exp']).to be > Time.now.to_i