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 +4 -4
- data/.travis.yml +3 -3
- data/Gemfile.lock +53 -53
- data/README.md +12 -10
- data/app/controllers/linkedin_sign_in/callbacks_controller.rb +20 -10
- data/bin/rails +1 -1
- data/lib/linkedin_sign_in.rb +25 -0
- data/lib/linkedin_sign_in/identity.rb +2 -3
- data/linkedin_sign_in.gemspec +2 -2
- data/test/controllers/callbacks_controller_test.rb +102 -10
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/storage/.keep +0 -0
- data/test/dummy/tmp/.keep +0 -0
- data/test/dummy/tmp/storage/.keep +0 -0
- data/test/test_helper.rb +0 -3
- data/tmp/.keep +0 -0
- metadata +13 -5
- data/lib/linkedin-id-token.rb +0 -190
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1ee16026971b50c11efcd2f1882de53f9ba5c3855fd4641d6901939571d7c277
|
4
|
+
data.tar.gz: 23d35a4f027293b47294757ec195ae6676c5e7809f72c4c1f3d1a71619bcf41c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
12
|
-
actionpack (= 5.2.
|
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.
|
16
|
-
actionpack (= 5.2.
|
17
|
-
actionview (= 5.2.
|
18
|
-
activejob (= 5.2.
|
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.
|
22
|
-
actionview (= 5.2.
|
23
|
-
activesupport (= 5.2.
|
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.
|
29
|
-
activesupport (= 5.2.
|
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.
|
35
|
-
activesupport (= 5.2.
|
34
|
+
activejob (5.2.2.1)
|
35
|
+
activesupport (= 5.2.2.1)
|
36
36
|
globalid (>= 0.3.6)
|
37
|
-
activemodel (5.2.
|
38
|
-
activesupport (= 5.2.
|
39
|
-
activerecord (5.2.
|
40
|
-
activemodel (= 5.2.
|
41
|
-
activesupport (= 5.2.
|
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.
|
44
|
-
actionpack (= 5.2.
|
45
|
-
activerecord (= 5.2.
|
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.
|
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.
|
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 (
|
57
|
-
concurrent-ruby (1.1.
|
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.
|
61
|
+
erubi (1.8.0)
|
62
62
|
faraday (0.15.4)
|
63
63
|
multipart-post (>= 1.2, < 3)
|
64
|
-
globalid (0.4.
|
64
|
+
globalid (0.4.2)
|
65
65
|
activesupport (>= 4.2.0)
|
66
|
-
hashdiff (0.3.
|
67
|
-
i18n (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.
|
78
|
+
mimemagic (0.3.3)
|
79
79
|
mini_mime (1.0.1)
|
80
|
-
mini_portile2 (2.
|
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.
|
87
|
-
mini_portile2 (~> 2.
|
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.
|
99
|
-
actioncable (= 5.2.
|
100
|
-
actionmailer (= 5.2.
|
101
|
-
actionpack (= 5.2.
|
102
|
-
actionview (= 5.2.
|
103
|
-
activejob (= 5.2.
|
104
|
-
activemodel (= 5.2.
|
105
|
-
activerecord (= 5.2.
|
106
|
-
activestorage (= 5.2.
|
107
|
-
activesupport (= 5.2.
|
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.
|
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.
|
117
|
-
actionpack (= 5.2.
|
118
|
-
activesupport (= 5.2.
|
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.
|
123
|
-
safe_yaml (1.0.
|
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.
|
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.
|
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.
|
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
|
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[:
|
110
|
-
User.find_by linkedin_id:
|
111
|
-
|
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
|
-
* `
|
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
|
-
* `
|
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
|
-
|
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
|
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/
|
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.
|
data/lib/linkedin_sign_in.rb
CHANGED
@@ -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
|
32
|
+
positions = @payload.dig("positions", "values")
|
34
33
|
current_position = positions.find { |position| position["isCurrent"] }
|
35
|
-
current_position
|
34
|
+
current_position.dig("company", "name")
|
36
35
|
end
|
37
36
|
|
38
37
|
private
|
data/linkedin_sign_in.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'linkedin_sign_in'
|
3
|
-
s.version = '0.
|
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
|
+
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
|
-
|
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[:
|
12
|
+
assert_equal 'ya29.GlwIBo', flash[:linkedin_sign_in][:token]
|
13
|
+
assert_nil flash[:linkedin_sign_in][:error]
|
13
14
|
end
|
14
15
|
|
15
|
-
|
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
|
-
|
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
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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.
|
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:
|
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:
|
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:
|
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
|
data/lib/linkedin-id-token.rb
DELETED
@@ -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
|