rom-auth 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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