linkedin_sign_in 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfa7e967cbeab70c8230680b68b03b9a2dc4848619cd0d73c02b26d3bf291dd8
4
- data.tar.gz: 459f6693fe5a538af91fba21f6e66a0277ec87957d4ea5a93dba464619ce5388
3
+ metadata.gz: 1ee16026971b50c11efcd2f1882de53f9ba5c3855fd4641d6901939571d7c277
4
+ data.tar.gz: 23d35a4f027293b47294757ec195ae6676c5e7809f72c4c1f3d1a71619bcf41c
5
5
  SHA512:
6
- metadata.gz: 4a7ca8ae4af9d6bfc6fe2c808e5ff6fbbe74818ac610b86f60f5a8013a41e2e625a9183bee29af12ebcc2035e9689b6c05161d1224d1f0045b933069120e05b3
7
- data.tar.gz: ee7c235b7a2bf01c83fdb0b7effdda88d8f561aba79f618328ec7a3affd81f1aed87f23741185e77f477de1f3973771aeb7786a74b8bdf696807b5c49d6a1f16
6
+ metadata.gz: 6721873e8eff33dd2b6fead2e547f138b9a987a4d71f275e59b54462a17010891a09eff4e75da15caa695aa395baef92e529dc4b4ac4b32c186d030bb133f975
7
+ data.tar.gz: '0845e4f905d41382d50b0d80e38ced16e8dfd6af72f3ba2e7487edb8275da7a1c0bd1b8c5be28618914b4ae0f755e5ac1b9bda790803c99bfbdcc36ba7a34773'
data/.travis.yml CHANGED
@@ -2,14 +2,14 @@ language: ruby
2
2
  sudo: false
3
3
  cache: bundler
4
4
 
5
- # Bundler/RubyGems incompat on Ruby 2.5.0
6
- before_install: gem install bundler
5
+ # Bundler/RubyGems incompat on Ruby 2.5.0 and 2.6.1
6
+ before_install: gem update --system && gem install bundler -v 1.17.3
7
7
 
8
8
  rvm:
9
- - 2.2
10
9
  - 2.3
11
10
  - 2.4
12
11
  - 2.5
12
+ - 2.6
13
13
  - ruby-head
14
14
 
15
15
  matrix:
data/Gemfile.lock CHANGED
@@ -1,70 +1,70 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- linkedin_sign_in (0.3.1)
4
+ linkedin_sign_in (0.4.0)
5
5
  oauth2 (>= 1.4.0)
6
6
  rails (>= 5.2.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- actioncable (5.2.1.1)
12
- actionpack (= 5.2.1.1)
11
+ actioncable (5.2.2.1)
12
+ actionpack (= 5.2.2.1)
13
13
  nio4r (~> 2.0)
14
14
  websocket-driver (>= 0.6.1)
15
- actionmailer (5.2.1.1)
16
- actionpack (= 5.2.1.1)
17
- actionview (= 5.2.1.1)
18
- activejob (= 5.2.1.1)
15
+ actionmailer (5.2.2.1)
16
+ actionpack (= 5.2.2.1)
17
+ actionview (= 5.2.2.1)
18
+ activejob (= 5.2.2.1)
19
19
  mail (~> 2.5, >= 2.5.4)
20
20
  rails-dom-testing (~> 2.0)
21
- actionpack (5.2.1.1)
22
- actionview (= 5.2.1.1)
23
- activesupport (= 5.2.1.1)
21
+ actionpack (5.2.2.1)
22
+ actionview (= 5.2.2.1)
23
+ activesupport (= 5.2.2.1)
24
24
  rack (~> 2.0)
25
25
  rack-test (>= 0.6.3)
26
26
  rails-dom-testing (~> 2.0)
27
27
  rails-html-sanitizer (~> 1.0, >= 1.0.2)
28
- actionview (5.2.1.1)
29
- activesupport (= 5.2.1.1)
28
+ actionview (5.2.2.1)
29
+ activesupport (= 5.2.2.1)
30
30
  builder (~> 3.1)
31
31
  erubi (~> 1.4)
32
32
  rails-dom-testing (~> 2.0)
33
33
  rails-html-sanitizer (~> 1.0, >= 1.0.3)
34
- activejob (5.2.1.1)
35
- activesupport (= 5.2.1.1)
34
+ activejob (5.2.2.1)
35
+ activesupport (= 5.2.2.1)
36
36
  globalid (>= 0.3.6)
37
- activemodel (5.2.1.1)
38
- activesupport (= 5.2.1.1)
39
- activerecord (5.2.1.1)
40
- activemodel (= 5.2.1.1)
41
- activesupport (= 5.2.1.1)
37
+ activemodel (5.2.2.1)
38
+ activesupport (= 5.2.2.1)
39
+ activerecord (5.2.2.1)
40
+ activemodel (= 5.2.2.1)
41
+ activesupport (= 5.2.2.1)
42
42
  arel (>= 9.0)
43
- activestorage (5.2.1.1)
44
- actionpack (= 5.2.1.1)
45
- activerecord (= 5.2.1.1)
43
+ activestorage (5.2.2.1)
44
+ actionpack (= 5.2.2.1)
45
+ activerecord (= 5.2.2.1)
46
46
  marcel (~> 0.3.1)
47
- activesupport (5.2.1.1)
47
+ activesupport (5.2.2.1)
48
48
  concurrent-ruby (~> 1.0, >= 1.0.2)
49
49
  i18n (>= 0.7, < 2)
50
50
  minitest (~> 5.1)
51
51
  tzinfo (~> 1.1)
52
- addressable (2.5.2)
52
+ addressable (2.6.0)
53
53
  public_suffix (>= 2.0.2, < 4.0)
54
54
  arel (9.0.0)
55
55
  builder (3.2.3)
56
- byebug (10.0.2)
57
- concurrent-ruby (1.1.3)
56
+ byebug (11.0.1)
57
+ concurrent-ruby (1.1.5)
58
58
  crack (0.4.3)
59
59
  safe_yaml (~> 1.0.0)
60
60
  crass (1.0.4)
61
- erubi (1.7.1)
61
+ erubi (1.8.0)
62
62
  faraday (0.15.4)
63
63
  multipart-post (>= 1.2, < 3)
64
- globalid (0.4.1)
64
+ globalid (0.4.2)
65
65
  activesupport (>= 4.2.0)
66
- hashdiff (0.3.7)
67
- i18n (1.1.1)
66
+ hashdiff (0.3.8)
67
+ i18n (1.6.0)
68
68
  concurrent-ruby (~> 1.0)
69
69
  jwt (2.1.0)
70
70
  loofah (2.2.3)
@@ -75,16 +75,16 @@ GEM
75
75
  marcel (0.3.3)
76
76
  mimemagic (~> 0.3.2)
77
77
  method_source (0.9.2)
78
- mimemagic (0.3.2)
78
+ mimemagic (0.3.3)
79
79
  mini_mime (1.0.1)
80
- mini_portile2 (2.3.0)
80
+ mini_portile2 (2.4.0)
81
81
  minitest (5.11.3)
82
82
  multi_json (1.13.1)
83
83
  multi_xml (0.6.0)
84
84
  multipart-post (2.0.0)
85
85
  nio4r (2.3.1)
86
- nokogiri (1.8.5)
87
- mini_portile2 (~> 2.3.0)
86
+ nokogiri (1.10.1)
87
+ mini_portile2 (~> 2.4.0)
88
88
  oauth2 (1.4.1)
89
89
  faraday (>= 0.8, < 0.16.0)
90
90
  jwt (>= 1.0, < 3.0)
@@ -95,32 +95,32 @@ GEM
95
95
  rack (2.0.6)
96
96
  rack-test (1.1.0)
97
97
  rack (>= 1.0, < 3)
98
- rails (5.2.1.1)
99
- actioncable (= 5.2.1.1)
100
- actionmailer (= 5.2.1.1)
101
- actionpack (= 5.2.1.1)
102
- actionview (= 5.2.1.1)
103
- activejob (= 5.2.1.1)
104
- activemodel (= 5.2.1.1)
105
- activerecord (= 5.2.1.1)
106
- activestorage (= 5.2.1.1)
107
- activesupport (= 5.2.1.1)
98
+ rails (5.2.2.1)
99
+ actioncable (= 5.2.2.1)
100
+ actionmailer (= 5.2.2.1)
101
+ actionpack (= 5.2.2.1)
102
+ actionview (= 5.2.2.1)
103
+ activejob (= 5.2.2.1)
104
+ activemodel (= 5.2.2.1)
105
+ activerecord (= 5.2.2.1)
106
+ activestorage (= 5.2.2.1)
107
+ activesupport (= 5.2.2.1)
108
108
  bundler (>= 1.3.0)
109
- railties (= 5.2.1.1)
109
+ railties (= 5.2.2.1)
110
110
  sprockets-rails (>= 2.0.0)
111
111
  rails-dom-testing (2.0.3)
112
112
  activesupport (>= 4.2.0)
113
113
  nokogiri (>= 1.6)
114
114
  rails-html-sanitizer (1.0.4)
115
115
  loofah (~> 2.2, >= 2.2.2)
116
- railties (5.2.1.1)
117
- actionpack (= 5.2.1.1)
118
- activesupport (= 5.2.1.1)
116
+ railties (5.2.2.1)
117
+ actionpack (= 5.2.2.1)
118
+ activesupport (= 5.2.2.1)
119
119
  method_source
120
120
  rake (>= 0.8.7)
121
121
  thor (>= 0.19.0, < 2.0)
122
- rake (12.3.1)
123
- safe_yaml (1.0.4)
122
+ rake (12.3.2)
123
+ safe_yaml (1.0.5)
124
124
  sprockets (3.7.2)
125
125
  concurrent-ruby (~> 1.0)
126
126
  rack (> 1, < 3)
@@ -132,7 +132,7 @@ GEM
132
132
  thread_safe (0.3.6)
133
133
  tzinfo (1.2.5)
134
134
  thread_safe (~> 0.1)
135
- webmock (3.4.2)
135
+ webmock (3.5.1)
136
136
  addressable (>= 2.3.6)
137
137
  crack (>= 0.3.2)
138
138
  hashdiff
@@ -144,7 +144,7 @@ PLATFORMS
144
144
  ruby
145
145
 
146
146
  DEPENDENCIES
147
- bundler (~> 1.15)
147
+ bundler (~> 1.17.2)
148
148
  byebug
149
149
  jwt (>= 1.5.6)
150
150
  linkedin_sign_in!
@@ -152,4 +152,4 @@ DEPENDENCIES
152
152
  webmock (>= 3.4.2)
153
153
 
154
154
  BUNDLED WITH
155
- 1.16.4
155
+ 1.17.2
data/README.md CHANGED
@@ -78,7 +78,8 @@ This gem provides a `linkedin_sign_in_button` helper. It generates a button whic
78
78
  ```
79
79
 
80
80
  The `proceed_to` argument is required. After authenticating with Linkedin, the gem redirects to `proceed_to`, providing
81
- a Linkedin ID token in `flash[:linkedin_sign_in_token]`. Your application decides what to do with it:
81
+ a LinkedIn ID token in `flash[:linkedin_sign_in][:token]` or an [OAuth authorizaton code grant error](https://tools.ietf.org/html/rfc6749#section-4.1.2.1)
82
+ in `flash[:linkedin_sign_in][:error]`. Your application decides what to do with it:
82
83
 
83
84
  ```ruby
84
85
  # config/routes.rb
@@ -106,9 +107,12 @@ class LoginsController < ApplicationController
106
107
 
107
108
  private
108
109
  def authenticate_with_linkedin
109
- if flash[:linkedin_sign_in_token].present?
110
- User.find_by linkedin_id: LinkedinSignIn::Identity.new(flash[:linkedin_sign_in_token]).user_id
111
- end
110
+ if id_token = flash[:linkedin_sign_in][:token]
111
+ User.find_by linkedin_id: LinkedIn::Identity.new(id_token).user_id
112
+ elsif error = flash[:linkedin_sign_in][:error]
113
+ logger.error "LinkedIn authentication error: #{error}"
114
+ nil
115
+ end
112
116
  end
113
117
  end
114
118
  ```
@@ -126,20 +130,18 @@ origin as your application. This means it must have the same protocol, host, and
126
130
  The `LinkedinSignIn::Identity` class decodes and verifies the integrity of a Linkedin ID token. It exposes the profile
127
131
  information contained in the token via the following instance methods:
128
132
 
129
- * `name`
133
+ * `first_name`
134
+
135
+ * `last_name`
130
136
 
131
137
  * `email_address`
132
138
 
133
139
  * `user_id`: A string that uniquely identifies a single Linkedin user. Use this, not `email_address`, to associate a
134
140
  Linkedin user with an application user. A Linkedin user’s email address may change, but their `user_id` will remain constant.
135
141
 
136
- * `email_verified?`
137
-
138
142
  * `avatar_url`
139
143
 
140
- * `locale`
141
-
142
- * `hosted_domain`: The user’s hosted G Suite domain, provided only if they belong to a G Suite.
144
+ * `current_company_name`: name of the current company the user is working at
143
145
 
144
146
 
145
147
  ## Security
@@ -2,26 +2,36 @@ require_dependency 'linkedin_sign_in/redirect_protector'
2
2
 
3
3
  class LinkedinSignIn::CallbacksController < LinkedinSignIn::BaseController
4
4
  def show
5
- if valid_request?
6
- redirect_to proceed_to_url, flash: { linkedin_sign_in_token: token }
7
- else
8
- head :unprocessable_entity
9
- end
5
+ redirect_to proceed_to_url, flash: { linkedin_sign_in: linkedin_sign_in_response }
10
6
  rescue LinkedinSignIn::RedirectProtector::Violation => error
11
7
  logger.error error.message
12
8
  head :bad_request
13
9
  end
14
10
 
15
11
  private
16
- def valid_request?
17
- flash[:state].present? && params.require(:state) == flash[:state] && params[:error].blank?
18
- end
19
-
20
12
  def proceed_to_url
21
13
  flash[:proceed_to].tap { |url| LinkedinSignIn::RedirectProtector.ensure_same_origin(url, request.url) }
22
14
  end
23
15
 
16
+ def linkedin_sign_in_response
17
+ if valid_request? && params[:code].present?
18
+ { token: token }
19
+ else
20
+ { error: error_message_for(params[:error]) }
21
+ end
22
+ rescue OAuth2::Error => error
23
+ { error: error_message_for(error.code) }
24
+ end
25
+
26
+ def valid_request?
27
+ flash[:state].present? && params[:state] == flash[:state]
28
+ end
29
+
24
30
  def token
25
- client.auth_code.get_token(params.require(:code)).token
31
+ client.auth_code.get_token(params[:code]).token
32
+ end
33
+
34
+ def error_message_for(error_code)
35
+ error_code.presence_in(LinkedinSignIn::OAUTH2_ERRORS) || "invalid_request"
26
36
  end
27
37
  end
data/bin/rails CHANGED
@@ -3,7 +3,7 @@
3
3
  # installed from the root of your application.
4
4
 
5
5
  ENGINE_ROOT = File.expand_path('..', __dir__)
6
- ENGINE_PATH = File.expand_path('../lib/blorgh/engine', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/linkedin_sign_in/engine', __dir__)
7
7
  APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
8
 
9
9
  # Set up gems listed in the Gemfile.
@@ -4,6 +4,31 @@ require 'active_support/rails'
4
4
  module LinkedinSignIn
5
5
  mattr_accessor :client_id
6
6
  mattr_accessor :client_secret
7
+
8
+ # https://tools.ietf.org/html/rfc6749#section-4.1.2.1
9
+ authorization_request_errors = %w[
10
+ invalid_request
11
+ unauthorized_client
12
+ access_denied
13
+ unsupported_response_type
14
+ invalid_scope
15
+ server_error
16
+ temporarily_unavailable
17
+ ]
18
+
19
+ # https://tools.ietf.org/html/rfc6749#section-5.2
20
+ access_token_request_errors = %w[
21
+ invalid_request
22
+ invalid_client
23
+ invalid_grant
24
+ unauthorized_client
25
+ unsupported_grant_type
26
+ invalid_scope
27
+ ]
28
+
29
+ # Authorization Code Grant errors from both authorization requests
30
+ # and access token requests.
31
+ OAUTH2_ERRORS = authorization_request_errors | access_token_request_errors
7
32
  end
8
33
 
9
34
  require 'linkedin_sign_in/identity'
@@ -1,4 +1,3 @@
1
- require 'linkedin-id-token'
2
1
  require 'active_support/core_ext/module/delegation'
3
2
 
4
3
  module LinkedinSignIn
@@ -30,9 +29,9 @@ module LinkedinSignIn
30
29
  end
31
30
 
32
31
  def current_company_name
33
- positions = @payload["positions"]["values"]
32
+ positions = @payload.dig("positions", "values")
34
33
  current_position = positions.find { |position| position["isCurrent"] }
35
- current_position["company"]["name"]
34
+ current_position.dig("company", "name")
36
35
  end
37
36
 
38
37
  private
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'linkedin_sign_in'
3
- s.version = '0.3.1'
3
+ s.version = '0.4.0'
4
4
  s.authors = ['Vincent Robert']
5
5
  s.email = ['vincent.robert@genezys.net']
6
6
  s.summary = 'Sign in (or up) with Linkedin for Rails applications'
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
12
12
  s.add_dependency 'rails', '>= 5.2.0'
13
13
  s.add_dependency 'oauth2', '>= 1.4.0'
14
14
 
15
- s.add_development_dependency 'bundler', '~> 1.15'
15
+ s.add_development_dependency 'bundler', '~> 1.17.2'
16
16
  s.add_development_dependency 'jwt', '>= 1.5.6'
17
17
  s.add_development_dependency 'webmock', '>= 3.4.2'
18
18
 
@@ -5,16 +5,92 @@ class LinkedinSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
5
5
  post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
6
6
  assert_response :redirect
7
7
 
8
- stub_token_request code: '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
8
+ stub_token_for '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
9
9
 
10
10
  get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
11
11
  assert_redirected_to 'http://www.example.com/login'
12
- assert_equal 'ya29.GlwIBo', flash[:linkedin_sign_in_token]
12
+ assert_equal 'ya29.GlwIBo', flash[:linkedin_sign_in][:token]
13
+ assert_nil flash[:linkedin_sign_in][:error]
13
14
  end
14
15
 
15
- test "protecting against CSRF" do
16
+ # Authorization request errors: https://tools.ietf.org/html/rfc6749#section-4.1.2.1
17
+ %w[ invalid_request unauthorized_client access_denied unsupported_response_type invalid_scope server_error temporarily_unavailable ].each do |error|
18
+ test "receiving an authorization code grant error: #{error}" do
19
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
20
+ assert_response :redirect
21
+
22
+ get linkedin_sign_in.callback_url(error: error, state: flash[:state])
23
+ assert_redirected_to 'http://www.example.com/login'
24
+ assert_nil flash[:linkedin_sign_in][:token]
25
+ assert_equal error, flash[:linkedin_sign_in][:error]
26
+ end
27
+ end
28
+
29
+ test "receiving an invalid authorization error" do
30
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
31
+ assert_response :redirect
32
+
33
+ get linkedin_sign_in.callback_url(error: 'unknown error code', state: flash[:state])
34
+ assert_redirected_to 'http://www.example.com/login'
35
+ assert_nil flash[:linkedin_sign_in][:token]
36
+ assert_equal "invalid_request", flash[:linkedin_sign_in][:error]
37
+ end
38
+
39
+ test "receiving neither code nor error" do
40
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
41
+ assert_response :redirect
42
+
43
+ get linkedin_sign_in.callback_url(state: flash[:state])
44
+ assert_redirected_to 'http://www.example.com/login'
45
+ assert_nil flash[:linkedin_sign_in][:token]
46
+ assert_equal 'invalid_request', flash[:linkedin_sign_in][:error]
47
+ end
48
+
49
+ # Access token request errors: https://tools.ietf.org/html/rfc6749#section-5.2
50
+ %w[ invalid_request invalid_client invalid_grant unauthorized_client unsupported_grant_type invalid_scope ].each do |error|
51
+ test "receiving an access token request error: #{error}" do
52
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
53
+ assert_response :redirect
54
+
55
+ stub_token_error_for '4/SgCpHSVW5-Cy', error: error
56
+
57
+ get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
58
+ assert_redirected_to 'http://www.example.com/login'
59
+ assert_nil flash[:linkedin_sign_in][:id_token]
60
+ assert_equal error, flash[:linkedin_sign_in][:error]
61
+ end
62
+ end
63
+
64
+ test "protecting against CSRF without flash state" do
65
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
66
+ assert_response :redirect
67
+
16
68
  get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
17
- assert_response :unprocessable_entity
69
+ assert_redirected_to 'http://www.example.com/login'
70
+ assert_nil flash[:linkedin_sign_in][:token]
71
+ assert_equal 'invalid_request', flash[:linkedin_sign_in][:error]
72
+ end
73
+
74
+ test "protecting against CSRF with invalid state" do
75
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
76
+ assert_response :redirect
77
+ assert_not_nil flash[:state]
78
+
79
+ get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
80
+ assert_redirected_to 'http://www.example.com/login'
81
+ assert_nil flash[:linkedin_sign_in][:token]
82
+ assert_equal 'invalid_request', flash[:linkedin_sign_in][:error]
83
+ end
84
+
85
+ test "protecting against CSRF with missing state" do
86
+ post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
87
+ assert_response :redirect
88
+ assert_not_nil flash[:state]
89
+
90
+ get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy')
91
+ assert_redirected_to 'http://www.example.com/login'
92
+ assert_nil flash[:linkedin_sign_in][:token]
93
+ assert_equal 'invalid_request', flash[:linkedin_sign_in][:error]
18
94
  end
19
95
 
20
96
  test "protecting against open redirects" do
@@ -26,11 +102,27 @@ class LinkedinSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
26
102
  end
27
103
 
28
104
  private
29
- def stub_token_request(code:, **params)
30
- stub_request(:post, 'https://www.linkedin.com/oauth/v2/accessToken').
31
- with(body: { grant_type: 'authorization_code', code: code,
32
- client_id: FAKE_LINKEDIN_CLIENT_ID, client_secret: FAKE_LINKEDIN_CLIENT_SECRET,
33
- redirect_uri: 'http://www.example.com/linkedin_sign_in/callback' }).
34
- to_return(status: 200, headers: { 'Content-Type' => 'application/json' }, body: JSON.generate(params))
105
+ def stub_token_for(code, **response_body)
106
+ stub_token_request(code, status: 200, response: response_body)
107
+ end
108
+
109
+ def stub_token_error_for(code, error:)
110
+ stub_token_request(code, status: 418, response: { error: error })
111
+ end
112
+
113
+ def stub_token_request(code, status:, response:)
114
+ stub_request(:post, 'https://www.linkedin.com/oauth/v2/accessToken').with(
115
+ body: {
116
+ grant_type: 'authorization_code',
117
+ code: code,
118
+ client_id: FAKE_LINKEDIN_CLIENT_ID,
119
+ client_secret: FAKE_LINKEDIN_CLIENT_SECRET,
120
+ redirect_uri: 'http://www.example.com/linkedin_sign_in/callback'
121
+ }
122
+ ).to_return(
123
+ status: status,
124
+ headers: { 'Content-Type' => 'application/json' },
125
+ body: JSON.generate(response)
126
+ )
35
127
  end
36
128
  end
File without changes
File without changes
File without changes
File without changes
data/test/test_helper.rb CHANGED
@@ -17,9 +17,6 @@ if LINKEDIN_X509_CERTIFICATE.not_after <= Time.now
17
17
  raise "Test certificate is expired. Generate a new one and run the tests again: `bundle exec rake test:certificate:generate`."
18
18
  end
19
19
 
20
- require 'linkedin-id-token'
21
- # LinkedinSignIn::Identity.validator = Validator.new(x509_cert: LINKEDIN_X509_CERTIFICATE)
22
-
23
20
  class ActionView::TestCase
24
21
  private
25
22
  def assert_dom_equal(expected, actual, message = nil)
data/tmp/.keep ADDED
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linkedin_sign_in
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vincent Robert
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-28 00:00:00.000000000 Z
11
+ date: 2019-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '1.15'
47
+ version: 1.17.2
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '1.15'
54
+ version: 1.17.2
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: jwt
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -102,7 +102,6 @@ files:
102
102
  - app/helpers/linkedin_sign_in/button_helper.rb
103
103
  - bin/rails
104
104
  - config/routes.rb
105
- - lib/linkedin-id-token.rb
106
105
  - lib/linkedin_sign_in.rb
107
106
  - lib/linkedin_sign_in/engine.rb
108
107
  - lib/linkedin_sign_in/identity.rb
@@ -160,6 +159,7 @@ files:
160
159
  - test/dummy/config/routes.rb
161
160
  - test/dummy/config/spring.rb
162
161
  - test/dummy/config/storage.yml
162
+ - test/dummy/db/test.sqlite3
163
163
  - test/dummy/lib/assets/.keep
164
164
  - test/dummy/log/.keep
165
165
  - test/dummy/package.json
@@ -169,11 +169,15 @@ files:
169
169
  - test/dummy/public/apple-touch-icon-precomposed.png
170
170
  - test/dummy/public/apple-touch-icon.png
171
171
  - test/dummy/public/favicon.ico
172
+ - test/dummy/storage/.keep
173
+ - test/dummy/tmp/.keep
174
+ - test/dummy/tmp/storage/.keep
172
175
  - test/helpers/button_helper_test.rb
173
176
  - test/key.pem
174
177
  - test/models/identity_test.rb
175
178
  - test/models/redirect_protector_test.rb
176
179
  - test/test_helper.rb
180
+ - tmp/.keep
177
181
  homepage: https://github.com/genezys/linkedin_sign_in
178
182
  licenses:
179
183
  - MIT
@@ -251,6 +255,7 @@ test_files:
251
255
  - test/dummy/config/routes.rb
252
256
  - test/dummy/config/spring.rb
253
257
  - test/dummy/config/storage.yml
258
+ - test/dummy/db/test.sqlite3
254
259
  - test/dummy/lib/assets/.keep
255
260
  - test/dummy/log/.keep
256
261
  - test/dummy/package.json
@@ -260,6 +265,9 @@ test_files:
260
265
  - test/dummy/public/apple-touch-icon-precomposed.png
261
266
  - test/dummy/public/apple-touch-icon.png
262
267
  - test/dummy/public/favicon.ico
268
+ - test/dummy/storage/.keep
269
+ - test/dummy/tmp/.keep
270
+ - test/dummy/tmp/storage/.keep
263
271
  - test/helpers/button_helper_test.rb
264
272
  - test/key.pem
265
273
  - test/models/identity_test.rb
@@ -1,190 +0,0 @@
1
- # encoding: utf-8
2
- # Copyright 2012 Google Inc.
3
- #
4
- # Licensed under the Apache License, Version 2.0 (the "License");
5
- # you may not use this file except in compliance with the License.
6
- # You may obtain a copy of the License at
7
- #
8
- # http://www.apache.org/licenses/LICENSE-2.0
9
- #
10
- # Unless required by applicable law or agreed to in writing, software
11
- # distributed under the License is distributed on an "AS IS" BASIS,
12
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- # See the License for the specific language governing permissions and
14
- # limitations under the License.
15
-
16
- ##
17
- # Validates strings alleged to be ID Tokens issued by Google; if validation
18
- # succeeds, returns the decoded ID Token as a hash.
19
- # It's a good idea to keep an instance of this class around for a long time,
20
- # because it caches the keys, performs validation statically, and only
21
- # refreshes from Google when required (once per day by default)
22
- #
23
- # @author Tim Bray, adapted from code by Bob Aman
24
-
25
- require 'json'
26
- require 'jwt'
27
- require 'monitor'
28
- require 'net/http'
29
- require 'openssl'
30
-
31
- module LinkedinIDToken
32
- class CertificateError < StandardError; end
33
- class ValidationError < StandardError; end
34
- class ExpiredTokenError < ValidationError; end
35
- class SignatureError < ValidationError; end
36
- class InvalidIssuerError < ValidationError; end
37
- class AudienceMismatchError < ValidationError; end
38
- class ClientIDMismatchError < ValidationError; end
39
-
40
- class Validator
41
- include MonitorMixin
42
-
43
- LINKEDIN_CERTS_URI = 'https://www.googleapis.com/oauth2/v1/certs'
44
- LINKEDIN_CERTS_EXPIRY = 3600 # 1 hour
45
-
46
- # https://developers.google.com/identity/sign-in/web/backend-auth
47
- LINKEDIN_ISSUERS = ['accounts.google.com', 'https://accounts.google.com']
48
-
49
- def initialize(options = {})
50
- super()
51
-
52
- if options[:x509_cert]
53
- @certs_mode = :literal
54
- @certs = { :_ => options[:x509_cert] }
55
- # elsif options[:jwk_uri] # TODO
56
- # @certs_mode = :jwk
57
- # @certs = {}
58
- else
59
- @certs_mode = :old_skool
60
- @certs = {}
61
- end
62
-
63
- @certs_expiry = options.fetch(:expiry, LINKEDIN_CERTS_EXPIRY)
64
- end
65
-
66
- ##
67
- # If it validates, returns a hash with the JWT payload from the ID Token.
68
- # You have to provide an "aud" value, which must match the
69
- # token's field with that name, and will similarly check cid if provided.
70
- #
71
- # If something fails, raises an error
72
- #
73
- # @param [String] token
74
- # The string form of the token
75
- # @param [String] aud
76
- # The required audience value
77
- # @param [String] cid
78
- # The optional client-id ("azp" field) value
79
- #
80
- # @return [Hash] The decoded ID token
81
- def check(token, aud, cid = nil)
82
- synchronize do
83
- payload = check_cached_certs(token, aud, cid)
84
-
85
- unless payload
86
- # no certs worked, might've expired, refresh
87
- if refresh_certs
88
- payload = check_cached_certs(token, aud, cid)
89
-
90
- unless payload
91
- raise SignatureError, 'Token not verified as issued by Google'
92
- end
93
- else
94
- raise CertificateError, 'Unable to retrieve Google public keys'
95
- end
96
- end
97
-
98
- payload
99
- end
100
- end
101
-
102
- private
103
-
104
- # tries to validate the token against each cached cert.
105
- # Returns the token payload or raises a ValidationError or
106
- # nil, which means none of the certs validated.
107
- def check_cached_certs(token, aud, cid)
108
- payload = nil
109
-
110
- # find first public key that validates this token
111
- @certs.detect do |key, cert|
112
- begin
113
- public_key = cert.public_key
114
- decoded_token = JWT.decode(token, public_key, !!public_key, { :algorithm => 'RS256' })
115
- payload = decoded_token.first
116
-
117
- # in Feb 2013, the 'cid' claim became the 'azp' claim per changes
118
- # in the OIDC draft. At some future point we can go all-azp, but
119
- # this should keep everything running for a while
120
- if payload['azp']
121
- payload['cid'] = payload['azp']
122
- elsif payload['cid']
123
- payload['azp'] = payload['cid']
124
- end
125
- payload
126
- rescue JWT::ExpiredSignature
127
- raise ExpiredTokenError, 'Token signature is expired'
128
- rescue JWT::DecodeError
129
- nil # go on, try the next cert
130
- end
131
- end
132
-
133
- if payload
134
- if !(payload.has_key?('aud') && payload['aud'] == aud)
135
- raise AudienceMismatchError, 'Token audience mismatch'
136
- end
137
- if cid && payload['cid'] != cid
138
- raise ClientIDMismatchError, 'Token client-id mismatch'
139
- end
140
- if !LINKEDIN_ISSUERS.include?(payload['iss'])
141
- raise InvalidIssuerError, 'Token issuer mismatch'
142
- end
143
- payload
144
- else
145
- nil
146
- end
147
- end
148
-
149
- # returns false if there was a problem
150
- def refresh_certs
151
- case @certs_mode
152
- when :literal
153
- true # no-op
154
- when :old_skool
155
- old_skool_refresh_certs
156
- # when :jwk # TODO
157
- # jwk_refresh_certs
158
- end
159
- end
160
-
161
- def old_skool_refresh_certs
162
- return true unless certs_cache_expired?
163
-
164
- uri = URI(LINKEDIN_CERTS_URI)
165
- get = Net::HTTP::Get.new uri.request_uri
166
- http = Net::HTTP.new(uri.host, uri.port)
167
- http.use_ssl = true
168
- res = http.request(get)
169
-
170
- if res.is_a?(Net::HTTPSuccess)
171
- new_certs = Hash[JSON.load(res.body).map do |key, cert|
172
- [key, OpenSSL::X509::Certificate.new(cert)]
173
- end]
174
- @certs.merge! new_certs
175
- @certs_last_refresh = Time.now
176
- true
177
- else
178
- false
179
- end
180
- end
181
-
182
- def certs_cache_expired?
183
- if defined? @certs_last_refresh
184
- Time.now > @certs_last_refresh + @certs_expiry
185
- else
186
- true
187
- end
188
- end
189
- end
190
- end