rom-auth 0.0.4

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,92 @@
1
+ module ROM::Auth
2
+ module Plugins
3
+ class AuthenticationEventsPlugin < Plugin
4
+
5
+ class Configuration
6
+ include Virtus.model
7
+
8
+ attribute :table_name, Symbol, default: :authentication_events
9
+ end
10
+
11
+ def install
12
+ system.extend(CallbackOverrides)
13
+
14
+ config = configuration
15
+
16
+ @mapper = Class.new(ROM::Mapper) do
17
+ relation(config.table_name)
18
+ model(AuthenticationEvent)
19
+ register_as :rom_auth_event
20
+ end
21
+
22
+ @relation = Class.new(ROM::Relation[:sql]) do
23
+ dataset(config.table_name)
24
+ end
25
+
26
+ @command = Class.new(ROM::Commands::Create[:sql]) do
27
+ register_as :create
28
+ relation(config.table_name)
29
+ result :one
30
+ end
31
+ end
32
+
33
+ def migrate(setup)
34
+ AuthenticationEventsMigration.new(system, setup, configuration).run
35
+ end
36
+
37
+ class AuthenticationEvent
38
+ include Virtus.value_object(coerce: false)
39
+
40
+ values do
41
+ attribute :user_id, Integer # FIXME should be dynamic and could be account_id
42
+ attribute :success, Boolean
43
+ attribute :started_at, DateTime
44
+ attribute :ended_at, DateTime
45
+ attribute :identifier, String
46
+ attribute :type, String
47
+ attribute :authenticated, Boolean
48
+ attribute :data, String
49
+ end
50
+ end
51
+
52
+ class AuthenticationEventsMigration < Migration
53
+ def run
54
+ auth_config = system.configuration
55
+ config = self.config
56
+ user_fk_name = auth_config.user_fk_name
57
+
58
+ database.create_table(config.table_name) do
59
+ primary_key :id
60
+ foreign_key(user_fk_name, auth_config.users_table_name.to_sym)
61
+ DateTime :started_at
62
+ DateTime :ended_at
63
+ String :identifier
64
+ String :type
65
+ Boolean :authenticated
66
+ Boolean :success
67
+ String :data
68
+ # TODO login_ip, failure reason
69
+ end
70
+ end
71
+ end
72
+
73
+ module CallbackOverrides
74
+ def on_authentication_completed(data)
75
+ super
76
+
77
+ # ROM.env.command(plugins[AuthenticationEventsPlugin].configuration.table_name).try{
78
+ # create(data)
79
+ # }
80
+ ROM.env.command(plugins[AuthenticationEventsPlugin].configuration.table_name).create.call(data)
81
+
82
+ # TODO
83
+ # - log origin/details & warn user
84
+ # - compute failed logins per minute and alert if above threshold
85
+ # - throttling
86
+ #
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,114 @@
1
+ module ROM::Auth
2
+ module Plugins
3
+ class LockdownPlugin < Plugin
4
+
5
+ class Configuration
6
+ include Virtus.model
7
+
8
+ attribute :table_name, Symbol, default: nil
9
+ attribute :lock_strategy, Object
10
+ attribute :unlock_strategy, Object
11
+ end
12
+
13
+ def initialize(*args)
14
+ super
15
+
16
+ # FIXME move to configuration?
17
+ configuration.table_name ||= system.configuration.users_table_name
18
+ end
19
+
20
+ def install
21
+ system.extend(CallbackOverrides)
22
+
23
+ config = configuration
24
+
25
+ @relation = Class.new(ROM::Relation[:sql]) do
26
+ register_as :rom_auth_lockdowns
27
+ dataset(config.table_name)
28
+
29
+ def by_user_id(user_id)
30
+ where(id: user_id)
31
+ end
32
+ end
33
+
34
+ @mapper = Class.new(ROM::Mapper) do
35
+ relation(:rom_auth_lockdowns)
36
+ model(Lockdown)
37
+ register_as :rom_auth_lockdown
38
+ end
39
+
40
+ @command = Class.new(ROM::Commands::Update[:sql]) do
41
+ register_as :update
42
+ relation(:rom_auth_lockdowns)
43
+ result :one
44
+ end
45
+ end
46
+
47
+ def migrate(setup)
48
+ LockdownMigration.new(system, setup, configuration).run
49
+ end
50
+
51
+ def lock_strategy
52
+ configuration.lock_strategy
53
+ end
54
+
55
+ def unlock_strategy
56
+ configuration.unlock_strategy
57
+ end
58
+
59
+ def is_locked?(user_id)
60
+ #user.locked_at.present?
61
+ ROM.env.relation(:rom_auth_lockdowns).by_user_id(user_id).as(:rom_auth_lockdown).first.try(:locked_at).present?
62
+ end
63
+
64
+ def lock(user_id, reason)
65
+ raise ArgumentError unless user_id.is_a?(Integer)
66
+ ROM.env.command(:rom_auth_lockdowns).update.where(id: user_id).call(locked_at: Time.now, lock_reason: reason)
67
+ end
68
+
69
+ def unlock(user_id)
70
+ ROM.env.command(:rom_auth_lockdowns).update.where(id: user_id).call(locked_at: nil, lock_reason: nil)
71
+ end
72
+
73
+ class LockdownMigration < Migration
74
+ def run
75
+ config = self.config
76
+ auth_config = system.configuration
77
+
78
+ database.alter_table(config.table_name) do
79
+ add_column :locked_at, DateTime, default: nil, null: true
80
+ add_column :lock_reason, String, null: true
81
+ end
82
+ end
83
+ end
84
+
85
+ class Lockdown
86
+ include Virtus.value_object(coerce: false)
87
+
88
+ values do
89
+ attribute :id, Integer
90
+ attribute :locked_at, DateTime
91
+ attribute :lock_reason, String
92
+ end
93
+ end
94
+
95
+ module CallbackOverrides
96
+ def authentication_authorized?(user, credentials)
97
+ plugin = plugins[LockdownPlugin]
98
+
99
+ plugin.unlock_strategy.call(plugin, user, credentials) if plugin.unlock_strategy
100
+ super && !plugin.is_locked?(user[:id])
101
+ end
102
+
103
+ def on_authentication_completed(data)
104
+ super
105
+
106
+ plugin = plugins[LockdownPlugin]
107
+
108
+ plugin.lock_strategy.call(plugin, data[:user_id], data) if plugin.lock_strategy
109
+ end
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,22 @@
1
+ module ROM::Auth
2
+ module Plugins
3
+ class Plugin
4
+
5
+ attr_reader :system, :configuration
6
+
7
+ def initialize(system, config)
8
+ raise ArgumentError unless system.kind_of?(System)
9
+ @system = system
10
+ @configuration = config
11
+ end
12
+
13
+ def migrate(*args)
14
+ end
15
+
16
+ def install(*args)
17
+ raise NotImplementedError
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ module ROM::Auth
2
+ module Support
3
+ module ShorthandSymbol
4
+
5
+ def self.strip(suffix)
6
+ mod = Module.new
7
+
8
+ mod.instance_eval <<-RUBY
9
+ def included(cls)
10
+ def cls.shorthand_symbol
11
+ #name.split('::').last.gsub(/#{suffix.source}/,'').underscore.to_sym
12
+ name.demodulize.gsub(/#{suffix.source}/,'').underscore.to_sym
13
+ end
14
+ end
15
+ RUBY
16
+
17
+ mod
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,146 @@
1
+ module ROM
2
+ module Auth
3
+ class System
4
+
5
+ attr_reader :configuration, :plugins
6
+
7
+ def initialize(config)
8
+ raise ArgumentError unless config.is_a?(Configuration)
9
+ @configuration = config
10
+
11
+ load_plugins!
12
+ end
13
+
14
+ def migrate!(setup)
15
+ plugins.each do |_type, plugin|
16
+ plugin.migrate(setup)
17
+ end
18
+ end
19
+
20
+ # def authenticators
21
+ # Authenticators::Authenticator.descendants.inject({}) do |acc, desc|
22
+ # acc.merge(desc.shorthand_symbol => desc)
23
+ # end
24
+ # end
25
+
26
+ def authenticate(credentials)
27
+ raise ArgumentError unless credentials
28
+
29
+ success = false
30
+ now = Time.now
31
+ user = identify_user(credentials)
32
+
33
+ on_authentication_attempt(credentials)
34
+
35
+ if user
36
+ authenticated = run_authentication_check(credentials)
37
+
38
+ if authenticated
39
+ on_authentication_success(credentials)
40
+
41
+ if authentication_authorized?(user, credentials)
42
+ on_authorized_authentication(credentials)
43
+ success = true
44
+ else
45
+ on_unauthorized_authentication(credentials)
46
+ end
47
+ else
48
+ on_authentication_failure(credentials)
49
+ end
50
+ else
51
+ on_identification_failure(credentials)
52
+ end
53
+
54
+ on_authentication_completed(
55
+ identifier: credentials.identifier,
56
+ user_id: (user[:id] if user),
57
+ started_at: now,
58
+ ended_at: Time.now,
59
+ type: credentials.type,
60
+ authenticated: authenticated,
61
+ success: success
62
+ )
63
+
64
+ user if success
65
+ end
66
+
67
+ def logger
68
+ configuration.logger
69
+ end
70
+
71
+ def inspect
72
+ "#<ROM::Auth::AuthenticationSystem plugins: #{plugins.keys}>"
73
+ end
74
+
75
+ private
76
+
77
+ def load_plugins!
78
+ @plugins = configuration.plugins.inject({}) do |acc, (plugin, args)|
79
+ acc.merge(plugin => plugin.new(self, *args))
80
+ end
81
+
82
+ @plugins.each do |_type, plugin|
83
+ plugin.install
84
+ end
85
+ end
86
+
87
+ def identify_user(credentials)
88
+ raise NotImplementedError
89
+ end
90
+
91
+ def run_authentication_check(credentials)
92
+ #authenticators[type].new.authenticate(user, data)
93
+ raise NotImplementedError
94
+ end
95
+
96
+ def authentication_authorized?(user, credentials)
97
+ true # FIXME
98
+ end
99
+
100
+ def on_authentication_attempt(credentials)
101
+ logger.info("Attempt: #{credentials.identifier}")
102
+ end
103
+
104
+ def on_identification_failure(credentials)
105
+ logger.info("Identification failed: #{credentials.identifier}")
106
+ end
107
+
108
+ def on_authentication_completed(data)
109
+ raise ArgumentError unless data.is_a?(Hash)
110
+ #raise ArgumentError unless data.keys.to_set == [:identifier, :user_id, :started_at, :ended_at, :authenticated, :authenticator, :success].to_set
111
+ # TODO :reason ?
112
+
113
+ instrument_authentication_event(data)
114
+
115
+ # TODO
116
+ # - log origin/details & warn user
117
+ # - compute failed logins per minute and alert if above threshold
118
+ # - throttling
119
+ #
120
+ end
121
+
122
+ def on_authentication_failure(credentials)
123
+ logger.info("Authentication failed: #{credentials.identifier}")
124
+ end
125
+
126
+ def on_authentication_success(credentials)
127
+ logger.info("Authentication succeeded: #{credentials.identifier}")
128
+ end
129
+
130
+ def on_unauthorized_authentication(credentials)
131
+ logger.info("Unauthorized: #{credentials.identifier}")
132
+ end
133
+
134
+ def on_authorized_authentication(credentials)
135
+ logger.info("Authorized: #{credentials.identifier}")
136
+ end
137
+
138
+ def instrument_authentication_event(auth_data)
139
+ configuration.instrumentation.instrument(
140
+ 'rom:auth:authentication_attempt',
141
+ auth_data
142
+ )
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,5 @@
1
+ module ROM
2
+ module Auth
3
+ VERSION = "0.0.4"
4
+ end
5
+ end
@@ -0,0 +1,162 @@
1
+ describe 'CommonPlugins' do
2
+ let(:setup) { ROM.setup(:sql, "sqlite::memory") }
3
+ let(:connection) { setup.default.connection }
4
+
5
+ def password
6
+ 'somepassword'
7
+ end
8
+
9
+ def password_verifier
10
+ ROM::Auth::PasswordVerifiers::PBKDF2Verifier.for_password(password)
11
+ end
12
+
13
+ before do
14
+ connection.create_table(:users) do
15
+ primary_key :id
16
+ end
17
+
18
+ class User
19
+ include Virtus.value_object(coerce: false)
20
+
21
+ values do
22
+ attribute :id, Integer
23
+ end
24
+ end
25
+
26
+ @users = Class.new(ROM::Relation[:sql]) do
27
+ register_as :users
28
+ dataset :users
29
+
30
+ def by_id(id)
31
+ where(id: id)
32
+ end
33
+ end
34
+
35
+ @mapper = Class.new(ROM::Mapper) do
36
+ relation(:users)
37
+ model(User) # FIXME
38
+ end
39
+ end
40
+
41
+ it '#authenticate with AuthenticationCredentialsPlugin' do
42
+ config = ROM::Auth::Configuration.new do |c|
43
+ c.plugin(ROM::Auth::Plugins::AuthenticationCredentialsPlugin) do
44
+ end
45
+
46
+ c.plugin(ROM::Auth::Plugins::AuthenticationEventsPlugin) do
47
+ end
48
+ end
49
+
50
+ system = ROM::Auth::System.new(config)
51
+
52
+ system.migrate!(setup)
53
+
54
+ rom = ROM.finalize.env
55
+
56
+ connection[:users].insert(id: 1)
57
+ connection[:authentication_credentials].insert(
58
+ user_id: 1,
59
+ type: 'email',
60
+ identifier: 'a@b.c.de',
61
+ verifier_data: password_verifier.to_s
62
+ )
63
+
64
+ credentials = double(type: 'email', identifier: 'a@b.c.de', password: password)
65
+ user = rom.relation(:users).first
66
+
67
+ assert{ system.authenticate(credentials) == user }
68
+ end
69
+
70
+ it '#authenticate with AuthenticationEventsPlugin' do
71
+ config = ROM::Auth::Configuration.new do |c|
72
+ c.plugin(ROM::Auth::Plugins::AuthenticationCredentialsPlugin) do
73
+ end
74
+
75
+ c.plugin(ROM::Auth::Plugins::AuthenticationEventsPlugin) do
76
+ end
77
+
78
+ c.plugin(ROM::Auth::Plugins::LockdownPlugin) do
79
+ end
80
+ end
81
+
82
+ system = ROM::Auth::System.new(config)
83
+
84
+ system.migrate!(setup)
85
+
86
+ rom = ROM.finalize.env
87
+
88
+ connection[:users].insert(id: 1)
89
+ connection[:authentication_credentials].insert(
90
+ user_id: 1,
91
+ type: 'email',
92
+ identifier: 'a@b.c.de',
93
+ verifier_data: password_verifier.to_s
94
+ )
95
+
96
+ credentials = double(type: 'email', identifier: 'a@b.c.de', password: password)
97
+ user = rom.relation(:users).first
98
+
99
+ auths = rom.relation(:authentication_events)
100
+
101
+ assert{ system.authenticate(credentials) == user }
102
+ assert{ auths.relation.count == 1 }
103
+
104
+ event = auths.as(:rom_auth_event).first
105
+ assert{ event.type == 'email' }
106
+ assert{ event.authenticated == true }
107
+ assert{ event.success == true }
108
+ assert{ event.user_id == 1 }
109
+ end
110
+
111
+ it '#authenticate with LockdownPlugin' do
112
+ config = ROM::Auth::Configuration.new do |c|
113
+ c.plugin(ROM::Auth::Plugins::AuthenticationCredentialsPlugin) do
114
+ end
115
+
116
+ c.plugin(ROM::Auth::Plugins::AuthenticationEventsPlugin) do
117
+ end
118
+
119
+ c.plugin(ROM::Auth::Plugins::LockdownPlugin) do |c|
120
+ c.lock_strategy = ->(plugin, user_id, credentials){
121
+ #plugin.system.logger.info('LOCKING')
122
+ plugin.lock(user_id, 'Login failed')
123
+ }
124
+ end
125
+ end
126
+
127
+ system = ROM::Auth::System.new(config)
128
+
129
+ system.migrate!(setup)
130
+
131
+ rom = ROM.finalize.env
132
+
133
+ connection[:users].insert(id: 1)
134
+ connection[:authentication_credentials].insert(
135
+ user_id: 1,
136
+ type: 'email',
137
+ identifier: 'a@b.c.de',
138
+ verifier_data: password_verifier.to_s
139
+ )
140
+
141
+ credentials = double(type: 'email', identifier: 'a@b.c.de', password: 'incorrect')
142
+
143
+ user = rom.relation(:users).first
144
+
145
+ assert{ system.authenticate(credentials) == nil }
146
+
147
+ lock = rom.relation(:rom_auth_lockdowns).as(:rom_auth_lockdown).first
148
+
149
+ assert{ lock.locked_at.close_to?(Time.now, 1) }
150
+ assert{ lock.lock_reason == 'Login failed' }
151
+
152
+ plugin = system.plugins[ROM::Auth::Plugins::LockdownPlugin]
153
+ assert{ plugin.is_locked?(lock.id) == true }
154
+
155
+ plugin.unlock(lock.id)
156
+ assert{ plugin.is_locked?(lock.id) == false }
157
+
158
+ lock = rom.relation(:rom_auth_lockdowns).as(:rom_auth_lockdown).first
159
+ assert{ lock.locked_at == nil }
160
+ assert{ lock.lock_reason == nil }
161
+ end
162
+ end