restforce 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e4311062c172c1fd0706872045d29fed90d5b36df5f6c2656d88c37ac680fb2
4
- data.tar.gz: f8b6e07bd7533342229f0bb02ef00a731dda64990927bc2c8d822fbfa46f5a29
3
+ metadata.gz: c4d4058f81fe1d435976494baa2c81caa57515d21f1ffcb0784286d5a608af39
4
+ data.tar.gz: ddd80cd062c00f2c9d249e63e2cec5103da55a225c42d443e4370c2d234fae36
5
5
  SHA512:
6
- metadata.gz: 33d02a24a1fc9e7efd40a318f75a48992215b759bcb33c8860a0e4e49fbc5a2eac13cce908dac4605164af58fa6e43473b753668926e0875deeba63eac4cc51c
7
- data.tar.gz: 1a6155b0ec55633ebf061e7e709b7e391fe266563f5476b658040139e977ea00baf356602aacf8c5b2aef313c0b48296bfad984d444fe437249424810b567512
6
+ metadata.gz: 2b37491f67c8f1494aa849b6118c6b5d0bcfae35ebc5f8e313c68d1989fd4fda486ff661a8dfb74181526958b6baa8cff2b477b7297bdeb9bf98a6e44becfb1f
7
+ data.tar.gz: c046776763a2d2bc13890f6c64241052d25d8e1ded812770ef99aa7df407662065f9bbc349c94d902a66cf71d400770b63dd964a355ea9d2cd57cffe3500156f
@@ -1,3 +1,7 @@
1
+ ## 4.1.0 (Oct 20, 2019)
2
+
3
+ * Add support for JWT authentication (@nathanKramer, @tagCincy)
4
+
1
5
  ## 4.0.0 (Oct 9, 2019)
2
6
 
3
7
  * __Deprecate support for Ruby 2.3__, since [Ruby 2.3 reached its end-of-life](https://www.ruby-lang.org/en/news/2019/03/31/support-of-ruby-2-3-has-ended/) in March 2019. (This is the only breaking change included in this version.)
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ gemspec
5
5
 
6
6
  gem 'faraday', '~> 0.17.0'
7
7
  gem 'jruby-openssl', platforms: :jruby
8
+ gem 'jwt'
8
9
  gem 'rake'
9
10
 
10
11
  group :development do
data/README.md CHANGED
@@ -25,7 +25,7 @@ Features include:
25
25
 
26
26
  Add this line to your application's Gemfile:
27
27
 
28
- gem 'restforce', '~> 4.0.0'
28
+ gem 'restforce', '~> 4.1.0'
29
29
 
30
30
  And then execute:
31
31
 
@@ -111,6 +111,21 @@ client = Restforce.new(username: 'foo',
111
111
  api_version: '41.0')
112
112
  ```
113
113
 
114
+ #### JWT Bearer Token
115
+
116
+ If you prefer to use a [JWT Bearer Token](https://developer.salesforce.com/page/Digging_Deeper_into_OAuth_2.0_on_Force.com#Obtaining_an_Access_Token_using_a_JWT_Bearer_Token) to authenticate:
117
+
118
+ ```ruby
119
+ client = Restforce.new(username: 'foo',
120
+ client_id: 'client_id',
121
+ instance_url: 'instance_url',
122
+ jwt_key: 'certificate_private_key',
123
+ api_version: '38.0')
124
+ ```
125
+
126
+ The `jwt_key` option is the private key of the certificate uploaded to your Connected App in Salesforce.
127
+ Choose "use digital signatures" in the Connected App configuration screen to upload your certificate.
128
+
114
129
  You can also set the username, password, security token, client ID, client
115
130
  secret and API version in environment variables:
116
131
 
@@ -473,11 +488,11 @@ document = client.query('select Id, Name, Body from Document').first
473
488
  File.open(document.Name, 'wb') { |f| f.write(document.Body) }
474
489
  ```
475
490
 
476
- **Note:** The example above is only applicable if your SOQL query returns a single Document record. If more than one record is returned,
491
+ **Note:** The example above is only applicable if your SOQL query returns a single Document record. If more than one record is returned,
477
492
  the Body field contains an URL to retrieve the BLOB content for the first 2000 records returned. Subsequent records contain the BLOB content
478
- in the Body field. This is confusing and hard to debug. See notes in [Issue #301](https://github.com/restforce/restforce/issues/301#issuecomment-298972959) explaining this detail.
479
- **Executive Summary:** Don't retrieve the Body field in a SOQL query; instead, use the BLOB retrieval URL documented
480
- in [SObject BLOB Retrieve](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_blob_retrieve.htm)
493
+ in the Body field. This is confusing and hard to debug. See notes in [Issue #301](https://github.com/restforce/restforce/issues/301#issuecomment-298972959) explaining this detail.
494
+ **Executive Summary:** Don't retrieve the Body field in a SOQL query; instead, use the BLOB retrieval URL documented
495
+ in [SObject BLOB Retrieve](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_blob_retrieve.htm)
481
496
 
482
497
  * * *
483
498
 
@@ -574,14 +589,14 @@ There are two magic values for the replay ID accepted by Salesforce:
574
589
 
575
590
  **Warning**: Only use a replay ID of a event from the last 24 hours otherwise
576
591
  Salesforce will not send anything, including newer events. If in doubt, use one
577
- of the two magic replay IDs mentioned above.
592
+ of the two magic replay IDs mentioned above.
578
593
 
579
- You might want to store the replay ID in some sort of datastore so you can
594
+ You might want to store the replay ID in some sort of datastore so you can
580
595
  access it, for example between application restarts. In that case, there is the
581
596
  option of passing a custom replay handler which responds to `[]` and `[]=`.
582
597
 
583
598
  Below is a sample replay handler that stores the replay ID for each channel in
584
- memory using a Hash, stores a timestamp and has some rudimentary logic that
599
+ memory using a Hash, stores a timestamp and has some rudimentary logic that
585
600
  will use one of the magic IDs depending on the value of the timestamp:
586
601
 
587
602
  ```ruby
@@ -637,9 +652,9 @@ EM.run {
637
652
  puts message.inspect
638
653
  end
639
654
  }
640
- ```
655
+ ```
641
656
 
642
- _See also_:
657
+ _See also_:
643
658
 
644
659
  * [Force.com Streaming API docs](http://www.salesforce.com/us/developer/docs/api_streaming/index.htm)
645
660
  * [Message Durability docs](https://developer.salesforce.com/docs/atlas.en-us.api_streaming.meta/api_streaming/using_streaming_api_durability.htm)
@@ -3,6 +3,7 @@
3
3
  require 'faraday'
4
4
  require 'faraday_middleware'
5
5
  require 'json'
6
+ require 'jwt'
6
7
 
7
8
  require 'restforce/version'
8
9
  require 'restforce/config'
@@ -19,6 +19,8 @@ module Restforce
19
19
  Restforce::Middleware::Authentication::Password
20
20
  elsif oauth_refresh?
21
21
  Restforce::Middleware::Authentication::Token
22
+ elsif jwt?
23
+ Restforce::Middleware::Authentication::JWTBearer
22
24
  end
23
25
  end
24
26
 
@@ -38,6 +40,14 @@ module Restforce
38
40
  options[:client_id] &&
39
41
  options[:client_secret]
40
42
  end
43
+
44
+ # Internal: Returns true if jwt bearer token flow should be used for
45
+ # authentication.
46
+ def jwt?
47
+ options[:jwt_key] &&
48
+ options[:username] &&
49
+ options[:client_id]
50
+ end
41
51
  end
42
52
  end
43
53
  end
@@ -28,6 +28,8 @@ module Restforce
28
28
  # password and oauth authentication
29
29
  # :client_secret - The oauth client secret to use.
30
30
  #
31
+ # :jwt_key - The private key for JWT authentication
32
+ #
31
33
  # :host - The String hostname to use during
32
34
  # authentication requests
33
35
  # (default: 'login.salesforce.com').
@@ -108,6 +108,9 @@ module Restforce
108
108
  # The OAuth client secret
109
109
  option :client_secret, default: lambda { ENV['SALESFORCE_CLIENT_SECRET'] }
110
110
 
111
+ # The private key for JWT authentication
112
+ option :jwt_key
113
+
111
114
  # Set this to true if you're authenticating with a Sandbox instance.
112
115
  # Defaults to false.
113
116
  option :host, default: lambda { ENV['SALESFORCE_HOST'] || 'login.salesforce.com' }
@@ -6,8 +6,9 @@ module Restforce
6
6
  # will attempt to either reauthenticate (username and password) or refresh
7
7
  # the oauth access token (if a refresh token is present).
8
8
  class Middleware::Authentication < Restforce::Middleware
9
- autoload :Password, 'restforce/middleware/authentication/password'
10
- autoload :Token, 'restforce/middleware/authentication/token'
9
+ autoload :Password, 'restforce/middleware/authentication/password'
10
+ autoload :Token, 'restforce/middleware/authentication/token'
11
+ autoload :JWTBearer, 'restforce/middleware/authentication/jwt_bearer'
11
12
 
12
13
  # Rescue from 401's, authenticate then raise the error again so the client
13
14
  # can reissue the request.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module Restforce
6
+ class Middleware
7
+ class Authentication
8
+ class JWTBearer < Restforce::Middleware::Authentication
9
+ def params
10
+ {
11
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
12
+ assertion: jwt_bearer_token
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def jwt_bearer_token
19
+ JWT.encode claim_set, private_key, 'RS256'
20
+ end
21
+
22
+ def claim_set
23
+ {
24
+ iss: @options[:client_id],
25
+ sub: @options[:username],
26
+ aud: @options[:host],
27
+ iat: Time.now.utc.to_i,
28
+ exp: Time.now.utc.to_i + 180
29
+ }
30
+ end
31
+
32
+ def private_key
33
+ OpenSSL::PKey::RSA.new(@options[:jwt_key])
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '4.0.0'
4
+ VERSION = '4.1.0'
5
5
  end
@@ -28,12 +28,13 @@ Gem::Specification.new do |gem|
28
28
  gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 1.0']
29
29
 
30
30
  gem.add_dependency 'json', '>= 1.7.5'
31
+ gem.add_dependency 'jwt', ['>= 1.5.6']
31
32
 
32
33
  gem.add_dependency 'hashie', ['>= 1.2.0', '< 4.0']
33
34
 
34
35
  gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
35
36
  gem.add_development_dependency 'rspec', '~> 2.14.0'
36
- gem.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
37
+ gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
37
38
  gem.add_development_dependency 'rubocop', '~> 0.75.0'
38
39
  gem.add_development_dependency 'simplecov', '~> 0.17.1'
39
40
  gem.add_development_dependency 'webmock', '~> 3.7.6'
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpAIBAAKCAQEAy3KYqxZIgVDgFwdA+OQcKMJQu3iUTlyCSk9b3RLBOudnvk8u
3
+ n0ShtKkOKB4b4RZeedcrlKESoak/6NS+M7CDemRT0EagqUiz/ZsZxB2KUp7au+d8
4
+ 0KWX99/loBjDttuon8ITDw2WFC9X0+TZqfsXcQ0iV1/9Sf8WHShd8ZqShjJBlEvf
5
+ 7u7VdNW8dXrl+4cvpPzspVxg6jVotEpmp875jmGRvshgx0iz0jtfAyxaaKStITC6
6
+ MxufVNDgIYQDl6queh8b9noDLtt17Eq6YnropYN1hOjaLtoLBP7AN2gsXG7N3vqC
7
+ JG619W9X4zCmKztv4oGjymInrS2msC2J02dNGQIDAQABAoIBAAurTARsJ8Z7DA9m
8
+ FBzygIb59kV6eg8wkSyP9rXscHbfdPzeb88k0Z2aILy+VV0IumyEofRJdNce7RJ+
9
+ uVYfprrrbD9C/c4X5HMEZWrxQtDQWb1zXp5dESVfiz0ujnM7kCVxrUQsxFHuETyP
10
+ IMj2JPcQCMs4L0ACSJNtkE3eTs8xko5kwDHZGiLTi5jD1bLgaHl1A+9CTU8LosTy
11
+ hEIrNSZfNidDPU4QSbwoElYZxpDMSbtyHaIk1WHz7zLzWoogK3x5AIQh64wWAQVd
12
+ zzlp2j2jSM7oQ9j+k1aNiUBdDoRX53jmaIwE/1WDW/LT33qAoqRw+5qHeLRoRcfu
13
+ 3uj/WI0CgYEA6wnpIUhqqWT+febhXtCr1mAJlAJpzUUQncN6Zk0Kj/kE1V52OqgL
14
+ gtOactII7J3+0zK7KGptqseTank0ghmGNdRBQ7+1JTQhpjLrCm/huKDhl+sBk95u
15
+ opxw/ZTwMFYPwsmZlFcy4uWRjtI+QzaV+2Xk5JF57H8vUiX/+XqseQcCgYEA3Zdw
16
+ zVHpcVPlyiXCbSvwb9IYXiJaQl/Rg96Klxah3MZNyRRKe5IoKUTJwEDuQ1MAHrve
17
+ cWrNLcXhX6r/PzIXSSLe71wgwpn7UcaqWzZJqqN7OIGEeTzYWbB6tGhse7Dw7tWB
18
+ hRkQSE0LPzZqboHz5msRM02sa61qiI5+ASJvIN8CgYEAvT+IoEzv3R89ruBVPQPm
19
+ KMHBVJSw3iArJex8xJxp0c0fMDJUHhyq0BdTd/pYRzVcNm/VtNAlJ2p07zlSpyKo
20
+ JvWV61gUIjWclnbPO+MkK4YWvzzxUz+5c2NlszjWQQU6wYuUBpZDmeBg2E++5F2y
21
+ W+8KY2QjeOJbltiUCCvXbccCgYEAqARYB5aARumyZqBS16xlVqQazeWGQqWcmzx2
22
+ ITGL8XZ7LGgyQZgE06XQw/F3t5yLjsIsXBr7ECXmST/C4gv9E/tYxm04edV/dfYI
23
+ 3bhACx6CI8owxCyabwcdQwWam/8B8FX7KwxiCDBCwt9ju/7VDHVKSXgvsEWBbaF9
24
+ cSbG1EkCgYBZFztTUnD/cLMcvLUegN0K+6Qa3x3nRSrlrJ+v51mU1X8G8qNyFO67
25
+ gUq9h4xbCl4Z5ZTuFKXwPM4XaMzfYdrWNS2zl5IG14FXS077GhDKe062b9mFoxtm
26
+ aViCit4Hm8xpLTS8x9KB7yYAiF9sR/GklW1SUCIqnpL9JShkhzjfZw==
27
+ -----END RSA PRIVATE KEY-----
@@ -52,6 +52,16 @@ describe Restforce::Concerns::Authentication do
52
52
 
53
53
  it { should eq Restforce::Middleware::Authentication::Token }
54
54
  end
55
+
56
+ context 'when jwt option is provided' do
57
+ before do
58
+ client.stub username_password?: false
59
+ client.stub oauth_refresh?: false
60
+ client.stub jwt?: true
61
+ end
62
+
63
+ it { should eq Restforce::Middleware::Authentication::JWTBearer }
64
+ end
55
65
  end
56
66
 
57
67
  describe '.username_password?' do
@@ -100,4 +110,29 @@ describe Restforce::Concerns::Authentication do
100
110
  it { should_not be_true }
101
111
  end
102
112
  end
113
+
114
+ describe '.jwt?' do
115
+ subject { client.jwt? }
116
+ let(:options) do
117
+ {}
118
+ end
119
+
120
+ before do
121
+ client.stub options: options
122
+ end
123
+
124
+ context 'when jwt options are provided' do
125
+ let(:options) do
126
+ { jwt_key: 'JWT_PRIVATE_KEY',
127
+ username: 'foo',
128
+ client_id: 'client' }
129
+ end
130
+
131
+ it { should be_true }
132
+ end
133
+
134
+ context 'when jwt options are not provided' do
135
+ it { should_not be_true }
136
+ end
137
+ end
103
138
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ describe Restforce::Middleware::Authentication::JWTBearer do
5
+ let(:jwt_key) { File.read('spec/fixtures/test_private.key') }
6
+
7
+ let(:options) do
8
+ { host: 'login.salesforce.com',
9
+ client_id: 'client_id',
10
+ username: 'foo',
11
+ jwt_key: jwt_key,
12
+ instance_url: 'https://na1.salesforce.com',
13
+ adapter: :net_http }
14
+ end
15
+
16
+ it_behaves_like 'authentication middleware' do
17
+ let(:success_request) do
18
+ stub_login_request(
19
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
20
+ "assertion=abc1234567890"
21
+ ).to_return(status: 200, body: fixture(:auth_success_response))
22
+ end
23
+
24
+ let(:fail_request) do
25
+ stub_login_request(
26
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
27
+ "assertion=abc1234567890"
28
+ ).to_return(status: 400, body: fixture(:refresh_error_response))
29
+ end
30
+ end
31
+
32
+ context 'allows jwt_key as string' do
33
+ let(:jwt_key) do
34
+ File.read('spec/fixtures/test_private.key')
35
+ end
36
+
37
+ let(:options) do
38
+ { host: 'login.salesforce.com',
39
+ client_id: 'client_id',
40
+ username: 'foo',
41
+ jwt_key: jwt_key,
42
+ instance_url: 'https://na1.salesforce.com',
43
+ adapter: :net_http }
44
+ end
45
+
46
+ it_behaves_like 'authentication middleware' do
47
+ let(:success_request) do
48
+ stub_login_request(
49
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
50
+ "assertion=abc1234567890"
51
+ ).to_return(status: 200, body: fixture(:auth_success_response))
52
+ end
53
+
54
+ let(:fail_request) do
55
+ stub_login_request(
56
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
57
+ "assertion=abc1234567890"
58
+ ).to_return(status: 400, body: fixture(:refresh_error_response))
59
+ end
60
+ end
61
+ end
62
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restforce
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric J. Holmes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-10-09 00:00:00.000000000 Z
12
+ date: 2019-10-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: 1.7.5
68
+ - !ruby/object:Gem::Dependency
69
+ name: jwt
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.5.6
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.5.6
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: hashie
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -119,14 +133,14 @@ dependencies:
119
133
  requirements:
120
134
  - - "~>"
121
135
  - !ruby/object:Gem::Version
122
- version: 0.3.0
136
+ version: 0.4.1
123
137
  type: :development
124
138
  prerelease: false
125
139
  version_requirements: !ruby/object:Gem::Requirement
126
140
  requirements:
127
141
  - - "~>"
128
142
  - !ruby/object:Gem::Version
129
- version: 0.3.0
143
+ version: 0.4.1
130
144
  - !ruby/object:Gem::Dependency
131
145
  name: rubocop
132
146
  requirement: !ruby/object:Gem::Requirement
@@ -210,6 +224,7 @@ files:
210
224
  - lib/restforce/mash.rb
211
225
  - lib/restforce/middleware.rb
212
226
  - lib/restforce/middleware/authentication.rb
227
+ - lib/restforce/middleware/authentication/jwt_bearer.rb
213
228
  - lib/restforce/middleware/authentication/password.rb
214
229
  - lib/restforce/middleware/authentication/token.rb
215
230
  - lib/restforce/middleware/authorization.rb
@@ -266,6 +281,7 @@ files:
266
281
  - spec/fixtures/sobject/upsert_multiple_error_response.json
267
282
  - spec/fixtures/sobject/upsert_updated_success_response.json
268
283
  - spec/fixtures/sobject/write_error_response.json
284
+ - spec/fixtures/test_private.key
269
285
  - spec/integration/abstract_client_spec.rb
270
286
  - spec/integration/data/client_spec.rb
271
287
  - spec/spec_helper.rb
@@ -291,6 +307,7 @@ files:
291
307
  - spec/unit/data/client_spec.rb
292
308
  - spec/unit/document_spec.rb
293
309
  - spec/unit/mash_spec.rb
310
+ - spec/unit/middleware/authentication/jwt_bearer_spec.rb
294
311
  - spec/unit/middleware/authentication/password_spec.rb
295
312
  - spec/unit/middleware/authentication/token_spec.rb
296
313
  - spec/unit/middleware/authentication_spec.rb
@@ -364,6 +381,7 @@ test_files:
364
381
  - spec/fixtures/sobject/upsert_multiple_error_response.json
365
382
  - spec/fixtures/sobject/upsert_updated_success_response.json
366
383
  - spec/fixtures/sobject/write_error_response.json
384
+ - spec/fixtures/test_private.key
367
385
  - spec/integration/abstract_client_spec.rb
368
386
  - spec/integration/data/client_spec.rb
369
387
  - spec/spec_helper.rb
@@ -389,6 +407,7 @@ test_files:
389
407
  - spec/unit/data/client_spec.rb
390
408
  - spec/unit/document_spec.rb
391
409
  - spec/unit/mash_spec.rb
410
+ - spec/unit/middleware/authentication/jwt_bearer_spec.rb
392
411
  - spec/unit/middleware/authentication/password_spec.rb
393
412
  - spec/unit/middleware/authentication/token_spec.rb
394
413
  - spec/unit/middleware/authentication_spec.rb