sso 0.1.0.alpha1 → 0.1.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ # Setting up an SSO server
2
+
3
+ ### Assumptions
4
+
5
+ * You use doorkeeper as a Rails OAuth server.
6
+ * You want to provide single-sign-on for the end-users.
7
+ * All OAuth clients ("consumers") are developed by you. You have full control over them and can automatically trust them (i.e. you can set `skip_authorization { true }` in your doorkeeper.rb initializer). This makes sense, because why would you ask the end-user for permission to login to another subsystem of your SSO world? The whole idea with SSO is that your users don't need to notice switching between the OAuth clients.
8
+ * The SSO session is to be browser-wide and app-wide. If you click on "login" you will be logged in on every client web app in that browser. If you click on "logout" you will be logged out of every client web app in that browser.
9
+ * You use warden to login at the SSO server, it is, however, **not** okay to use scopes here. That's an assumption which makes this gem dramatically more simple and I didn't find a downside yet (Warden scopes are not really an ideal authorization solution anyway).
10
+
11
+ ### Setup
12
+
13
+ For now, see [these point of interests](https://github.com/halo/sso/search?q=POI) to see how exactly a rails app can be setup.
@@ -0,0 +1,170 @@
1
+ module SSO
2
+ module Server
3
+ module Authentications
4
+ class Passport
5
+ include ::SSO::Logging
6
+
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def authenticate
12
+ result = authenticate!
13
+
14
+ if result.success?
15
+ result
16
+ else
17
+ # TODO: Prevent Flooding here.
18
+ debug { "The Passport authentication failed: #{result.code}" }
19
+ Operations.failure :passport_authentication_failed, object: failure_rack_array
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :request
26
+
27
+ def authenticate!
28
+ check_arguments { |failure| return failure }
29
+
30
+ unless valid_signature?
31
+ warn { 'I found the corresponding passport, but the request was not properly signed with it.' }
32
+ return Operations.failure :invalid_signature, object: failure_rack_array
33
+ end
34
+
35
+ debug { 'The request was properly signed, I found the corresponding passport.' }
36
+ update_passport
37
+
38
+ if passport.state == state
39
+ Operations.success :signature_approved_no_changes, object: success_same_state_rack_array
40
+ else
41
+ debug { "The current user state #{passport.state.inspect} does not match the provided state #{state.inspect}" }
42
+ Operations.success :signature_approved_state_changed, object: success_new_state_rack_array
43
+ end
44
+ end
45
+
46
+ def check_arguments
47
+ yield Operations.failure :missing_verb if verb.blank?
48
+ yield Operations.failure :missing_passport_id if passport_id.blank?
49
+ yield Operations.failure :missing_state if state.blank?
50
+ yield Operations.failure :passport_not_found if passport.blank?
51
+ yield Operations.failure :passport_revoked if passport.invalid?
52
+ # yield Operations.failure :user_not_encapsulated if passport.user.blank?
53
+ Operations.success :arguments_are_valid
54
+ end
55
+
56
+ def success_new_state_rack_array
57
+ payload = { success: true, code: :passport_changed, passport: passport.export }
58
+ [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
59
+ end
60
+
61
+ def success_same_state_rack_array
62
+ payload = { success: true, code: :passpord_unmodified }
63
+ [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
64
+ end
65
+
66
+ # You might be wondering why we don't simply return a 401 or 404 status code.
67
+ # The reason is that the receiving end would have no way to determine whether that reply is due to a
68
+ # nginx configuration error or because the passport is actually invalid. We don't want to revoke
69
+ # all passports simply because a load balancer is pointing to the wrong Rails application or something.
70
+ #
71
+ def failure_rack_array
72
+ payload = { success: true, code: :passport_invalid }
73
+ [200, { 'Content-Type' => 'application/json' }, [payload.to_json]]
74
+ end
75
+
76
+ def passport
77
+ @passport ||= backend.find_by_id passport_id
78
+ end
79
+
80
+ def passport_id
81
+ signature_request.authenticate { |passport_id| return passport_id }
82
+ rescue Signature::AuthenticationError
83
+ nil
84
+ end
85
+
86
+ def valid_signature?
87
+ signature_request.authenticate { Signature::Token.new passport_id, passport.secret }
88
+ true
89
+ rescue Signature::AuthenticationError
90
+ debug { 'It looks like the API signature for the passport verification was incorrect.' }
91
+ false
92
+ end
93
+
94
+ def signature_request
95
+ @signature_request ||= Signature::Request.new verb, path, params
96
+ end
97
+
98
+ def update_passport
99
+ debug { "Will update activity of Passport #{passport.id} if neccesary..." }
100
+ if passport.ip.to_s == ip.to_s && passport.agent.to_s == user_agent.to_s
101
+ debug { "No changes in IP or User Agent so I won't perform an update now..." }
102
+ Operations.success :already_up_to_date
103
+ else
104
+ debug { "Yes, it is necessary, updating activity of Passport #{passport.id}" }
105
+ passport.update_attributes ip: ip.to_s, agent: user_agent, activity_at: Time.now
106
+ Operations.success :passport_metadata_updated
107
+ end
108
+ end
109
+
110
+ def application
111
+ passport.application
112
+ end
113
+
114
+ def app_scopes
115
+ application.scopes
116
+ end
117
+
118
+ def insider?
119
+ if app_scopes.empty?
120
+ warn { "Doorkeeper::Application #{application.name} with ID #{application.id} has no scope restrictions. Assuming 'outsider' for now." }
121
+ return false
122
+ end
123
+
124
+ app_scopes.has_scopes? [:insider]
125
+ end
126
+
127
+ def ip
128
+ if insider?
129
+ params['ip']
130
+ else
131
+ request_ip
132
+ end
133
+ end
134
+
135
+ def user_agent
136
+ if insider?
137
+ params['user_agent']
138
+ else
139
+ request.user_agent
140
+ end
141
+ end
142
+
143
+ def request_ip
144
+ request.env['action_dispatch.remote_ip'] || fail('Whoops, I thought you were using Rails, but action_dispatch.remote_ip is empty!')
145
+ end
146
+
147
+ def verb
148
+ request.request_method
149
+ end
150
+
151
+ def path
152
+ request.path
153
+ end
154
+
155
+ def params
156
+ request.params
157
+ end
158
+
159
+ def state
160
+ params['state']
161
+ end
162
+
163
+ def backend
164
+ ::SSO::Server::Passport
165
+ end
166
+
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,80 @@
1
+ require 'logger'
2
+
3
+ module SSO
4
+ class Configuration
5
+
6
+ def human_readable_location_for_ip
7
+ @human_readable_location_for_ip || default_human_readable_location_for_ip
8
+ end
9
+ attr_writer :human_readable_location_for_ip
10
+
11
+ def exception_handler
12
+ @exception_handler || default_exception_handler
13
+ end
14
+ attr_writer :exception_handler
15
+
16
+ def user_state_base
17
+ @user_state_base || fail('You need to configure user_state_base, see SSO::Configuration for more info.')
18
+ end
19
+ attr_writer :user_state_base
20
+
21
+ def find_user_for_passport
22
+ @find_user_for_passport || fail('You need to configure find_user_for_passport, see SSO::Configuration for more info.')
23
+ end
24
+ attr_writer :find_user_for_passport
25
+
26
+ def user_state_key
27
+ @user_state_key || fail('You need to configure a secret user_state_key, see SSO::Configuration for more info.')
28
+ end
29
+ attr_writer :user_state_key
30
+
31
+ def logger
32
+ @logger ||= default_logger
33
+ end
34
+ attr_writer :logger
35
+
36
+ def environment
37
+ @environment ||= default_environment
38
+ end
39
+ attr_writer :environment
40
+
41
+ private
42
+
43
+ def default_logger
44
+ return ::Rails.logger if defined?(::Rails)
45
+ instance = ::Logger.new STDOUT
46
+ instance.level = default_log_level
47
+ instance
48
+ end
49
+
50
+ def default_log_level
51
+ case environment
52
+ when 'production' then ::Logger::WARN
53
+ when 'test' then ::Logger::UNKNOWN
54
+ else ::Logger::DEBUG
55
+ end
56
+ end
57
+
58
+ def default_environment
59
+ return ::Rails.env if defined?(::Rails)
60
+ return ENV['RACK_ENV'].to_s if ENV['RACK_ENV'].to_s != ''
61
+ 'unknown'
62
+ end
63
+
64
+ def default_exception_handler
65
+ proc do
66
+ return unless ::SSO.config.logger
67
+ ::SSO.config.logger.error(self.class) do
68
+ "An internal error occured #{exception.class.name} #{exception.message} #{exception.backtrace[0..5].join(' ') if exception.backtrace}"
69
+ end
70
+ end
71
+ end
72
+
73
+ def default_human_readable_location_for_ip
74
+ proc do
75
+ 'Unknown'
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,15 @@
1
+ module SSO
2
+
3
+ # Public: Lazy-loads and returns the the configuration instance.
4
+ #
5
+ def self.config
6
+ @config ||= ::SSO::Configuration.new
7
+ end
8
+
9
+ # Public: Yields the configuration instance.
10
+ #
11
+ def self.configure
12
+ yield config
13
+ end
14
+
15
+ end
@@ -0,0 +1,111 @@
1
+ module SSO
2
+ module Server
3
+ module Doorkeeper
4
+ class AccessTokenMarker
5
+ include ::SSO::Logging
6
+
7
+ attr_reader :request, :response
8
+ delegate :params, to: :request
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ @env = env
16
+ @request = ::ActionDispatch::Request.new @env
17
+ @response = @app.call @env
18
+
19
+ return response unless applicable?
20
+
21
+ if authorization_grant_flow?
22
+ handle_authorization_grant_flow
23
+ elsif password_flow?
24
+ handle_password_flow
25
+ else
26
+ fail NotImplementedError
27
+ end
28
+
29
+ response
30
+ end
31
+
32
+ def applicable?
33
+ request.method == 'POST' &&
34
+ (authorization_grant_flow? || password_flow?) &&
35
+ response_code == 200 &&
36
+ response_body &&
37
+ outgoing_access_token
38
+ end
39
+
40
+ def handle_authorization_grant_flow
41
+ # We cannot rely on session[:passport_id] here because the end-user might have cookies disabled.
42
+ # The only thing we can rely on to identify the user/Passport is the incoming grant token.
43
+ debug { %(Detected outgoing "Access Token" #{outgoing_access_token.inspect} of the "Authorization Code Grant" flow) }
44
+ debug { %(This Access Token belongs to "Authorization Grant Token" #{grant_token.inspect}. Augmenting related Passport with it...) }
45
+ registration = ::SSO::Server::Passports.register_access_token_from_grant grant_token: grant_token, access_token: outgoing_access_token
46
+
47
+ return if registration.success?
48
+ warn { 'The passport could not be augmented. Destroying warden session.' }
49
+ warden.logout
50
+ end
51
+
52
+ def handle_password_flow
53
+ local_passport_id = session[:passport_id] # <- We know this is always set because it was set in this very response
54
+ debug { %(Detected outgoing "Access Token" #{outgoing_access_token.inspect} of the "Resource Owner Password Credentials Grant" flow.) }
55
+ debug { %(Augmenting local Passport #{local_passport_id.inspect} with this outgoing Access Token...) }
56
+ generation = ::SSO::Server::Passports.register_access_token passport_id: local_passport_id, access_token: outgoing_access_token
57
+
58
+ return if generation.success?
59
+ warn { 'The passport could not be generated. Destroying warden session.' }
60
+ warden.logout
61
+ end
62
+
63
+ def response_body
64
+ response.last.first.presence
65
+ end
66
+
67
+ def response_code
68
+ response.first
69
+ end
70
+
71
+ def parsed_response_body
72
+ return unless response_body
73
+ ::JSON.parse response_body
74
+ rescue JSON::ParserError => exception
75
+ Trouble.notify exception
76
+ nil
77
+ end
78
+
79
+ def outgoing_access_token
80
+ return unless parsed_response_body
81
+ parsed_response_body['access_token']
82
+ end
83
+
84
+ def warden
85
+ request.env['warden']
86
+ end
87
+
88
+ def authorization_grant_flow?
89
+ grant_token.present?
90
+ end
91
+
92
+ def password_flow?
93
+ grant_type == 'password'
94
+ end
95
+
96
+ def grant_token
97
+ params['code']
98
+ end
99
+
100
+ def grant_type
101
+ params['grant_type']
102
+ end
103
+
104
+ def session
105
+ @env['rack.session']
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,85 @@
1
+ module SSO
2
+ module Server
3
+ module Doorkeeper
4
+ class GrantMarker
5
+ include ::SSO::Logging
6
+
7
+ attr_reader :response
8
+ delegate :session, to: :request
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ @env = env
16
+ @response = @app.call @env
17
+
18
+ return response unless outgoing_grant_token
19
+
20
+ if passport_id
21
+ debug { %(Detected outgoing "Authorization Grant Token" #{outgoing_grant_token.inspect} of the "Authorization Code Grant" flow.) }
22
+ debug { %(Augmenting Passport #{passport_id.inspect} with this outgoing Grant Token...) }
23
+ registration = ::SSO::Server::Passports.register_authorization_grant passport_id: passport_id, token: outgoing_grant_token
24
+
25
+ if registration.failure?
26
+ warn { 'The passport could not be augmented. Destroying warden session.' }
27
+ warden.logout
28
+ end
29
+ end
30
+
31
+ response
32
+ end
33
+
34
+ def request
35
+ ::ActionDispatch::Request.new @env
36
+ end
37
+
38
+ def code
39
+ response.first
40
+ end
41
+
42
+ def warden
43
+ request.env['warden']
44
+ end
45
+
46
+ def passport_id
47
+ session['passport_id']
48
+ end
49
+
50
+ def location_header
51
+ unless code == 302
52
+ # debug { "Uninteresting response, because it is not a redirect" }
53
+ return
54
+ end
55
+
56
+ response.second['Location']
57
+ end
58
+
59
+ def redirect_uri
60
+ unless location_header
61
+ # debug { "Uninteresting response, because there is no Location header" }
62
+ return
63
+ end
64
+
65
+ ::URI.parse location_header
66
+ end
67
+
68
+ def redirect_uri_params
69
+ return unless redirect_uri
70
+ ::Rack::Utils.parse_query redirect_uri.query
71
+ end
72
+
73
+ def outgoing_grant_token
74
+ unless redirect_uri_params && redirect_uri_params['code']
75
+ # debug { "Uninteresting response, because there is no code parameter sent" }
76
+ return
77
+ end
78
+
79
+ redirect_uri_params['code']
80
+ end
81
+
82
+ end
83
+ end
84
+ end
85
+ end