linkedin_sign_in 0.3
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 +7 -0
- data/.gitignore +13 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +155 -0
- data/MIT-LICENSE +20 -0
- data/README.md +154 -0
- data/Rakefile +40 -0
- data/SECURITY.md +3 -0
- data/app/controllers/linkedin_sign_in/authorizations_controller.rb +17 -0
- data/app/controllers/linkedin_sign_in/base_controller.rb +15 -0
- data/app/controllers/linkedin_sign_in/callbacks_controller.rb +27 -0
- data/app/helpers/linkedin_sign_in/button_helper.rb +7 -0
- data/bin/rails +16 -0
- data/config/routes.rb +4 -0
- data/lib/linkedin-id-token.rb +190 -0
- data/lib/linkedin_sign_in/engine.rb +32 -0
- data/lib/linkedin_sign_in/identity.rb +55 -0
- data/lib/linkedin_sign_in/redirect_protector.rb +25 -0
- data/lib/linkedin_sign_in.rb +10 -0
- data/linkedin_sign_in.gemspec +21 -0
- data/test/certificate.pem +19 -0
- data/test/controllers/authorizations_controller_test.rb +26 -0
- data/test/controllers/callbacks_controller_test.rb +36 -0
- data/test/dummy/.ruby-version +1 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/javascripts/cable.js +13 -0
- data/test/dummy/app/assets/javascripts/channels/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/channels/application_cable/channel.rb +4 -0
- data/test/dummy/app/channels/application_cable/connection.rb +4 -0
- data/test/dummy/app/controllers/application_controller.rb +2 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/jobs/application_job.rb +2 -0
- data/test/dummy/app/mailers/application_mailer.rb +4 -0
- data/test/dummy/app/models/application_record.rb +3 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +15 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +36 -0
- data/test/dummy/bin/update +31 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config/application.rb +20 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +32 -0
- data/test/dummy/config/environments/production.rb +57 -0
- data/test/dummy/config/environments/test.rb +33 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/content_security_policy.rb +25 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/linkedin_sign_in.rb +4 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +34 -0
- data/test/dummy/config/routes.rb +2 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config/storage.yml +34 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/package.json +5 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/helpers/button_helper_test.rb +36 -0
- data/test/key.pem +27 -0
- data/test/models/identity_test.rb +48 -0
- data/test/models/redirect_protector_test.rb +34 -0
- data/test/test_helper.rb +28 -0
- metadata +267 -0
@@ -0,0 +1,190 @@
|
|
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
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rails/engine'
|
2
|
+
|
3
|
+
module LinkedinSignIn
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace LinkedinSignIn
|
6
|
+
|
7
|
+
config.linkedin_sign_in = ActiveSupport::OrderedOptions.new
|
8
|
+
|
9
|
+
initializer 'linkedin_sign_in.config' do |app|
|
10
|
+
config.after_initialize do
|
11
|
+
LinkedinSignIn.client_id = config.linkedin_sign_in.client_id || app.credentials.dig(:linkedin_sign_in, :client_id)
|
12
|
+
LinkedinSignIn.client_secret = config.linkedin_sign_in.client_secret || app.credentials.dig(:linkedin_sign_in, :client_secret)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
initializer 'linkedin_sign_in.helpers' do
|
17
|
+
ActiveSupport.on_load :action_controller_base do
|
18
|
+
helper LinkedinSignIn::Engine.helpers
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer 'linkedin_sign_in.mount' do |app|
|
23
|
+
app.routes.prepend do
|
24
|
+
mount LinkedinSignIn::Engine, at: app.config.linkedin_sign_in.root || 'linkedin_sign_in'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
initializer 'linkedin_sign_in.parameter_filters' do |app|
|
29
|
+
app.config.filter_parameters << :code
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'linkedin-id-token'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
|
4
|
+
module LinkedinSignIn
|
5
|
+
class Identity
|
6
|
+
class ValidationError < StandardError; end
|
7
|
+
|
8
|
+
def initialize(token)
|
9
|
+
set_extracted_payload(token)
|
10
|
+
end
|
11
|
+
|
12
|
+
def user_id
|
13
|
+
@payload["id"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def first_name
|
17
|
+
@payload["firstName"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_name
|
21
|
+
@payload["lastName"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def email_address
|
25
|
+
@payload["emailAddress"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def avatar_url
|
29
|
+
@payload["pictureUrl"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_company_name
|
33
|
+
positions = @payload["positions"]["values"]
|
34
|
+
current_position = positions.find { |position| position["isCurrent"] }
|
35
|
+
current_position["company"]["name"]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def set_extracted_payload(token)
|
40
|
+
uri = URI("https://api.linkedin.com/v1/people/~:(id,firstName,lastName,picture-url,email-address,positions)?format=json")
|
41
|
+
request = Net::HTTP::Get.new uri
|
42
|
+
request['Authorization'] = "Bearer #{token}"
|
43
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
44
|
+
http.request(request)
|
45
|
+
end
|
46
|
+
|
47
|
+
case response
|
48
|
+
when Net::HTTPSuccess
|
49
|
+
@payload = JSON(response.body)
|
50
|
+
else
|
51
|
+
raise ValidationError, response.body
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module LinkedinSignIn
|
4
|
+
module RedirectProtector
|
5
|
+
extend self
|
6
|
+
|
7
|
+
class Violation < StandardError; end
|
8
|
+
|
9
|
+
QUALIFIED_URL_PATTERN = /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
|
10
|
+
|
11
|
+
def ensure_same_origin(target, source)
|
12
|
+
if target =~ QUALIFIED_URL_PATTERN && origin_of(target) != origin_of(source)
|
13
|
+
raise Violation, "Redirect target #{target} does not have same origin as request (expected #{origin_of(source)})"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def origin_of(url)
|
19
|
+
uri = URI(url)
|
20
|
+
"#{uri.scheme}://#{uri.host}:#{uri.port}"
|
21
|
+
rescue ArgumentError
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'linkedin_sign_in'
|
3
|
+
s.version = '0.3'
|
4
|
+
s.authors = ['Vincent Robert']
|
5
|
+
s.email = ['vincent.robert@genezys.net']
|
6
|
+
s.summary = 'Sign in (or up) with Linkedin for Rails applications'
|
7
|
+
s.homepage = 'https://github.com/genezys/linkedin_sign_in'
|
8
|
+
s.license = 'MIT'
|
9
|
+
|
10
|
+
s.required_ruby_version = '>= 1.9.3'
|
11
|
+
|
12
|
+
s.add_dependency 'rails', '>= 5.2.0'
|
13
|
+
s.add_dependency 'oauth2', '>= 1.4.0'
|
14
|
+
|
15
|
+
s.add_development_dependency 'bundler', '~> 1.15'
|
16
|
+
s.add_development_dependency 'jwt', '>= 1.5.6'
|
17
|
+
s.add_development_dependency 'webmock', '>= 3.4.2'
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDETCCAfmgAwIBAQIBADANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDDCRnb29n
|
3
|
+
bGUtc2lnbi1pbi1mb3ItcmFpbHMuZXhhbXBsZS5jb20wHhcNMTgwOTAyMjI0MTQw
|
4
|
+
WhcNMjMwOTAyMjI0MTQwWjAvMS0wKwYDVQQDDCRnb29nbGUtc2lnbi1pbi1mb3It
|
5
|
+
cmFpbHMuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
6
|
+
AQCtnO1OcLbdxj4f6I/aUMJkCJfrDNvp0v2ljUJoaq6hqWPmoZgcl92njBvt91np
|
7
|
+
JPfGaCy0ZYLfizUNBRKkfo6u3MXvubktYqk3SVCejy0TUE11PwRx1u/x0c2rCTa6
|
8
|
+
Y7ppAoO9Ur3yoccDmkceP8MpofHWetrdaxyhktlqy6gpM7V+kjj+anySQk4XqDJl
|
9
|
+
F4FXHt82HMNK3xbjXJyEoyMudGUISBDn/rG8b3LxEKawUiLVCI54g3+L/Oi4nZCE
|
10
|
+
XNCd/mvWpVFoPpFQGMoKW3S9KxFowvfkDSxWYwgYnWnsO0ueXS+WYML88KO1Qf7V
|
11
|
+
mEZ91u7w1/EiMVxiuczxycSfAgMBAAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0P
|
12
|
+
AQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUA
|
13
|
+
A4IBAQCVXynTCZhpbINC4j1prGaPfY6mRkCZzcRpQFim4C6hJtYSRn57qjpmYWG3
|
14
|
+
eVc3ElfNUAWgC3trACjN3hDqKv0/hH9TGTY9iFhc747L/VSaKWzH/uWewj1qTwsX
|
15
|
+
dUEFxZILAWvAMBNUT060t8bt+pSFc2h4fHsftOqFLfkFUcCr22QsWyueXzWZyDeZ
|
16
|
+
XWFGtD+WOR5SC4mIY359e75/vZsJymzZIfM+pfcaHnXtXez9SeLM81rvnRdR1b+H
|
17
|
+
/S0LT0dPRkXSvC2HRPwzHxVctNrDoaxON+OIMgd4lHAFs6doVoYmnprzO69+IBUK
|
18
|
+
s0LBENxbn2rd7IEl6EaC91cXCl3y
|
19
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class LinkedinSignIn::AuthorizationsControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
test "redirecting to Linkedin for authorization" do
|
5
|
+
post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
6
|
+
assert_response :redirect
|
7
|
+
assert_match 'https://www.linkedin.com/oauth/v2/authorization', response.location
|
8
|
+
|
9
|
+
params = extract_query_params_from(response.location)
|
10
|
+
assert_equal FAKE_LINKEDIN_CLIENT_ID, params[:client_id]
|
11
|
+
assert_equal 'login', params[:prompt]
|
12
|
+
assert_equal 'code', params[:response_type]
|
13
|
+
assert_equal 'http://www.example.com/linkedin_sign_in/callback', params[:redirect_uri]
|
14
|
+
assert_equal 'r_basicprofile r_emailaddress', params[:scope]
|
15
|
+
assert_match /[A-Za-z0-9+\/]{32}/, params[:state]
|
16
|
+
|
17
|
+
assert_equal 'http://www.example.com/login', flash[:proceed_to]
|
18
|
+
assert_equal params[:state], flash[:state]
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def extract_query_params_from(url)
|
23
|
+
query = URI(url).query
|
24
|
+
Rack::Utils.parse_query(query).symbolize_keys
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class LinkedinSignIn::CallbacksControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
test "receiving an authorization code" do
|
5
|
+
post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://www.example.com/login' }
|
6
|
+
assert_response :redirect
|
7
|
+
|
8
|
+
stub_token_request code: '4/SgCpHSVW5-Cy', access_token: 'ya29.GlwIBo', id_token: 'eyJhbGciOiJSUzI'
|
9
|
+
|
10
|
+
get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
|
11
|
+
assert_redirected_to 'http://www.example.com/login'
|
12
|
+
assert_equal 'ya29.GlwIBo', flash[:linkedin_sign_in_token]
|
13
|
+
end
|
14
|
+
|
15
|
+
test "protecting against CSRF" do
|
16
|
+
get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: 'invalid')
|
17
|
+
assert_response :unprocessable_entity
|
18
|
+
end
|
19
|
+
|
20
|
+
test "protecting against open redirects" do
|
21
|
+
post linkedin_sign_in.authorization_url, params: { proceed_to: 'http://malicious.example.com/login' }
|
22
|
+
assert_response :redirect
|
23
|
+
|
24
|
+
get linkedin_sign_in.callback_url(code: '4/SgCpHSVW5-Cy', state: flash[:state])
|
25
|
+
assert_response :bad_request
|
26
|
+
end
|
27
|
+
|
28
|
+
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))
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
2.5.0
|
data/test/dummy/Rakefile
ADDED
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
// Action Cable provides the framework to deal with WebSockets in Rails.
|
2
|
+
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
|
3
|
+
//
|
4
|
+
//= require action_cable
|
5
|
+
//= require_self
|
6
|
+
//= require_tree ./channels
|
7
|
+
|
8
|
+
(function() {
|
9
|
+
this.App || (this.App = {});
|
10
|
+
|
11
|
+
App.cable = ActionCable.createConsumer();
|
12
|
+
|
13
|
+
}).call(this);
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
File without changes
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Dummy</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= stylesheet_link_tag 'application', media: 'all' %>
|
9
|
+
<%= javascript_include_tag 'application' %>
|
10
|
+
</head>
|
11
|
+
|
12
|
+
<body>
|
13
|
+
<%= yield %>
|
14
|
+
</body>
|
15
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= yield %>
|
data/test/dummy/bin/rake
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'fileutils'
|
3
|
+
include FileUtils
|
4
|
+
|
5
|
+
# path to your application root.
|
6
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
7
|
+
|
8
|
+
def system!(*args)
|
9
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
10
|
+
end
|
11
|
+
|
12
|
+
chdir APP_ROOT do
|
13
|
+
# This script is a starting point to setup your application.
|
14
|
+
# Add necessary setup steps to this file.
|
15
|
+
|
16
|
+
puts '== Installing dependencies =='
|
17
|
+
system! 'gem install bundler --conservative'
|
18
|
+
system('bundle check') || system!('bundle install')
|
19
|
+
|
20
|
+
# Install JavaScript dependencies if using Yarn
|
21
|
+
# system('bin/yarn')
|
22
|
+
|
23
|
+
# puts "\n== Copying sample files =="
|
24
|
+
# unless File.exist?('config/database.yml')
|
25
|
+
# cp 'config/database.yml.sample', 'config/database.yml'
|
26
|
+
# end
|
27
|
+
|
28
|
+
puts "\n== Preparing database =="
|
29
|
+
system! 'bin/rails db:setup'
|
30
|
+
|
31
|
+
puts "\n== Removing old logs and tempfiles =="
|
32
|
+
system! 'bin/rails log:clear tmp:clear'
|
33
|
+
|
34
|
+
puts "\n== Restarting application server =="
|
35
|
+
system! 'bin/rails restart'
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'fileutils'
|
3
|
+
include FileUtils
|
4
|
+
|
5
|
+
# path to your application root.
|
6
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
7
|
+
|
8
|
+
def system!(*args)
|
9
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
10
|
+
end
|
11
|
+
|
12
|
+
chdir APP_ROOT do
|
13
|
+
# This script is a way to update your development environment automatically.
|
14
|
+
# Add necessary update steps to this file.
|
15
|
+
|
16
|
+
puts '== Installing dependencies =='
|
17
|
+
system! 'gem install bundler --conservative'
|
18
|
+
system('bundle check') || system!('bundle install')
|
19
|
+
|
20
|
+
# Install JavaScript dependencies if using Yarn
|
21
|
+
# system('bin/yarn')
|
22
|
+
|
23
|
+
puts "\n== Updating database =="
|
24
|
+
system! 'bin/rails db:migrate'
|
25
|
+
|
26
|
+
puts "\n== Removing old logs and tempfiles =="
|
27
|
+
system! 'bin/rails log:clear tmp:clear'
|
28
|
+
|
29
|
+
puts "\n== Restarting application server =="
|
30
|
+
system! 'bin/rails restart'
|
31
|
+
end
|
data/test/dummy/bin/yarn
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
APP_ROOT = File.expand_path('..', __dir__)
|
3
|
+
Dir.chdir(APP_ROOT) do
|
4
|
+
begin
|
5
|
+
exec "yarnpkg", *ARGV
|
6
|
+
rescue Errno::ENOENT
|
7
|
+
$stderr.puts "Yarn executable was not detected in the system."
|
8
|
+
$stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
|
9
|
+
exit 1
|
10
|
+
end
|
11
|
+
end
|