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