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,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,11 @@
1
+ module SSO
2
+ module Server
3
+ module Errors
4
+
5
+ Error = Class.new(StandardError)
6
+
7
+ WardenMissing = Class.new(Error)
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module SSO
2
+ module Server
3
+ module Geolocations
4
+ def self.human_readable_location_for_ip(_)
5
+ # Implement your favorite GeoIP lookup here
6
+ 'New York'
7
+ end
8
+ end
9
+ end
10
+ 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