sso 0.1.0.alpha5 → 0.1.0.alpha6

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