sso 0.1.0.alpha5 → 0.1.0.alpha6

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sso/client/README.md +17 -2
  3. data/lib/sso/client/authentications/passport.rb +176 -0
  4. data/lib/sso/client/passport.rb +7 -3
  5. data/lib/sso/client/passport_verifier.rb +130 -0
  6. data/lib/sso/client/warden/hooks/after_fetch.rb +32 -81
  7. data/lib/sso/client/warden/strategies/passport.rb +43 -0
  8. data/lib/sso/client.rb +3 -0
  9. data/lib/sso/server/authentications/passport.rb +16 -49
  10. data/lib/sso/server/configuration.rb +36 -6
  11. data/lib/sso/server/doorkeeper/access_token_marker.rb +7 -7
  12. data/lib/sso/server/middleware/passport_destruction.rb +40 -0
  13. data/lib/sso/server/middleware/{passport_creation.rb → passport_exchange.rb} +15 -11
  14. data/lib/sso/server/middleware/passport_verification.rb +2 -2
  15. data/lib/sso/server/passport.rb +43 -15
  16. data/lib/sso/server/passports.rb +59 -31
  17. data/lib/sso/server/warden/hooks/after_authentication.rb +1 -1
  18. data/lib/sso/server/warden/hooks/before_logout.rb +1 -1
  19. data/lib/sso/server/warden/strategies/passport.rb +8 -6
  20. data/lib/sso/server.rb +2 -3
  21. data/spec/dummy/app/controllers/sessions_controller.rb +1 -1
  22. data/spec/dummy/config/application.rb +6 -0
  23. data/spec/dummy/config/initializers/sso.rb +10 -6
  24. data/spec/dummy/config/initializers/warden.rb +3 -11
  25. data/spec/dummy/db/migrate/20150303132931_create_passports_table.rb +29 -15
  26. data/spec/dummy/db/schema.rb +10 -5
  27. data/spec/integration/oauth/authorization_code_spec.rb +80 -10
  28. data/spec/integration/oauth/{password_verification_spec.rb → password_spec.rb} +39 -3
  29. data/spec/lib/sso/client/authentications/passport_spec.rb +92 -0
  30. data/spec/{integration/oauth → lib/sso/client/warden/hooks}/after_fetch_spec.rb +4 -3
  31. data/spec/lib/sso/server/middleware/passport_destruction_spec.rb +33 -0
  32. data/spec/lib/sso/server/passports_spec.rb +104 -0
  33. data/spec/spec_helper.rb +2 -0
  34. data/spec/support/factories/doorkeeper/application.rb +0 -3
  35. data/spec/support/factories/server/passport.rb +5 -1
  36. data/spec/support/factories/server/user.rb +1 -1
  37. metadata +31 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b81f083797ce01186f402b3a38e3a8e5cc4a6d2e
4
- data.tar.gz: 5056b770891d6cf3655cb81513ad3fe88d542bde
3
+ metadata.gz: 5719f768df38f21f0427bf887e96e6c300380a52
4
+ data.tar.gz: cba2104943a75bb640747ba088229c8b14b7a1d5
5
5
  SHA512:
6
- metadata.gz: 1ae490829e5875ca39dcc25c1cfb9352fc856111329f5a3f2b1f33164156d2c35eb3e21897f8aa9d5233279d4d5a9415c5e0209c2ca42b88545e5c3cb3bc6d88
7
- data.tar.gz: 52b8f08786e63e9f22b0792081b6efa752ec2bc15e201bd42cae6a984c92749986da46ef9cf8cfe20d6214930ccd8b5dfe1ae7e845bc2e92119f1a1f8a3810f1
6
+ metadata.gz: 938d6845a25437b1b7fc5c20d9709031f50afaec5e9b9359eb6686db8a70e5ff3bd02f4f45fed36c5fc8b08275be08801ea9ec19228486caf910c5c049522b1c
7
+ data.tar.gz: 8983a717b6a800e7add505e27c1d9fce1368b8a9e651d1625e00ee3a968ad3ff8ba321019d5cb8908767db5231c6b53c088b190fdbcd45c3b9d7cdb1cddc8c22
@@ -18,7 +18,7 @@
18
18
  * A public OAuth Client, such as an `iPhone`, uses the `Resource Owner Password Credentials Grant` to exchange the `username` and `password` of the end user for an OAuth `access_token` with the OAuth permission scope `outsider`.
19
19
  * You exchange the `access_token` for a passport token. That is effectively your API token used to communicate with the OAuth Rails clients.
20
20
  * The OAuth Rails clients verify that token with the OAuth server at every request.
21
- * In effect, this turns your iPhone app into a Browser, technically not an OAuth Client.
21
+ * In effect, this turns your iPhone app into a Browser, technically not a trusted OAuth Client.
22
22
 
23
23
  #### Also good to know
24
24
 
@@ -35,7 +35,22 @@ gem 'sso', require: 'sso/client'
35
35
 
36
36
  #### Make sure you activated the Warden middleware provided by the `warden` gem
37
37
 
38
- See [the Warden wiki](https://github.com/hassox/warden/wiki/Setup)
38
+ See [the Warden wiki](https://github.com/hassox/warden/wiki/Setup).
39
+ However, one thing is special here, you must not store the entire object, but only a reference to the passport.
40
+ If you store the entire object, that would be a major security risk and allow for cookie replay attacks.
41
+
42
+ ```
43
+ class Warden::SessionSerializer
44
+ def serialize(passport)
45
+ Redis.set passport.id, passport.to_json
46
+ end
47
+
48
+ def deserialize(passport_id)
49
+ json = Redis.get passport_id
50
+ SSO::Client::Passport.new JSON.parse(json)
51
+ end
52
+ end
53
+ ```
39
54
 
40
55
  #### Set the URL to the SSO Server
41
56
 
@@ -0,0 +1,176 @@
1
+ module SSO
2
+ module Client
3
+ module Authentications
4
+ class Passport
5
+ include ::SSO::Logging
6
+ include ::SSO::Benchmarking
7
+
8
+ delegate :params, to: :request
9
+
10
+ def initialize(request)
11
+ @request = request
12
+ end
13
+
14
+ def authenticate
15
+ debug { "Performing authentication..." }
16
+ result = authenticate!
17
+
18
+ if result.success?
19
+ debug { "Authentication succeeded." }
20
+ return result
21
+ end
22
+
23
+ debug { "The Client Passport authentication failed: #{result.code}" }
24
+ Operations.failure :passport_authentication_failed, object: failure_rack_array
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :request, :passport_id
30
+
31
+ def authenticate!
32
+ chip_decryption { |failure| return failure }
33
+ check_request_signature { |failure| return failure }
34
+ passport = retrieve_passport { |failure| return failure }
35
+ passport.verified!
36
+
37
+ Operations.success :passport_received, object: passport
38
+ end
39
+
40
+ def retrieve_passport
41
+ debug { 'Retrieving Passport from server...' }
42
+ if verification.success? && verification.code == :passport_valid_and_modified
43
+ passport = verification.object
44
+
45
+ debug { "Successfully retrieved Passport with ID #{passport_id} from server." }
46
+ return passport
47
+ else
48
+ debug { 'Could not obtain Passport from server.' }
49
+ yield verification
50
+ end
51
+ end
52
+
53
+ def check_request_signature
54
+ debug { "Verifying request signature using Passport secret #{passport_secret.inspect}" }
55
+ signature_request.authenticate do |passport_id|
56
+ @passport_id = passport_id
57
+ Signature::Token.new passport_id, passport_secret
58
+ end
59
+ debug { 'Signature looks legit.' }
60
+ Operations.success :passport_signature_valid
61
+
62
+ rescue ::Signature::AuthenticationError => exception
63
+ debug { "The Signature Authentication failed. #{exception.message}" }
64
+ yield Operations.failure :invalid_passport_signature
65
+ end
66
+
67
+ def verifier
68
+ ::SSO::Client::PassportVerifier.new passport_id: passport_id, passport_state: 'refresh', passport_secret: passport_secret, user_ip: ip, user_agent: agent, device_id: device_id
69
+ end
70
+
71
+ def verification
72
+ @verification ||= verifier.call
73
+ end
74
+
75
+ def failure_rack_array
76
+ payload = { success: true, code: :passport_verification_failed }
77
+ [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
78
+ end
79
+
80
+ def signature_request
81
+ debug { "Verifying signature of #{request.request_method.inspect} #{request.path.inspect} #{request.params.inspect}"}
82
+ ::Signature::Request.new request.request_method, request.path, request.params
83
+ end
84
+
85
+ def check_chip
86
+ Operations.success :chip_syntax_valid
87
+ end
88
+
89
+ def chip_decryption
90
+ debug { "Validating chip decryptability of raw chip #{chip.inspect}"}
91
+ yield Operations.failure(:missing_chip, object: params) if chip.blank?
92
+ yield Operations.failure(:missing_chip_key) unless chip_key
93
+ yield Operations.failure(:missing_chip_iv) unless chip_iv
94
+ Operations.success :here_is_your_chip_plaintext, object: decrypt_chip
95
+
96
+ rescue OpenSSL::Cipher::CipherError => exception
97
+ yield Operations.failure :chip_decryption_failed, object: exception.message
98
+ end
99
+
100
+ def decrypt_chip
101
+ @decrypt_chip ||= decrypt_chip!
102
+ end
103
+
104
+ def decrypt_chip!
105
+ benchmark 'Passport chip decryption' do
106
+ decipher = chip_digest
107
+ decipher.decrypt
108
+ decipher.key = chip_key
109
+ decipher.iv = chip_iv
110
+ plaintext = decipher.update(chip_ciphertext) + decipher.final
111
+ logger.debug { "Decryptied chip plaintext #{plaintext.inspect} using key #{chip_key.inspect} and iv #{chip_iv.inspect} and ciphertext #{chip_ciphertext.inspect}"}
112
+ plaintext
113
+ end
114
+ end
115
+
116
+ def passport_secret
117
+ decrypt_chip
118
+ end
119
+
120
+ def chip_key
121
+ ::SSO.config.passport_chip_key
122
+ end
123
+
124
+ def user_state_digest
125
+ ::OpenSSL::Digest.new 'sha1'
126
+ end
127
+
128
+ def chip_ciphertext
129
+ Base64.decode64 encoded_chip_ciphertext
130
+ end
131
+
132
+ def encoded_chip_ciphertext
133
+ chip_ciphertext_and_iv.first
134
+ end
135
+
136
+ def chip_iv
137
+ Base64.decode64 chip_ciphertext_and_iv.last
138
+ end
139
+
140
+ def encoded_chip_iv
141
+ chip_iv
142
+ end
143
+
144
+ def chip_ciphertext_and_iv
145
+ chip.to_s.split '|'
146
+ end
147
+
148
+ def chip
149
+ params['passport_chip']
150
+ end
151
+
152
+ #def warden
153
+ # request.env['warden']
154
+ #end
155
+
156
+ def chip_digest
157
+ ::OpenSSL::Cipher::AES256.new :CBC
158
+ end
159
+
160
+ # TODO Use ActionDispatch remote IP or you might get the Load Balancer's IP instead :(
161
+ def ip
162
+ request.ip
163
+ end
164
+
165
+ def agent
166
+ request.user_agent
167
+ end
168
+
169
+ def device_id
170
+ request.params['device_id']
171
+ end
172
+
173
+ end
174
+ end
175
+ end
176
+ end
@@ -2,10 +2,14 @@ module SSO
2
2
  module Client
3
3
  class Passport
4
4
 
5
- attr_reader :id, :secret, :state, :user
5
+ attr_reader :id, :secret, :state, :user, :chip
6
6
 
7
- def initialize(id:, secret:, state:, user:)
8
- @id, @secret, @state, @user = id, secret, state, user
7
+ def initialize(id:, secret:, state:, user:, chip: nil)
8
+ @id = id
9
+ @secret = secret
10
+ @state = state
11
+ @user = user
12
+ @chip = chip
9
13
  end
10
14
 
11
15
  def verified!
@@ -0,0 +1,130 @@
1
+ module SSO
2
+ module Client
3
+ class PassportVerifier
4
+ include ::SSO::Benchmarking
5
+
6
+ attr_reader :passport_id, :passport_state, :passport_secret, :user_ip, :user_agent, :device_id
7
+
8
+ def initialize(passport_id:, passport_state:, passport_secret:, user_ip:, user_agent: nil, device_id: nil)
9
+ @passport_id = passport_id
10
+ @passport_state = passport_state
11
+ @passport_secret = passport_secret
12
+ @user_ip = user_ip
13
+ @user_agent = user_agent
14
+ @device_id = device_id
15
+ end
16
+
17
+ def call
18
+ get_response { |failure| return failure }
19
+ interpret_response
20
+
21
+ rescue JSON::ParserError
22
+ error { 'SSO Server response is not valid JSON.' }
23
+ error { response.inspect }
24
+ end
25
+
26
+ private
27
+
28
+ def get_response
29
+ yield Operations.failure(:server_unreachable, object: response) unless response.code == 200
30
+ yield Operations.failure(:server_response_not_parseable, object: response) unless parsed_response
31
+ yield Operations.failure(:server_response_missing_success_flag, object: response) unless response_has_success_flag?
32
+ yield Operations.failure(:server_response_unsuccessful, object: response) unless parsed_response['success'].to_s == 'true'
33
+ Operations.success :server_response_looks_legit
34
+ end
35
+
36
+ def interpret_response
37
+ debug { "Interpreting response code #{response_code.inspect}" }
38
+
39
+ case response_code
40
+ when :passpord_unmodified then Operations.success(:passport_valid)
41
+ when :passport_changed then Operations.success(:passport_valid_and_modified, object: received_passport)
42
+ when :passport_invalid then Operations.failure(:passport_invalid)
43
+ else Operations.failure(:unexpected_server_response_status, object: response)
44
+ end
45
+ end
46
+
47
+ def response_code
48
+ return :unknown_response_code if parsed_response['code'].to_s == ''
49
+ parsed_response['code'].to_s.to_sym
50
+ end
51
+
52
+ def received_passport
53
+ ::SSO::Client::Passport.new received_passport_attributes
54
+
55
+ rescue ArgumentError => exception
56
+ error { "Could not instantiate Passport from serialized response #{received_passport_attributes.inspect}" }
57
+ raise
58
+ end
59
+
60
+ def received_passport_attributes
61
+ attributes = parsed_response['passport']
62
+ attributes.keys.each do |key|
63
+ attributes[(key.to_sym rescue key) || key] = attributes.delete(key)
64
+ end
65
+ attributes
66
+ end
67
+
68
+ def params
69
+ { ip: user_ip, agent: user_agent, device_id: device_id, state: passport_state }
70
+ end
71
+
72
+ def token
73
+ Signature::Token.new passport_id, passport_secret
74
+ end
75
+
76
+ def signature_request
77
+ Signature::Request.new('GET', path, params)
78
+ end
79
+
80
+ def auth_hash
81
+ signature_request.sign token
82
+ end
83
+
84
+ def timeout_in_milliseconds
85
+ ::SSO.config.passport_verification_timeout_ms.to_i
86
+ end
87
+
88
+ def timeout_in_seconds
89
+ (timeout_in_milliseconds / 1000).round 2
90
+ end
91
+
92
+ # TODO Needs to be configurable
93
+ def path
94
+ OmniAuth::Strategies::SSO.passports_path
95
+ end
96
+
97
+ def base_endpoint
98
+ OmniAuth::Strategies::SSO.endpoint
99
+ end
100
+
101
+ def endpoint
102
+ URI.join(base_endpoint, path).to_s
103
+ end
104
+
105
+ def query_params
106
+ params.merge auth_hash
107
+ end
108
+
109
+ def response
110
+ @response ||= response!
111
+ end
112
+
113
+ def response!
114
+ debug { "Fetching Passport from #{endpoint.inspect}" }
115
+ benchmark 'Passport authorization request' do
116
+ ::HTTParty.get endpoint, timeout: timeout_in_seconds, query: query_params, headers: { 'Accept' => 'application/json' }
117
+ end
118
+ end
119
+
120
+ def parsed_response
121
+ response.parsed_response
122
+ end
123
+
124
+ def response_has_success_flag?
125
+ parsed_response && parsed_response.respond_to?(:key?) && parsed_response.key?('success')
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -14,6 +14,8 @@ module SSO
14
14
  include ::SSO::Benchmarking
15
15
 
16
16
  attr_reader :passport, :warden, :options
17
+ delegate :request, to: :warden
18
+ delegate :params, to: :request
17
19
 
18
20
  def self.activate(warden_options)
19
21
  ::Warden::Manager.after_fetch(warden_options) do |passport, warden, options|
@@ -39,51 +41,47 @@ module SSO
39
41
 
40
42
  private
41
43
 
42
- def verify
43
- debug { "Validating Passport #{passport.id.inspect} of logged in #{passport.user.class} in scope #{warden_scope.inspect}" }
44
- return server_unreachable! unless response.code == 200
45
- return server_response_not_parseable! unless parsed_response
46
- return server_response_missing_success_flag! unless response_has_success_flag?
47
- return server_response_unsuccessful! unless parsed_response['success'].to_s == 'true'
48
- verify!
49
44
 
50
- rescue JSON::ParserError
51
- error { 'SSO Server response is not valid JSON.' }
52
- error { response.inspect }
45
+ def verifier
46
+ ::SSO::Client::PassportVerifier.new passport_id: passport.id, passport_state: passport.state, passport_secret: passport.secret, user_ip: ip, user_agent: agent, device_id: device_id
53
47
  end
54
48
 
55
- def verify!
56
- code = parsed_response['code'].to_s == '' ? :unknown_response_code : parsed_response['code'].to_s.to_sym
57
-
58
- case code
59
- when :passport_changed then valid_passport_changed!
60
- when :passpord_unmodified then valid_passport_remains!
61
- when :passport_invalid then invalid_passport!
62
- else unexpected_server_response_status!
63
- end
49
+ def verification
50
+ @verification ||= verifier.call
64
51
  end
65
52
 
66
- def parsed_response
67
- response.parsed_response
53
+ def verification_code
54
+ verification.code
68
55
  end
69
56
 
70
- def response_has_success_flag?
71
- parsed_response && parsed_response.respond_to?(:key?) && parsed_response.key?('success')
57
+ def verify
58
+ debug { "Validating Passport #{passport.id.inspect} of logged in #{passport.user.class} in scope #{warden_scope.inspect}" }
59
+
60
+ case verification_code
61
+ when :server_unreachable then server_unreachable!
62
+ when :server_response_not_parseable then server_response_not_parseable!
63
+ when :server_response_missing_success_flag! then server_response_missing_success_flag!
64
+ when :server_response_unsuccessful! then server_response_unsuccessful!
65
+ when :passport_valid then passport_valid!
66
+ when :passport_valid_and_modified then passport_valid_and_modified!
67
+ when :passport_invalid then passport_invalid!
68
+ else unexpected_server_response_status!
69
+ end
72
70
  end
73
71
 
74
- def valid_passport_changed!
72
+ def passport_valid_and_modified!
75
73
  debug { 'Valid passport, but state changed' }
76
74
  passport.verified!
77
75
  # meter status: :valid, passport_id: user.passport_id
78
76
  end
79
77
 
80
- def valid_passport_remains!
78
+ def passport_valid!
81
79
  debug { 'Valid passport, no changes' }
82
- user.verified!
80
+ passport.verified!
83
81
  # meter status: :valid, passport_id: user.passport_id
84
82
  end
85
83
 
86
- def invalid_passport!
84
+ def passport_invalid!
87
85
  info { 'Your Passport is not valid any more.' }
88
86
  warden.logout warden_scope
89
87
  # meter status: :invalid, passport_id: user.passport_id
@@ -98,79 +96,32 @@ module SSO
98
96
  end
99
97
 
100
98
  def unexpected_server_response_status!
101
- error { 'SSO Server response did not include a known passport status code.' }
99
+ error { "SSO Server response did not include a known passport status code. #{verification_code.inspect}" }
102
100
  end
103
101
 
104
102
  def server_response_not_parseable!
105
103
  error { 'SSO Server response could not be parsed at all.' }
106
104
  end
107
105
 
108
- def endpoint
109
- URI.join(base_endpoint, path).to_s
110
- end
111
-
112
- def query_params
113
- params.merge auth_hash
114
- end
115
-
116
- # Needs to be configurable
117
- def path
118
- OmniAuth::Strategies::SSO.passports_path
119
- end
120
-
121
- def base_endpoint
122
- OmniAuth::Strategies::SSO.endpoint
123
- end
124
-
125
106
  def meter(*_)
126
107
  # This will be a hook for e.g. statistics, benchmarking, etc, measure everything
127
108
  end
128
109
 
129
110
  # TODO Use ActionDispatch remote IP or you might get the Load Balancer's IP instead :(
130
111
  def ip
131
- warden.request.ip
112
+ request.ip
132
113
  end
133
114
 
134
115
  def agent
135
- warden.request.user_agent
136
- end
137
-
138
- def warden_scope
139
- options[:scope]
140
- end
141
-
142
- def params
143
- { ip: ip, agent: agent, state: passport.state }
144
- end
145
-
146
- def token
147
- Signature::Token.new passport.id, passport.secret
148
- end
149
-
150
- def signature_request
151
- Signature::Request.new('GET', path, params)
116
+ request.user_agent
152
117
  end
153
118
 
154
- def auth_hash
155
- signature_request.sign token
119
+ def device_id
120
+ params['device_id']
156
121
  end
157
122
 
158
- def human_readable_timeout_in_ms
159
- (timeout_in_seconds * 1000).round
160
- end
161
-
162
- def timeout_in_seconds
163
- 0.1.seconds
164
- end
165
-
166
- def response
167
- @response ||= response!
168
- end
169
-
170
- def response!
171
- benchmark 'Passport authorization request' do
172
- ::HTTParty.get endpoint, timeout: timeout_in_seconds, query: query_params, headers: { 'Accept' => 'application/json' }
173
- end
123
+ def warden_scope
124
+ options[:scope]
174
125
  end
175
126
 
176
127
  end
@@ -0,0 +1,43 @@
1
+ module SSO
2
+ module Client
3
+ module Warden
4
+ module Strategies
5
+ # When the iPhone presents a Passport to Alpha, this is how Alpha verifies it with Bouncer.
6
+ class Passport < ::Warden::Strategies::Base
7
+ include ::SSO::Logging
8
+ include ::SSO::Benchmarking
9
+
10
+ def valid?
11
+ params['auth_version'].to_s != '' && params['state'] != ''
12
+ end
13
+
14
+ def authenticate!
15
+ debug { 'Authenticating from Passport...' }
16
+
17
+ authentication = passport_authentication
18
+
19
+ if authentication.success?
20
+ debug { 'Authentication from Passport successful.' }
21
+ debug { "Persisting trusted Passport #{authentication.object.inspect}" }
22
+ success! authentication.object
23
+ else
24
+ debug { 'Authentication from Passport failed.' }
25
+ debug { "Responding with #{authentication.object.inspect}" }
26
+ custom! authentication.object
27
+ end
28
+
29
+ rescue => exception
30
+ ::SSO.config.exception_handler.call exception
31
+ end
32
+
33
+ def passport_authentication
34
+ benchmark 'Passport proxy verification' do
35
+ ::SSO::Client::Authentications::Passport.new(request).authenticate
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/sso/client.rb CHANGED
@@ -4,5 +4,8 @@ require 'warden'
4
4
 
5
5
  require 'sso'
6
6
  require 'sso/client/passport'
7
+ require 'sso/client/passport_verifier'
7
8
  require 'sso/client/omniauth/strategies/sso'
8
9
  require 'sso/client/warden/hooks/after_fetch'
10
+ require 'sso/client/authentications/passport'
11
+ require 'sso/client/warden/strategies/passport'