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
@@ -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
|