sso 0.1.0.alpha1 → 0.1.0.alpha2
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.
- 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
@@ -0,0 +1,44 @@
|
|
1
|
+
module SSO
|
2
|
+
module Server
|
3
|
+
module Doorkeeper
|
4
|
+
class ResourceOwnerAuthenticator
|
5
|
+
include ::SSO::Logging
|
6
|
+
|
7
|
+
attr_reader :controller
|
8
|
+
|
9
|
+
def self.to_proc
|
10
|
+
proc { ::SSO::Server::Doorkeeper::ResourceOwnerAuthenticator.new(controller: self).call }
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(controller:)
|
14
|
+
@controller = controller
|
15
|
+
end
|
16
|
+
|
17
|
+
def call
|
18
|
+
debug { 'Detected "Authorization Code Grant" flow. Checking resource owner authentication...' }
|
19
|
+
|
20
|
+
unless warden
|
21
|
+
fail ::SSO::Server::Errors::WardenMissing, 'Please use the Warden middleware.'
|
22
|
+
end
|
23
|
+
|
24
|
+
if current_user
|
25
|
+
debug { "Yes, User with ID #{current_user.inspect} has a session." }
|
26
|
+
current_user
|
27
|
+
else
|
28
|
+
debug { 'No, no User is logged in right now. Initializing authentication procedure...' }
|
29
|
+
warden.authenticate! :password
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def warden
|
34
|
+
controller.request.env['warden']
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_user
|
38
|
+
warden.user
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module SSO
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace SSO
|
4
|
+
|
5
|
+
initializer 'sso.add_middleware' do |app|
|
6
|
+
app.middleware.insert_after ::Warden::Manager, ::SSO::Server::Middleware::PassportVerification
|
7
|
+
app.middleware.insert_after ::Warden::Manager, ::SSO::Server::Doorkeeper::GrantMarker
|
8
|
+
app.middleware.insert_after ::Warden::Manager, ::SSO::Server::Doorkeeper::AccessTokenMarker
|
9
|
+
end
|
10
|
+
|
11
|
+
config.generators do |g|
|
12
|
+
g.test_framework :rspec
|
13
|
+
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module SSO
|
2
|
+
module Server
|
3
|
+
module Middleware
|
4
|
+
class PassportVerification
|
5
|
+
include ::SSO::Logging
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
request = Rack::Request.new(env)
|
13
|
+
|
14
|
+
if request.get? && request.path == passports_path
|
15
|
+
debug { 'Detected incoming Passport verification request.' }
|
16
|
+
env['warden'].authenticate! :passport
|
17
|
+
else
|
18
|
+
debug { "I'm not interested in this request to #{request.path}" }
|
19
|
+
@app.call(env)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def passports_path
|
24
|
+
OmniAuth::Strategies::SSO.passports_path
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module SSO
|
4
|
+
module Server
|
5
|
+
# This could be MongoDB or whatever
|
6
|
+
class Passport < ActiveRecord::Base
|
7
|
+
include ::SSO::Logging
|
8
|
+
|
9
|
+
self.table_name = 'passports'
|
10
|
+
|
11
|
+
before_validation :ensure_secret
|
12
|
+
before_validation :ensure_group_id
|
13
|
+
before_validation :ensure_activity_at
|
14
|
+
|
15
|
+
before_save :update_location
|
16
|
+
|
17
|
+
belongs_to :application, class_name: 'Doorkeeper::Application'
|
18
|
+
|
19
|
+
validates :secret, :group_id, presence: true
|
20
|
+
validates :oauth_access_token_id, uniqueness: { scope: [:owner_id, :revoked_at], allow_blank: true }
|
21
|
+
validates :revoke_reason, allow_blank: true, format: { with: /\A[a-z_]+\z/ }
|
22
|
+
validates :application_id, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
23
|
+
|
24
|
+
attr_accessor :user
|
25
|
+
|
26
|
+
def export
|
27
|
+
debug { "Exporting Passport #{id} including the encapsulated user." }
|
28
|
+
{
|
29
|
+
id: id,
|
30
|
+
secret: secret,
|
31
|
+
user: user,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
['Passport', owner_id, ip, activity_at].join ', '
|
37
|
+
end
|
38
|
+
|
39
|
+
def state
|
40
|
+
if user
|
41
|
+
@state ||= state!
|
42
|
+
else
|
43
|
+
warn { 'Wait a minute, this Passport is not encapsulating a user!' }
|
44
|
+
'missing_user_for_state_calculation'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def state!
|
49
|
+
result = nil
|
50
|
+
time = Benchmark.realtime do
|
51
|
+
result = OpenSSL::HMAC.hexdigest user_state_digest, user_state_key, user_state_base
|
52
|
+
end
|
53
|
+
debug { "The user state digest is #{result.inspect}" }
|
54
|
+
debug { "Calculating the user state took #{(time * 1000).round(2)}ms" }
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
def user_state_digest
|
59
|
+
OpenSSL::Digest.new 'sha1'
|
60
|
+
end
|
61
|
+
|
62
|
+
def user_state_key
|
63
|
+
::SSO.config.user_state_key
|
64
|
+
end
|
65
|
+
|
66
|
+
def user_state_base
|
67
|
+
::SSO.config.user_state_base.call user
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def ensure_secret
|
73
|
+
self.secret ||= SecureRandom.uuid
|
74
|
+
end
|
75
|
+
|
76
|
+
def ensure_group_id
|
77
|
+
self.group_id ||= SecureRandom.uuid
|
78
|
+
end
|
79
|
+
|
80
|
+
def ensure_activity_at
|
81
|
+
self.activity_at ||= Time.now
|
82
|
+
end
|
83
|
+
|
84
|
+
def update_location
|
85
|
+
location_name = ::SSO::Server::Geolocations.human_readable_location_for_ip ip
|
86
|
+
debug { "Updating geolocation for #{ip} which is #{location_name}" }
|
87
|
+
self.location = location_name
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module SSO
|
2
|
+
module Server
|
3
|
+
# This is the one interaction point with persisting and querying Passports.
|
4
|
+
module Passports
|
5
|
+
extend ::SSO::Logging
|
6
|
+
|
7
|
+
def self.find(id)
|
8
|
+
record = backend.find_by_id(id)
|
9
|
+
|
10
|
+
if record
|
11
|
+
Operation.success(:record_found, object: record)
|
12
|
+
else
|
13
|
+
Operations.failure :record_not_found
|
14
|
+
end
|
15
|
+
|
16
|
+
rescue => exception
|
17
|
+
Operations.failure :backend_error, object: exception
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.generate(owner_id:, ip:, agent:)
|
21
|
+
debug { "Generating Passport for user ID #{owner_id.inspect} and IP #{ip.inspect} and Agent #{agent.inspect}" }
|
22
|
+
|
23
|
+
record = backend.create owner_id: owner_id, ip: ip, agent: agent, application_id: 0
|
24
|
+
|
25
|
+
if record.persisted?
|
26
|
+
debug { "Successfully generated passport with ID #{record.id}" }
|
27
|
+
Operations.success :generation_successful, object: record.id
|
28
|
+
else
|
29
|
+
Operations.failure :persistence_failed, object: record.errors.to_hash
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.register_authorization_grant(passport_id:, token:)
|
34
|
+
record = find_valid_passport(passport_id) { |failure| return failure }
|
35
|
+
access_grant = find_valid_access_grant(token) { |failure| return failure }
|
36
|
+
|
37
|
+
if record.update_attribute :oauth_access_grant_id, access_grant.id
|
38
|
+
debug { "Successfully augmented Passport #{record.id} with Authorization Grant ID #{access_grant.id} which is #{access_grant.token}" }
|
39
|
+
Operations.success :passport_augmented_with_access_token
|
40
|
+
else
|
41
|
+
Operations.failure :could_not_augment_passport_with_access_token
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.register_access_token_from_grant(grant_token:, access_token:)
|
46
|
+
access_grant = find_valid_access_grant(grant_token) { |failure| return failure }
|
47
|
+
access_token = find_valid_access_token(access_token) { |failure| return failure }
|
48
|
+
record = find_valid_passport_by_grant_id(access_grant.id) { |failure| return failure }
|
49
|
+
|
50
|
+
if record.update_attribute :oauth_access_token_id, access_token.id
|
51
|
+
debug { "Successfully augmented Passport #{record.id} with Access Token ID #{access_token.id} which is #{access_token.token}" }
|
52
|
+
Operations.success :passport_known_by_grant_augmented_with_access_token
|
53
|
+
else
|
54
|
+
Operations.failure :could_not_augment_passport_known_by_grant_with_access_token
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.register_access_token(passport_id:, access_token:)
|
59
|
+
access_token = find_valid_access_token(access_token) { |failure| return failure }
|
60
|
+
record = find_valid_passport(passport_id) { |failure| return failure }
|
61
|
+
|
62
|
+
if record.update_attribute :oauth_access_token_id, access_token.id
|
63
|
+
debug { "Successfully augmented Passport #{record.id} with Access Token ID #{access_token.id} which is #{access_token.token}" }
|
64
|
+
Operations.success :passport_augmented_with_access_token
|
65
|
+
else
|
66
|
+
Operations.failure :could_not_augment_passport_with_access_token
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.logout(passport_id:, provider_passport_id:)
|
71
|
+
if passport_id.present? || provider_passport_id.present?
|
72
|
+
debug { "Attemting to logout Passport groups of Passport IDs #{passport_id.inspect} and #{provider_passport_id.inspect}..." }
|
73
|
+
else
|
74
|
+
debug { "Should logout Passport groups now, but don't know which ones. Moving on..." }
|
75
|
+
return Operations.success :nothing_to_revoke_from
|
76
|
+
end
|
77
|
+
|
78
|
+
count = 0
|
79
|
+
count += logout_cluster(passport_id) if passport_id.present?
|
80
|
+
count += logout_cluster(provider_passport_id) if provider_passport_id.present?
|
81
|
+
|
82
|
+
Operations.success :passports_revoked, object: count
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def self.find_valid_passport(id)
|
88
|
+
record = backend.where(revoked_at: nil).find_by_id(id)
|
89
|
+
return record if record
|
90
|
+
|
91
|
+
debug { "Could not find valid passport with ID #{id.inspect}" }
|
92
|
+
debug { "All I have is #{backend.all.inspect}" }
|
93
|
+
yield Operations.failure :passport_not_found if block_given?
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.find_valid_passport_by_grant_id(id)
|
98
|
+
record = backend.where(revoked_at: nil).find_by_oauth_access_grant_id(id)
|
99
|
+
return record if record
|
100
|
+
|
101
|
+
warn { "Could not find valid passport by Authorization Grant ID #{id.inspect}" }
|
102
|
+
yield Operations.failure :passport_not_found
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.find_valid_access_grant(token)
|
107
|
+
record = ::Doorkeeper::AccessGrant.find_by_token token
|
108
|
+
|
109
|
+
if record && record.valid?
|
110
|
+
record
|
111
|
+
else
|
112
|
+
warn { "Could not find valid Authorization Grant Token #{token.inspect}" }
|
113
|
+
yield Operations.failure :access_grant_not_found
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.find_valid_access_token(token)
|
119
|
+
record = ::Doorkeeper::AccessToken.find_by_token token
|
120
|
+
|
121
|
+
if record && record.valid?
|
122
|
+
record
|
123
|
+
else
|
124
|
+
warn { "Could not find valid OAuth Access Token #{token.inspect}" }
|
125
|
+
yield Operations.failure :access_token_not_found
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.logout_cluster(passport_id)
|
131
|
+
unless passport_id.present? && record = backend.find_by_id(passport_id)
|
132
|
+
debug { "Cannot revoke Passport group of Passport ID #{passport_id.inspect} because it does not exist." }
|
133
|
+
return 0
|
134
|
+
end
|
135
|
+
|
136
|
+
debug { "Revoking Passport group #{record.group_id.inspect} of Passport ID #{passport_id.inspect}" }
|
137
|
+
affected_row_count = backend.where(group_id: record.group_id).update_all revoked_at: Time.now, revoke_reason: :logout
|
138
|
+
debug { "Successfully revoked #{affected_row_count.inspect} Passports." }
|
139
|
+
affected_row_count
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.backend
|
143
|
+
::SSO::Server::Passport
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module SSO
|
2
|
+
module Server
|
3
|
+
module Warden
|
4
|
+
module Hooks
|
5
|
+
class AfterAuthentication
|
6
|
+
include ::SSO::Logging
|
7
|
+
|
8
|
+
attr_reader :user, :warden, :options
|
9
|
+
|
10
|
+
def self.to_proc
|
11
|
+
proc do |user, warden, options|
|
12
|
+
begin
|
13
|
+
new(user: user, warden: warden, options: options).call
|
14
|
+
rescue => exception
|
15
|
+
::SSO.config.exception_handler.call exception
|
16
|
+
# The show must co on
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(user:, warden:, options:)
|
22
|
+
@user, @warden, @options = user, warden, options
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
debug { 'Starting hook because this is considered the first login of the current session...' }
|
27
|
+
request = warden.request
|
28
|
+
session = warden.env['rack.session']
|
29
|
+
|
30
|
+
debug { "Generating a passport for user #{user.id.inspect} for the session cookie at the SSO server..." }
|
31
|
+
attributes = { owner_id: user.id, ip: request.ip, agent: request.user_agent }
|
32
|
+
|
33
|
+
generation = SSO::Server::Passports.generate attributes
|
34
|
+
if generation.success?
|
35
|
+
debug { "Passport with ID #{generation.object.inspect} generated successfuly. Persisting it in session..." }
|
36
|
+
session[:passport_id] = generation.object
|
37
|
+
else
|
38
|
+
fail generation.code.inspect + generation.object.inspect
|
39
|
+
end
|
40
|
+
|
41
|
+
debug { 'Finished.' }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SSO
|
2
|
+
module Server
|
3
|
+
module Warden
|
4
|
+
module Hooks
|
5
|
+
class BeforeLogout
|
6
|
+
include ::SSO::Logging
|
7
|
+
|
8
|
+
attr_reader :user, :warden, :options
|
9
|
+
delegate :request, to: :warden
|
10
|
+
delegate :params, to: :request
|
11
|
+
delegate :session, to: :request
|
12
|
+
|
13
|
+
def self.to_proc
|
14
|
+
proc do |user, warden, options|
|
15
|
+
begin
|
16
|
+
new(user: user, warden: warden, options: options).call
|
17
|
+
rescue => exception
|
18
|
+
::SSO.config.exception_handler.call exception
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(user:, warden:, options:)
|
24
|
+
@user, @warden, @options = user, warden, options
|
25
|
+
end
|
26
|
+
|
27
|
+
def call
|
28
|
+
debug { 'Before warden destroys the passport in the cookie, it will revoke all connected Passports as well.' }
|
29
|
+
revoking = Passports.logout passport_id: params['passport_id'], provider_passport_id: session['passport_id']
|
30
|
+
|
31
|
+
error { 'Could not revoke the Passports.' } if revoking.failure?
|
32
|
+
debug { 'Finished.' }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|