linkedin_sign_in 0.3.1 → 0.4.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: 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