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 +4 -4
- data/lib/sso/benchmarking.rb +14 -0
- data/lib/sso/client.rb +7 -0
- data/lib/sso/client/README.md +92 -0
- data/lib/sso/client/omniauth/strategies/sso.rb +58 -0
- data/lib/sso/client/passport.rb +25 -0
- data/lib/sso/client/warden/hooks/after_fetch.rb +179 -0
- data/lib/sso/logging.rb +36 -0
- data/lib/sso/server.rb +26 -0
- data/lib/sso/server/README.md +13 -0
- data/lib/sso/server/authentications/passport.rb +170 -0
- data/lib/sso/server/configuration.rb +80 -0
- data/lib/sso/server/configure.rb +15 -0
- data/lib/sso/server/doorkeeper/access_token_marker.rb +111 -0
- data/lib/sso/server/doorkeeper/grant_marker.rb +85 -0
- data/lib/sso/server/doorkeeper/resource_owner_authenticator.rb +44 -0
- data/lib/sso/server/engine.rb +16 -0
- data/lib/sso/server/errors.rb +11 -0
- data/lib/sso/server/geolocations.rb +10 -0
- data/lib/sso/server/middleware/passport_verification.rb +30 -0
- data/lib/sso/server/passport.rb +92 -0
- data/lib/sso/server/passports.rb +148 -0
- data/lib/sso/server/warden/hooks/after_authentication.rb +47 -0
- data/lib/sso/server/warden/hooks/before_logout.rb +38 -0
- data/lib/sso/server/warden/strategies/passport.rb +39 -0
- metadata +25 -2
- data/lib/sso.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcd7cedda8e78677b8c5ea0eff1a558659daa1c1
|
4
|
+
data.tar.gz: 79fe0ceb4869c29d8a1cf886018816e76ca0d616
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 755b13e0c16bc4824e1c2bf33a0adc5a4a5078367ad0efccca69d7efc9295c44909722df80b73ca7d65bbc059f744bcf57715aa731c1d9e49a6df845e6c7957a
|
7
|
+
data.tar.gz: 5237534c2a282e8fb04cffe903774526c16f347f40b9dff1e3a8f73bd1eefd09fdcb74c922a148516b4bc8a9d9cbe946c037dafd6eea0143e221b2e9ff40a260
|
data/lib/sso/client.rb
ADDED
@@ -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
|
data/lib/sso/logging.rb
ADDED
@@ -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'
|