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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 502a677d0dbb1abcd9a531c018bd095e6fa2fe5e
4
- data.tar.gz: f7e1c5290d4ea845680b4c82272e391958a36889
3
+ metadata.gz: dcd7cedda8e78677b8c5ea0eff1a558659daa1c1
4
+ data.tar.gz: 79fe0ceb4869c29d8a1cf886018816e76ca0d616
5
5
  SHA512:
6
- metadata.gz: 3c9f9227968a527cd8fb844ee69bacfb92041b10fc47f1937b9a578d79b1b286b83ccdc9a57183173a5637d3e093b81e4ce59a0edd5f61cf60d880264973d719
7
- data.tar.gz: 8909701cee344d1cfb73d1e2549816a74cd580ddbd108d87ebf4c9495d9aafc93edd7eecfd66ba5e18919c2a66018636fa02543392fc23ac5c3a6e75521095e0
6
+ metadata.gz: 755b13e0c16bc4824e1c2bf33a0adc5a4a5078367ad0efccca69d7efc9295c44909722df80b73ca7d65bbc059f744bcf57715aa731c1d9e49a6df845e6c7957a
7
+ data.tar.gz: 5237534c2a282e8fb04cffe903774526c16f347f40b9dff1e3a8f73bd1eefd09fdcb74c922a148516b4bc8a9d9cbe946c037dafd6eea0143e221b2e9ff40a260
@@ -0,0 +1,14 @@
1
+ module SSO
2
+ module Benchmarking
3
+ include ::SSO::Logging
4
+
5
+ def benchmark(name, &block)
6
+ result = nil
7
+ seconds = Benchmark.realtime do
8
+ result = block.call
9
+ end
10
+ info { "#{name} took #{(seconds * 1000).round}ms" }
11
+ result
12
+ end
13
+ end
14
+ end
data/lib/sso/client.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'httparty'
2
+ require 'signature'
3
+ require 'warden'
4
+
5
+ require 'sso/client/passport'
6
+ require 'sso/client/omniauth/strategies/sso'
7
+ require 'sso/client/warden/hooks/after_fetch'
@@ -0,0 +1,92 @@
1
+ # Setting up an SSO client
2
+
3
+ ## Assumptions
4
+
5
+ * You have a rack app (e.g. Rails)
6
+ * You are not going to have a database table with users in your OAuth Clients. That information is only available in the [Rails OAuth server](https://github.com/halo/sso/blob/master/lib/sso/server/README.md).
7
+ * To avoid implementing your own solutions, you should use `warden.user` to persist your user in the session in the OAuth rails clients. It is no problem to use warden scopes here in the client.
8
+
9
+ ## How it works
10
+
11
+ #### Trusted OAuth clients
12
+
13
+ * A trusted OAuth client, let's call it `Alpha`, uses the `Authorization Code Grant` to obtain an OAuth `access_token` with the OAuth permission scope `insider`.
14
+ * The browser of the end user actually "visits" `Bouncer` for the login. That's where the user is persisted into the session. And that's where a passport is created for the user. So basically, through the OAuth server cookie, the SSO session is tied together. As long as it is there, you are logged in (in that browser e.g.).
15
+
16
+ #### Unstrusted OAuth clients
17
+
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
+ * You exchange the `access_token` for a passport token. That is effectively your API token used to communicate with the OAuth Rails clients.
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.
22
+
23
+ #### Also good to know
24
+
25
+ * If the passport verification request times out (like 100ms), the authentication/authorization of the previous request is assumed to still be valid.
26
+
27
+ ## Setup (trusted client)
28
+
29
+ #### Add the gem to your Gemfile
30
+
31
+ ```ruby
32
+ # Gemfile
33
+ gem 'sso', require: 'sso/client'
34
+ ```
35
+
36
+ #### Make sure you activated the Warden middleware provided by the `warden` gem
37
+
38
+ See [the Warden wiki](https://github.com/hassox/warden/wiki/Setup)
39
+
40
+ #### Set the URL to the SSO Server
41
+
42
+ See [also this piece of code](https://github.com/halo/sso/blob/master/lib/sso/client/omniauth/strategies/sso.rb#L7-L17).
43
+
44
+ ```bash
45
+ OMNIAUTH_SSO_ENDPOINT="http://server.example.com"
46
+ ```
47
+
48
+ #### Setup your login logic
49
+
50
+ Rails Example:
51
+
52
+ ```ruby
53
+ class SessionsController < ApplicationController
54
+ delegate :logout, to: :warden
55
+
56
+ def new
57
+ redirect_to '/auth/sso'
58
+ end
59
+
60
+ def create
61
+ warden.set_user auth_hash.info.to_hash
62
+ redirect_to root_path
63
+ end
64
+
65
+ def destroy
66
+ warden.logout
67
+ end
68
+
69
+ private
70
+
71
+ def auth_hash
72
+ request.env['omniauth.auth]
73
+ end
74
+
75
+ def warden
76
+ request.env['warden']
77
+ end
78
+
79
+ end
80
+ ````
81
+
82
+ #### Activate the middleware
83
+
84
+ This is done by making use of [Warden callbacks](https://github.com/hassox/warden/wiki/Callbacks). See [this piece of code](https://github.com/halo/sso/blob/master/lib/sso/client/warden/hooks/after_fetch.rb#L18-L22).
85
+
86
+ ```ruby
87
+ # e.g. config/initializers/warden.rb
88
+ # The options are passed on to `::Warden::Manager.after_fetch`
89
+ SSO::Client::Warden::Hooks::AfterFetch.activate scope: :vip
90
+ ``
91
+ #### Profit
92
+
@@ -0,0 +1,58 @@
1
+ require 'omniauth-oauth2'
2
+
3
+ module OmniAuth
4
+ module Strategies
5
+ class SSO < OmniAuth::Strategies::OAuth2
6
+
7
+ def self.endpoint
8
+ if ENV['OMNIAUTH_SSO_ENDPOINT'].to_s != ''
9
+ ENV['OMNIAUTH_SSO_ENDPOINT'].to_s
10
+ elsif development_environment?
11
+ ENV['OMNIAUTH_SSO_ENDPOINT'] || 'http://sso.dev:8080'
12
+ elsif test_environment?
13
+ 'https://sso.example.com'
14
+ else
15
+ fail 'You must set OMNIAUTH_SSO_ENDPOINT to point to the SSO OAuth server'
16
+ end
17
+ end
18
+
19
+ def self.development_environment?
20
+ defined?(Rails) && Rails.env.development?
21
+ end
22
+
23
+ def self.test_environment?
24
+ defined?(Rails) && Rails.env.test? || ENV['RACK_ENV'] == 'test'
25
+ end
26
+
27
+ def self.passports_path
28
+ if ENV['OMNIAUTH_SSO_PASSPORTS_PATH'].to_s != ''
29
+ ENV['OMNIAUTH_SSO_PASSPORTS_PATH'].to_s
30
+ else
31
+ # We know this namespace is not occupied because /oauth is owned by Doorkeeper
32
+ '/oauth/sso/v1/passports'
33
+ end
34
+ end
35
+
36
+ option :name, :sso
37
+ option :client_options, site: endpoint, authorize_path: '/oauth/authorize'
38
+
39
+ uid { raw_info['id'] if raw_info }
40
+
41
+ info do
42
+ {
43
+ # Passport
44
+ id: uid,
45
+ state: raw_info['state'],
46
+ secret: raw_info['secret'],
47
+ user: raw_info['user'],
48
+ }
49
+ end
50
+
51
+ def raw_info
52
+ params = { ip: request.ip, agent: request.user_agent }
53
+ @raw_info ||= access_token.post(self.class.passports_path, params: params).parsed
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ module SSO
2
+ module Client
3
+ class Passport
4
+
5
+ attr_reader :id, :secret, :state, :user
6
+
7
+ def initialize(id:, secret:, state:, user:)
8
+ @id, @secret, @state, @user = id, secret, state, user
9
+ end
10
+
11
+ def verified!
12
+ @verified = true
13
+ end
14
+
15
+ def verified?
16
+ @verified == true
17
+ end
18
+
19
+ def unverified?
20
+ !verified?
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,179 @@
1
+ module SSO
2
+ module Client
3
+ module Warden
4
+ module Hooks
5
+ # This is a helpful `Warden::Manager.after_fetch` hook for Alpha and Beta.
6
+ # Whenever Carol is fetched out of the session, we also verify her passport.
7
+ #
8
+ # Usage:
9
+ #
10
+ # SSO::Client::Warden::Hooks::AfterFetch.activate scope: :vip
11
+ #
12
+ class AfterFetch
13
+ include ::SSO::Logging
14
+ include ::SSO::Benchmarking
15
+
16
+ attr_reader :passport, :warden, :options
17
+
18
+ def self.activate(warden_options)
19
+ ::Warden::Manager.after_fetch(warden_options) do |passport, warden, options|
20
+ ::SSO::Client::Warden::Hooks::AfterFetch.new(passport: passport, warden: warden, options: options).call
21
+ end
22
+ end
23
+
24
+ def initialize(passport:, warden:, options:)
25
+ @passport, @warden, @options = passport, warden, options
26
+ end
27
+
28
+ def call
29
+ return unless passport.is_a?(::SSO::Client::Passport)
30
+ verify
31
+
32
+ rescue Timeout::Error
33
+ error { 'SSO Server timed out. Continuing with last known authentication/authorization...' }
34
+ # meter status: :timeout, scope: scope, passport_id: user.passport_id, timeout_ms: human_readable_timeout_in_ms
35
+
36
+ rescue => exception
37
+ ::SSO.config.exception_handler.call exception
38
+ end
39
+
40
+ private
41
+
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
+
50
+ rescue JSON::ParserError
51
+ error { 'SSO Server response is not valid JSON.' }
52
+ error { response.inspect }
53
+ end
54
+
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
64
+ end
65
+
66
+ def parsed_response
67
+ response.parsed_response
68
+ end
69
+
70
+ def response_has_success_flag?
71
+ parsed_response && parsed_response.respond_to?(:key?) && parsed_response.key?('success')
72
+ end
73
+
74
+ def valid_passport_changed!
75
+ debug { 'Valid passport, but state changed' }
76
+ passport.verified!
77
+ # meter status: :valid, passport_id: user.passport_id
78
+ end
79
+
80
+ def valid_passport_remains!
81
+ debug { 'Valid passport, no changes' }
82
+ user.verified!
83
+ # meter status: :valid, passport_id: user.passport_id
84
+ end
85
+
86
+ def invalid_passport!
87
+ info { 'Your Passport is not valid any more.' }
88
+ warden.logout warden_scope
89
+ # meter status: :invalid, passport_id: user.passport_id
90
+ end
91
+
92
+ def server_unreachable!
93
+ error { "SSO Server responded with an unexpected HTTP status code (#{response.code.inspect} instead of 200)." }
94
+ end
95
+
96
+ def server_response_missing_success_flag!
97
+ error { 'SSO Server response did not include the expected success flag.' }
98
+ end
99
+
100
+ def unexpected_server_response_status!
101
+ error { 'SSO Server response did not include a known passport status code.' }
102
+ end
103
+
104
+ def server_response_not_parseable!
105
+ error { 'SSO Server response could not be parsed at all.' }
106
+ end
107
+
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
+ def meter(*_)
126
+ # This will be a hook for e.g. statistics, benchmarking, etc, measure everything
127
+ end
128
+
129
+ def ip
130
+ warden.request.ip
131
+ end
132
+
133
+ def agent
134
+ warden.request.user_agent
135
+ end
136
+
137
+ def warden_scope
138
+ options[:scope]
139
+ end
140
+
141
+ def params
142
+ { ip: ip, agent: agent, state: passport.state }
143
+ end
144
+
145
+ def token
146
+ Signature::Token.new passport.id, passport.secret
147
+ end
148
+
149
+ def signature_request
150
+ Signature::Request.new('GET', path, params)
151
+ end
152
+
153
+ def auth_hash
154
+ signature_request.sign token
155
+ end
156
+
157
+ def human_readable_timeout_in_ms
158
+ (timeout_in_seconds * 1000).round
159
+ end
160
+
161
+ def timeout_in_seconds
162
+ 0.1.seconds
163
+ end
164
+
165
+ def response
166
+ @response ||= response!
167
+ end
168
+
169
+ def response!
170
+ benchmark 'Passport authorization request' do
171
+ ::HTTParty.get endpoint, timeout: timeout_in_seconds, query: query_params, headers: { 'Accept' => 'application/json' }
172
+ end
173
+ end
174
+
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,36 @@
1
+ module SSO
2
+ # One thing tha bugs me is when I cannot see which part of the code caused a log message.
3
+ # This mixin will include the current class name as Logger `progname` so you can show that it in your logfiles.
4
+ #
5
+ module Logging
6
+
7
+ def debug(&block)
8
+ logger && logger.debug(progname, &block)
9
+ end
10
+
11
+ def info(&block)
12
+ logger && logger.info(progname, &block)
13
+ end
14
+
15
+ def warn(&block)
16
+ logger && logger.warn(progname, &block)
17
+ end
18
+
19
+ def error(&block)
20
+ logger && logger.error(progname, &block)
21
+ end
22
+
23
+ def fatal(&block)
24
+ logger && logger.fatal(progname, &block)
25
+ end
26
+
27
+ def progname
28
+ self.class.name
29
+ end
30
+
31
+ def logger
32
+ ::SSO.config.logger
33
+ end
34
+
35
+ end
36
+ end
data/lib/sso/server.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'rails' # <- Doorkeeper secretly depends on this
2
+ require 'doorkeeper'
3
+ require 'operation'
4
+ require 'httparty'
5
+ require 'omniauth'
6
+ require 'signature'
7
+ require 'warden'
8
+
9
+ require 'sso/server/errors'
10
+ require 'sso/server/passport'
11
+ require 'sso/server/passports'
12
+ require 'sso/server/geolocations'
13
+ require 'sso/server/configuration'
14
+ require 'sso/server/configure'
15
+ require 'sso/server/engine'
16
+
17
+ require 'sso/server/authentications/passport'
18
+ require 'sso/server/middleware/passport_verification'
19
+
20
+ require 'sso/server/warden/hooks/after_authentication'
21
+ require 'sso/server/warden/hooks/before_logout'
22
+ require 'sso/server/warden/strategies/passport'
23
+
24
+ require 'sso/server/doorkeeper/resource_owner_authenticator'
25
+ require 'sso/server/doorkeeper/grant_marker'
26
+ require 'sso/server/doorkeeper/access_token_marker'