sso 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|