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 +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
|