sso 0.1.1 → 0.1.2
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/lib/sso/client/authentications/passport.rb +1 -1
- data/lib/sso/client/passport_verifier.rb +21 -3
- data/lib/sso/client/warden/hooks/after_fetch.rb +1 -1
- data/lib/sso/server.rb +1 -0
- data/lib/sso/server/authentications/passport.rb +2 -2
- data/lib/sso/server/configuration.rb +12 -2
- data/lib/sso/server/middleware/passport_destruction.rb +1 -1
- data/lib/sso/server/middleware/passport_exchange.rb +3 -10
- data/lib/sso/server/passport.rb +1 -1
- data/lib/sso/server/passports.rb +1 -22
- data/lib/sso/server/passports/activity.rb +92 -0
- data/spec/lib/sso/client/authentications/passport_spec.rb +18 -5
- data/spec/lib/sso/client/warden/hooks/after_fetch_spec.rb +2 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/sso/test/helpers.rb +2 -2
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e1f2feee69f64105e38a748befeebe0de6d33a5
|
4
|
+
data.tar.gz: c0637201a0d104c075dc7c049eef0d9f381e0596
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3fbaa085b9054c762040a7a91f19999ce4a7745b388c431ff754f7b7c23ffc6ac21e15f8bc3cfeb995dd8803682cd363117f13577ccd4c7ddb24647f96a179db
|
7
|
+
data.tar.gz: 336dfa7ce8ee44a663fe2a8678c67966f021f4a2927fded0d239333877e9043e9fead2c503f1296d2df54b225e913cfbfed669d43ad612e3a9921679a710c346
|
@@ -127,7 +127,7 @@ module SSO
|
|
127
127
|
decipher.key = chip_key
|
128
128
|
decipher.iv = chip_iv
|
129
129
|
plaintext = decipher.update(chip_ciphertext) + decipher.final
|
130
|
-
logger.debug { "
|
130
|
+
logger.debug { "Decrypted chip plaintext #{plaintext.inspect} using key #{chip_key.inspect} and iv #{chip_iv.inspect} and ciphertext #{chip_ciphertext.inspect}" }
|
131
131
|
plaintext
|
132
132
|
end
|
133
133
|
end
|
@@ -52,7 +52,7 @@ module SSO
|
|
52
52
|
def received_passport
|
53
53
|
::SSO::Client::Passport.new received_passport_attributes
|
54
54
|
|
55
|
-
rescue ArgumentError
|
55
|
+
rescue ArgumentError
|
56
56
|
error { "Could not instantiate Passport from serialized response #{received_passport_attributes.inspect}" }
|
57
57
|
raise
|
58
58
|
end
|
@@ -66,7 +66,25 @@ module SSO
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def params
|
69
|
-
{ ip: user_ip, agent: user_agent, device_id: device_id, state: passport_state }
|
69
|
+
result = { ip: user_ip, agent: user_agent, device_id: device_id, state: passport_state }
|
70
|
+
result.merge! insider_id: insider_id, insider_signature: insider_signature
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
def insider_id
|
75
|
+
::SSO.config.oauth_client_id
|
76
|
+
end
|
77
|
+
|
78
|
+
def insider_secret
|
79
|
+
::SSO.config.oauth_client_secret
|
80
|
+
end
|
81
|
+
|
82
|
+
def insider_signature
|
83
|
+
::OpenSSL::HMAC.hexdigest signature_digest, insider_secret, user_ip
|
84
|
+
end
|
85
|
+
|
86
|
+
def signature_digest
|
87
|
+
OpenSSL::Digest.new 'sha1'
|
70
88
|
end
|
71
89
|
|
72
90
|
def token
|
@@ -89,7 +107,7 @@ module SSO
|
|
89
107
|
(timeout_in_milliseconds / 1000).round 2
|
90
108
|
end
|
91
109
|
|
92
|
-
# TODO Needs to be configurable
|
110
|
+
# TODO: Needs to be configurable
|
93
111
|
def path
|
94
112
|
OmniAuth::Strategies::SSO.passports_path
|
95
113
|
end
|
@@ -109,7 +109,7 @@ module SSO
|
|
109
109
|
# This will be a hook for e.g. statistics, benchmarking, etc, measure everything
|
110
110
|
end
|
111
111
|
|
112
|
-
# TODO Use ActionDispatch remote IP or you might get the Load Balancer's IP instead :(
|
112
|
+
# TODO: Use ActionDispatch remote IP or you might get the Load Balancer's IP instead :(
|
113
113
|
def ip
|
114
114
|
request.ip
|
115
115
|
end
|
data/lib/sso/server.rb
CHANGED
@@ -54,7 +54,6 @@ module SSO
|
|
54
54
|
yield Operations.failure :passport_not_found if passport.blank?
|
55
55
|
yield Operations.failure :passport_revoked if passport.invalid?
|
56
56
|
debug { 'Arguments are fine.' }
|
57
|
-
#yield Operations.failure :user_not_encapsulated if passport.user.blank?
|
58
57
|
Operations.success :arguments_are_valid
|
59
58
|
end
|
60
59
|
|
@@ -83,7 +82,8 @@ module SSO
|
|
83
82
|
end
|
84
83
|
|
85
84
|
def passport!
|
86
|
-
|
85
|
+
record = backend.find_by_id(passport_id)
|
86
|
+
return unless record
|
87
87
|
debug { "Successfully loaded Passport #{passport_id} from database." }
|
88
88
|
record
|
89
89
|
end
|
@@ -37,6 +37,16 @@ module SSO
|
|
37
37
|
end
|
38
38
|
attr_writer :passport_verification_timeout_ms
|
39
39
|
|
40
|
+
def oauth_client_id
|
41
|
+
@oauth_client_id || fail('You need to configure the oauth_client_id, see SSO::Configuration for more info.')
|
42
|
+
end
|
43
|
+
attr_writer :oauth_client_id
|
44
|
+
|
45
|
+
def oauth_client_secret
|
46
|
+
@oauth_client_secret || fail('You need to configure the oauth_client_secret, see SSO::Configuration for more info.')
|
47
|
+
end
|
48
|
+
attr_writer :oauth_client_secret
|
49
|
+
|
40
50
|
# Both
|
41
51
|
|
42
52
|
def exception_handler
|
@@ -89,13 +99,13 @@ module SSO
|
|
89
99
|
proc do |exception|
|
90
100
|
return unless ::SSO.config.logger
|
91
101
|
::SSO.config.logger.error(self.class) do
|
92
|
-
"An internal error
|
102
|
+
"An internal error occurred #{exception.class.name} #{exception.message} #{exception.backtrace[0..5].join(' ') if exception.backtrace}"
|
93
103
|
end
|
94
104
|
end
|
95
105
|
end
|
96
106
|
|
97
107
|
def default_human_readable_location_for_ip
|
98
|
-
proc do
|
108
|
+
proc do
|
99
109
|
'Unknown'
|
100
110
|
end
|
101
111
|
end
|
@@ -11,7 +11,7 @@ module SSO
|
|
11
11
|
def call(env)
|
12
12
|
request = Rack::Request.new(env)
|
13
13
|
|
14
|
-
|
14
|
+
unless request.delete? && request.path.start_with?(passports_path)
|
15
15
|
debug { "I'm not interested in this #{request.request_method.inspect} request to #{request.path.inspect} I only care for DELETE #{passports_path.inspect}" }
|
16
16
|
return @app.call(env)
|
17
17
|
end
|
@@ -12,10 +12,8 @@ module SSO
|
|
12
12
|
|
13
13
|
def call(env)
|
14
14
|
request = Rack::Request.new(env)
|
15
|
-
remote_ip = request.env['action_dispatch.remote_ip'].to_s
|
16
|
-
device_id = request.params['device_id']
|
17
15
|
|
18
|
-
|
16
|
+
unless request.post? && request.path == passports_path
|
19
17
|
debug { "I'm not interested in this #{request.request_method.inspect} request to #{request.path.inspect} I only care for POST #{passports_path.inspect}" }
|
20
18
|
return @app.call(env)
|
21
19
|
end
|
@@ -24,13 +22,8 @@ module SSO
|
|
24
22
|
debug { "Detected incoming Passport creation request for access token #{token.inspect}" }
|
25
23
|
access_token = ::Doorkeeper::AccessToken.find_by_token token
|
26
24
|
|
27
|
-
unless access_token
|
28
|
-
|
29
|
-
end
|
30
|
-
|
31
|
-
unless access_token.valid?
|
32
|
-
return json_code :access_token_invalid
|
33
|
-
end
|
25
|
+
return json_code :access_token_not_found unless access_token
|
26
|
+
return json_code :access_token_invalid unless access_token.valid?
|
34
27
|
|
35
28
|
finding = ::SSO::Server::Passports.find_by_access_token_id(access_token.id)
|
36
29
|
if finding.failure?
|
data/lib/sso/server/passport.rb
CHANGED
@@ -71,7 +71,7 @@ module SSO
|
|
71
71
|
ciphertext = cipher.update chip_plaintext
|
72
72
|
ciphertext << cipher.final
|
73
73
|
debug { "The Passport chip plaintext #{chip_plaintext.inspect} was encrypted using key #{chip_key.inspect} and IV #{chip_iv.inspect} and resultet in ciphertext #{ciphertext.inspect}" }
|
74
|
-
chip = [Base64.encode64(ciphertext).strip
|
74
|
+
chip = [Base64.encode64(ciphertext).strip, Base64.encode64(chip_iv).strip].join('|')
|
75
75
|
logger.debug { "Augmented passport #{id.inspect} with chip #{chip.inspect}" }
|
76
76
|
chip
|
77
77
|
end
|
data/lib/sso/server/passports.rb
CHANGED
@@ -43,28 +43,7 @@ module SSO
|
|
43
43
|
def self.update_activity(passport_id:, request:)
|
44
44
|
record = find_valid_passport(passport_id) { |failure| return failure }
|
45
45
|
|
46
|
-
|
47
|
-
if record.insider?
|
48
|
-
proxied_ip = request['ip']
|
49
|
-
unless proxied_ip
|
50
|
-
warn { "There should have been a proxied IP param, but there was none. I will use the immediare IP #{immediate_ip} now." }
|
51
|
-
proxied_ip = immediate_ip
|
52
|
-
end
|
53
|
-
attributes = { ip: proxied_ip, agent: request['agent'], device: request['device_id'] }
|
54
|
-
else
|
55
|
-
attributes = { ip: immediate_ip, agent: request.user_agent, device: request.params['device_id'] }
|
56
|
-
end
|
57
|
-
attributes.merge! activity_at: Time.now
|
58
|
-
|
59
|
-
record.stamps ||= {} # <- Not thread-safe, this may potentially delete all existing stamps, I guess
|
60
|
-
record.stamps[attributes[:ip]] = Time.now.to_i
|
61
|
-
|
62
|
-
debug { "Updating activity of #{record.insider? ? :insider : :outsider} Passport #{passport_id.inspect} using IP #{attributes[:ip]} agent #{attributes[:agent]} and device #{attributes[:device]}" }
|
63
|
-
if record.update_attributes(attributes)
|
64
|
-
Operations.success :passport_metadata_updated
|
65
|
-
else
|
66
|
-
Operations.failure :could_not_update_passport_activity, object: record.errors.to_hash
|
67
|
-
end
|
46
|
+
::SSO::Server::Passports::Activity.new(passport: record, request: request).call
|
68
47
|
end
|
69
48
|
|
70
49
|
def self.register_authorization_grant(passport_id:, token:)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module SSO
|
2
|
+
module Server
|
3
|
+
module Passports
|
4
|
+
class Activity
|
5
|
+
include ::SSO::Logging
|
6
|
+
|
7
|
+
attr_reader :passport, :request
|
8
|
+
|
9
|
+
def initialize(passport:, request:)
|
10
|
+
@passport = passport
|
11
|
+
@request = request
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
if passport.insider? || trusted_proxy_app?
|
16
|
+
proxied_ip = request['ip']
|
17
|
+
unless proxied_ip
|
18
|
+
warn { "There should have been a proxied IP param, but there was none. I will use the immediate IP #{immediate_ip} now." }
|
19
|
+
proxied_ip = immediate_ip
|
20
|
+
end
|
21
|
+
attributes = { ip: proxied_ip, agent: request['agent'], device: request['device_id'] }
|
22
|
+
else
|
23
|
+
attributes = { ip: immediate_ip, agent: request.user_agent, device: request['device_id'] }
|
24
|
+
end
|
25
|
+
attributes.merge! activity_at: Time.now
|
26
|
+
|
27
|
+
passport.stamps ||= {} # <- Not thread-safe, this may potentially delete all existing stamps, I guess
|
28
|
+
passport.stamps[attributes[:ip]] = Time.now.to_i
|
29
|
+
|
30
|
+
debug { "Updating activity of #{passport.insider? ? :insider : :outsider} Passport #{passport.id.inspect} using IP #{attributes[:ip]} agent #{attributes[:agent]} and device #{attributes[:device]}" }
|
31
|
+
if passport.update_attributes(attributes)
|
32
|
+
Operations.success :passport_metadata_updated
|
33
|
+
else
|
34
|
+
Operations.failure :could_not_update_passport_activity, object: passport.errors.to_hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def trusted_proxy_app?
|
39
|
+
unless insider_id
|
40
|
+
debug { 'This is an immediate request because there is no insider_id param' }
|
41
|
+
return
|
42
|
+
end
|
43
|
+
|
44
|
+
unless insider_signature
|
45
|
+
debug { 'This is an immediate request because there is no insider_signature param' }
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
application = ::Doorkeeper::Application.find_by_id(insider_id)
|
50
|
+
unless application
|
51
|
+
warn { 'The insider_id param does not correspond to an existing Doorkeeper Application' }
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
unless application.scopes.include?('insider')
|
56
|
+
warn { 'The Doorkeeper Application belonging to this insider_id param is considered an outsider' }
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
expected_signature = ::OpenSSL::HMAC.hexdigest signature_digest, application.secret, proxied_ip
|
61
|
+
unless insider_signature == expected_signature
|
62
|
+
warn { "The insider signature #{insider_signature.inspect} does not match my expectation #{expected_signature.inspect}" }
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
debug { 'This is a proxied request because insider_id and insider_signature are valid' }
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def signature_digest
|
71
|
+
OpenSSL::Digest.new 'sha1'
|
72
|
+
end
|
73
|
+
|
74
|
+
def proxied_ip
|
75
|
+
request['ip']
|
76
|
+
end
|
77
|
+
|
78
|
+
def insider_id
|
79
|
+
request['insider_id']
|
80
|
+
end
|
81
|
+
|
82
|
+
def insider_signature
|
83
|
+
request['insider_signature']
|
84
|
+
end
|
85
|
+
|
86
|
+
def immediate_ip
|
87
|
+
request.respond_to?(:remote_ip) ? request.remote_ip : request.ip
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
RSpec.describe SSO::Client::Authentications::Passport, type: :request, db: true do
|
3
|
+
RSpec.describe SSO::Client::Authentications::Passport, type: :request, db: true, reveal_exceptions: true do
|
4
4
|
|
5
5
|
# Untrusted Client
|
6
6
|
let(:request_method) { 'GET' }
|
@@ -31,12 +31,15 @@ RSpec.describe SSO::Client::Authentications::Passport, type: :request, db: true
|
|
31
31
|
let(:passport_chip) { server_passport.chip! }
|
32
32
|
|
33
33
|
# Server
|
34
|
+
let(:oauth_client) { create :outsider_doorkeeper_application }
|
34
35
|
let(:insider) { false }
|
35
36
|
let(:server_user) { create :user, name: 'Emily', tags: %i(cool nice) }
|
36
37
|
let!(:server_passport) { create :passport, user: server_user, owner_id: server_user.id, ip: ip, agent: agent, insider: insider }
|
37
38
|
|
38
39
|
before do
|
39
40
|
SSO.config.passport_chip_key = SecureRandom.hex
|
41
|
+
SSO.config.oauth_client_id = oauth_client.id
|
42
|
+
SSO.config.oauth_client_secret = oauth_client.secret
|
40
43
|
end
|
41
44
|
|
42
45
|
context 'no changes' do
|
@@ -57,16 +60,26 @@ RSpec.describe SSO::Client::Authentications::Passport, type: :request, db: true
|
|
57
60
|
expect(passport).to be_modified
|
58
61
|
end
|
59
62
|
|
60
|
-
it 'tracks the immediate request IP' do
|
61
|
-
expect(server_passport.reload.ip).to eq '127.0.0.1'
|
62
|
-
end
|
63
|
-
|
64
63
|
it 'attaches the user attributes to the passport' do
|
65
64
|
expect(passport.user).to be_instance_of Hash
|
66
65
|
expect(passport.user['name']).to eq 'Emily'
|
67
66
|
expect(passport.user['email']).to eq 'emily@example.com'
|
68
67
|
expect(passport.user['tags'].sort).to eq %w(cool is_working_from_home nice).sort
|
69
68
|
end
|
69
|
+
|
70
|
+
context 'outsider oauth client' do
|
71
|
+
it 'tracks the immediate request IP' do
|
72
|
+
expect(server_passport.reload.ip).to eq '127.0.0.1'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'insider oauth client' do
|
77
|
+
let(:oauth_client) { create :insider_doorkeeper_application }
|
78
|
+
|
79
|
+
it 'tracks the untrusted client IP' do
|
80
|
+
expect(server_passport.reload.ip).to eq ip
|
81
|
+
end
|
82
|
+
end
|
70
83
|
end
|
71
84
|
|
72
85
|
context 'insider passport' do
|
@@ -27,6 +27,8 @@ RSpec.describe SSO::Client::Warden::Hooks::AfterFetch, type: :request, db: true
|
|
27
27
|
# The server dynamically injects some tags. In order to calculate the user state correctly in our test setup,
|
28
28
|
# We need to "simulate" what the tags will look like once the server modified them. No big problem.
|
29
29
|
allow(server_user).to receive(:tags).and_return %w(wears_glasses is_working_from_home never_gives_up)
|
30
|
+
SSO.config.oauth_client_id = SecureRandom.hex
|
31
|
+
SSO.config.oauth_client_secret = SecureRandom.hex
|
30
32
|
end
|
31
33
|
|
32
34
|
context 'invalid passport' do
|
data/spec/spec_helper.rb
CHANGED
@@ -44,8 +44,16 @@ RSpec.configure do |config|
|
|
44
44
|
DatabaseCleaner.start
|
45
45
|
end
|
46
46
|
|
47
|
+
config.before :each, reveal_exceptions: true do
|
48
|
+
SSO.config.exception_handler = proc { |exception| fail exception }
|
49
|
+
end
|
50
|
+
|
47
51
|
config.after :each do
|
48
52
|
Timecop.return
|
53
|
+
SSO.config.exception_handler = nil
|
54
|
+
SSO.config.passport_chip_key = nil
|
55
|
+
SSO.config.oauth_client_id = nil
|
56
|
+
SSO.config.oauth_client_secret = nil
|
49
57
|
end
|
50
58
|
|
51
59
|
config.after :each, db: true do
|
@@ -5,8 +5,8 @@ module SSO
|
|
5
5
|
module Helpers
|
6
6
|
|
7
7
|
# Inspired by Warden::Spec::Helpers
|
8
|
-
def env_with_params(path =
|
9
|
-
method = params.delete(:method) ||
|
8
|
+
def env_with_params(path = '/', params = {}, env = {})
|
9
|
+
method = params.delete(:method) || 'GET'
|
10
10
|
env = { 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => "#{method}" }.merge(env)
|
11
11
|
Rack::MockRequest.env_for "#{path}?#{Rack::Utils.build_query(params)}", env
|
12
12
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- halo
|
@@ -281,6 +281,7 @@ files:
|
|
281
281
|
- lib/sso/server/middleware/passport_verification.rb
|
282
282
|
- lib/sso/server/passport.rb
|
283
283
|
- lib/sso/server/passports.rb
|
284
|
+
- lib/sso/server/passports/activity.rb
|
284
285
|
- lib/sso/server/warden/hooks/after_authentication.rb
|
285
286
|
- lib/sso/server/warden/hooks/before_logout.rb
|
286
287
|
- lib/sso/server/warden/strategies/passport.rb
|