rom-auth 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +63 -0
- data/lib/rom-auth.rb +6 -0
- data/lib/rom/auth.rb +34 -0
- data/lib/rom/auth/authenticators/authenticator.rb +11 -0
- data/lib/rom/auth/authenticators/password_authenticator.rb +14 -0
- data/lib/rom/auth/configuration.rb +45 -0
- data/lib/rom/auth/digest.rb +34 -0
- data/lib/rom/auth/migration.rb +25 -0
- data/lib/rom/auth/password_verifiers/password_verifier.rb +69 -0
- data/lib/rom/auth/password_verifiers/pbkdf2_verifier.rb +24 -0
- data/lib/rom/auth/plugins/authentication_credentials_plugin.rb +115 -0
- data/lib/rom/auth/plugins/authentication_events_plugin.rb +92 -0
- data/lib/rom/auth/plugins/lockdown_plugin.rb +114 -0
- data/lib/rom/auth/plugins/plugin.rb +22 -0
- data/lib/rom/auth/support/shorthand_symbol.rb +22 -0
- data/lib/rom/auth/system.rb +146 -0
- data/lib/rom/auth/version.rb +5 -0
- data/spec/integration/common_plugins_spec.rb +162 -0
- data/spec/integration/system_spec.rb +68 -0
- data/spec/unit/rom/auth/configuration_spec.rb +27 -0
- data/spec/unit/rom/auth/password_verifiers/pbkdf2_verifier_spec.rb +30 -0
- data/spec/unit/rom/auth/plugins/authentication_events_plugin_spec.rb +109 -0
- data/spec/unit/rom/auth_spec.rb +3 -0
- metadata +242 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0ce8711f63f7f47e34d33f9eb7d06cf50c786dec
|
4
|
+
data.tar.gz: f348776bc063992f9e86e8205c61a244d07b8ed8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f26618867fd3c1580bd258a7c1c5c14ec62bd9a357ad93fa4222f827191965a7fc03c22031aadd9d419da9e2487b82adc2846cd08a044555080bebc785525842
|
7
|
+
data.tar.gz: dfc67314e9c244cf45e09480985bce757f46bf29f17ee0458b763419a10de6ecb8078ecec5d7ffa3fd82467fd27a546a0e0c61dbaf6fbe2d77221fef14f9617f
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# rom-auth
|
2
|
+
|
3
|
+
* https://github.com/Ragmaanir/rom-auth
|
4
|
+
|
5
|
+
## DESCRIPTION:
|
6
|
+
|
7
|
+
Low level Authentication solution based on [ROM](http://rom-rb.org). It is based on plugins and only supposed to do database-oriented authentication of users. It is completely independent of HTTP/Rails.
|
8
|
+
|
9
|
+
## FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
* authentication of users via passwords
|
12
|
+
* Plugins
|
13
|
+
* A plugin for storing authentication attempts and their success/failure
|
14
|
+
* A lockdown plugin for locking down user accounts on authentication failure
|
15
|
+
|
16
|
+
## SYNOPSIS:
|
17
|
+
|
18
|
+
* TODO
|
19
|
+
|
20
|
+
## REQUIREMENTS:
|
21
|
+
|
22
|
+
* activesupport
|
23
|
+
* rom
|
24
|
+
* pbkdf2
|
25
|
+
* virtus
|
26
|
+
|
27
|
+
## INSTALL:
|
28
|
+
|
29
|
+
* gem install rom-auth
|
30
|
+
|
31
|
+
## DEVELOPERS:
|
32
|
+
|
33
|
+
After checking out the source, run:
|
34
|
+
|
35
|
+
$ rake newb
|
36
|
+
|
37
|
+
This task will install any missing dependencies, run the tests/specs,
|
38
|
+
and generate the RDoc.
|
39
|
+
|
40
|
+
## LICENSE:
|
41
|
+
|
42
|
+
(The MIT License)
|
43
|
+
|
44
|
+
Copyright (c) 2015 Ragmaanir
|
45
|
+
|
46
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
47
|
+
a copy of this software and associated documentation files (the
|
48
|
+
'Software'), to deal in the Software without restriction, including
|
49
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
50
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
51
|
+
permit persons to whom the Software is furnished to do so, subject to
|
52
|
+
the following conditions:
|
53
|
+
|
54
|
+
The above copyright notice and this permission notice shall be
|
55
|
+
included in all copies or substantial portions of the Software.
|
56
|
+
|
57
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
58
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
59
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
60
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
61
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
62
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
63
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/rom-auth.rb
ADDED
data/lib/rom/auth.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require 'rom'
|
5
|
+
require 'rom-sql'
|
6
|
+
require 'virtus'
|
7
|
+
require 'active_support'
|
8
|
+
require 'active_support/core_ext'
|
9
|
+
|
10
|
+
# require 'rom/auth/support/shorthand_symbol'
|
11
|
+
|
12
|
+
require 'rom/auth/version'
|
13
|
+
require 'rom/auth/configuration'
|
14
|
+
require 'rom/auth/system'
|
15
|
+
|
16
|
+
# require 'rom/auth/authenticators/authenticator'
|
17
|
+
# require 'rom/auth/authenticators/password_authenticator'
|
18
|
+
|
19
|
+
require 'rom/auth/migration'
|
20
|
+
|
21
|
+
require 'rom/auth/plugins/plugin'
|
22
|
+
require 'rom/auth/plugins/authentication_events_plugin'
|
23
|
+
require 'rom/auth/plugins/authentication_credentials_plugin'
|
24
|
+
require 'rom/auth/plugins/lockdown_plugin'
|
25
|
+
|
26
|
+
require 'rom/auth/digest'
|
27
|
+
require 'rom/auth/password_verifiers/password_verifier'
|
28
|
+
require 'rom/auth/password_verifiers/pbkdf2_verifier'
|
29
|
+
|
30
|
+
module ROM
|
31
|
+
module Auth
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
module Authenticators
|
3
|
+
class PasswordAuthenticator < Authenticator
|
4
|
+
|
5
|
+
def authenticate(user, identifier, password)
|
6
|
+
raise ArgumentError unless password.is_a?(String)
|
7
|
+
|
8
|
+
#user.password_verifier.verifies?(password)
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ROM
|
2
|
+
module Auth
|
3
|
+
class Configuration
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :users_table_name, Symbol, default: :users
|
7
|
+
attribute :email_confirmation_token_length, Range
|
8
|
+
attribute :cookie_authentication_token_length, Range
|
9
|
+
attribute :instrumentation, Object, default: ->(c,_){ c.find_default_instrumentation }
|
10
|
+
attribute :plugins, Hash, default: {}
|
11
|
+
attribute :logger, Object, default: ->(_,_){
|
12
|
+
Logger.new(STDOUT).tap do |l|
|
13
|
+
l.level = Logger::WARN
|
14
|
+
l.progname = 'ROM::Auth'
|
15
|
+
end
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize(*args, &block)
|
19
|
+
super(*args)
|
20
|
+
block.call(self) if block
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def plugin(cls, options={}, &block)
|
25
|
+
raise(ArgumentError, "Expected a class but was #{cls.inspect}") unless cls.is_a?(Class)
|
26
|
+
config = cls.const_get(:Configuration).new(options)
|
27
|
+
block.call(config) if block
|
28
|
+
self.plugins = plugins.merge(cls => config)
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_default_instrumentation
|
32
|
+
ActiveSupport::Notifications
|
33
|
+
end
|
34
|
+
|
35
|
+
def singular_users_table_name
|
36
|
+
users_table_name.to_s.singularize
|
37
|
+
end
|
38
|
+
|
39
|
+
def user_fk_name
|
40
|
+
[singular_users_table_name, :id].join('_').to_sym
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ROM
|
2
|
+
module Auth
|
3
|
+
|
4
|
+
class Digest
|
5
|
+
def initialize(data)
|
6
|
+
raise(ArgumentError, "String required got #{data.class}") if !data.is_a?(String)
|
7
|
+
@data = data
|
8
|
+
end
|
9
|
+
|
10
|
+
def length
|
11
|
+
@data.length
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
raise ArgumentError if !other.is_a?(Digest)
|
16
|
+
raise ArgumentError if length != other.length
|
17
|
+
|
18
|
+
# SECURITY timing attack
|
19
|
+
# TODO because of short-circuiting this is actually not 100% constant time. fix this.
|
20
|
+
result = @data.chars.zip(other.to_s.chars).inject(true) do |res, (char, other_char)|
|
21
|
+
eq = (char == other_char)
|
22
|
+
res && eq
|
23
|
+
end
|
24
|
+
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
@data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
class Migration
|
3
|
+
attr_reader :system, :setup, :config
|
4
|
+
|
5
|
+
def initialize(system, setup, config)
|
6
|
+
@system = system
|
7
|
+
@setup = setup
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def column_exists?(table, column)
|
12
|
+
database.schema(table).find{ |col| col.first == column }
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def database
|
22
|
+
@setup.default.connection
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ROM
|
2
|
+
module Auth
|
3
|
+
module PasswordVerifiers
|
4
|
+
class PasswordVerifier
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = { :iterations => 15000, :hash_function => :sha256 }.freeze
|
7
|
+
DEFAULT_SALT_LENGTH = 16
|
8
|
+
SEPARATOR = ","
|
9
|
+
VERIFIERS = {}
|
10
|
+
|
11
|
+
attr_reader :salt, :digest, :options
|
12
|
+
|
13
|
+
def verifies?(plaintext_password)
|
14
|
+
tested_digest = compute_digest(plaintext_password, salt, options)
|
15
|
+
|
16
|
+
digest == tested_digest
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
[type, salt, digest.to_s, options[:iterations]].join(SEPARATOR)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
case other
|
25
|
+
when PasswordVerifier then to_s == other.to_s
|
26
|
+
when String then to_s == other
|
27
|
+
else false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.for_password(plaintext_password, options={})
|
32
|
+
raise ArgumentError unless plaintext_password.is_a?(String)
|
33
|
+
new(options.merge(:password => plaintext_password))
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.from_s(string)
|
37
|
+
kind, salt, digest, iterations = string.split(SEPARATOR)
|
38
|
+
|
39
|
+
raise(ArgumentError, "Invalid password verifier kind: #{kind.inspect}") unless VERIFIERS.keys.map(&:to_s).include?(kind)
|
40
|
+
|
41
|
+
VERIFIERS[kind.to_sym].new(DEFAULT_OPTIONS.merge(:digest => digest, :salt => salt, :iterations => iterations.to_i))
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def initialize(options={})
|
47
|
+
raise(ArgumentError, ":password or :digest required but got #{options.inspect}") unless options.values_at(:password, :digest).compact.one?
|
48
|
+
|
49
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
50
|
+
@salt = @options.delete(:salt) || generate_random_salt(@options.delete(:salt_length))
|
51
|
+
@digest = if digest_str = @options.delete(:digest)
|
52
|
+
Digest.new(digest_str)
|
53
|
+
else
|
54
|
+
compute_digest(@options[:password], @salt, @options)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def compute_digest(plaintext_password, salt, options={})
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_random_salt(length=nil)
|
63
|
+
SecureRandom.hex(length || DEFAULT_SALT_LENGTH)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'pbkdf2'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Auth
|
5
|
+
module PasswordVerifiers
|
6
|
+
class PBKDF2Verifier < PasswordVerifier
|
7
|
+
|
8
|
+
PasswordVerifier::VERIFIERS.merge!(:PBKDF2 => PBKDF2Verifier)
|
9
|
+
|
10
|
+
def type
|
11
|
+
:PBKDF2
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def compute_digest(plaintext_password, salt, options={})
|
17
|
+
hex = PBKDF2.new(options.merge(:password => plaintext_password, :salt => salt)).hex_string
|
18
|
+
Digest.new(hex)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module ROM::Auth
|
2
|
+
module Plugins
|
3
|
+
class AuthenticationCredentialsPlugin < Plugin
|
4
|
+
|
5
|
+
class Configuration
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
attribute :table_name, Symbol, default: :authentication_credentials
|
9
|
+
end
|
10
|
+
|
11
|
+
def install
|
12
|
+
system.extend(CallbackOverrides)
|
13
|
+
|
14
|
+
config = configuration
|
15
|
+
|
16
|
+
@mapper_cls = Class.new(ROM::Mapper) do
|
17
|
+
relation(config.table_name)
|
18
|
+
model(AuthenticationCredential)
|
19
|
+
register_as :rom_auth_credential
|
20
|
+
end
|
21
|
+
|
22
|
+
@relation_cls = Class.new(ROM::Relation[:sql]) do
|
23
|
+
dataset(config.table_name)
|
24
|
+
|
25
|
+
def find_record(credentials)
|
26
|
+
raise if !credentials.respond_to?(:type) || !credentials.respond_to?(:identifier)
|
27
|
+
where(
|
28
|
+
type: credentials.type,
|
29
|
+
identifier: credentials.identifier
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def migrate(setup)
|
36
|
+
AuthenticationCredentialsMigration.new(system, setup, configuration).run
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_credential_entry(credentials)
|
40
|
+
ROM.env.relation(configuration.table_name).find_record(credentials).as(:rom_auth_credential).first
|
41
|
+
end
|
42
|
+
|
43
|
+
def identify_user(credentials)
|
44
|
+
cred = find_credential_entry(credentials)
|
45
|
+
ROM.env.relation(system.configuration.users_table_name).by_id(cred.user_id).first if cred
|
46
|
+
end
|
47
|
+
|
48
|
+
def authenticate(credentials)
|
49
|
+
cred = find_credential_entry(credentials)
|
50
|
+
|
51
|
+
cred.verifier.verifies?(credentials.password)
|
52
|
+
end
|
53
|
+
|
54
|
+
class AuthenticationCredential
|
55
|
+
include Virtus.value_object(coerce: false)
|
56
|
+
|
57
|
+
values do
|
58
|
+
attribute :user_id, Integer # FIXME this could be dynamic and could be account_id
|
59
|
+
attribute :created_at, DateTime
|
60
|
+
attribute :updated_at, DateTime
|
61
|
+
attribute :identifier, String
|
62
|
+
attribute :type, String
|
63
|
+
attribute :verifier_data, String
|
64
|
+
#attribute :verifier_type, String
|
65
|
+
attribute :active, Boolean
|
66
|
+
end
|
67
|
+
|
68
|
+
def verifier
|
69
|
+
PasswordVerifiers::PasswordVerifier.from_s(verifier_data)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class AuthenticationCredentialsMigration < Migration
|
74
|
+
def run
|
75
|
+
config = self.config
|
76
|
+
auth_config = system.configuration
|
77
|
+
|
78
|
+
user_fk_name = auth_config.user_fk_name
|
79
|
+
|
80
|
+
database.create_table(config.table_name) do
|
81
|
+
foreign_key(user_fk_name, auth_config.users_table_name.to_sym)
|
82
|
+
|
83
|
+
String :identifier
|
84
|
+
String :type
|
85
|
+
Boolean :active
|
86
|
+
#add_constraint(:password_verifier_length, Sequel.function(:char_length, :password_verifier)=>64..255)
|
87
|
+
|
88
|
+
String :verifier_data
|
89
|
+
|
90
|
+
DateTime :confirmed_at, null: true
|
91
|
+
String :confirmation_token, null: true
|
92
|
+
#add_constraint(:email_confirmation_token_length) { char_length(email_confirmation_token) == config.email_confirmation_token_length }
|
93
|
+
#add_constraint(:email_confirmation_token_length, Sequel.function(:char_length, :email_confirmation_token) => config.email_confirmation_token_length)
|
94
|
+
|
95
|
+
#String :cookie_authentication_token, null: true
|
96
|
+
#add_constraint(:cookie_authentication_token_length) { char_length(:cookie_authentication_token) == config.cookie_authentication_token_length }
|
97
|
+
#add_constraint(:cookie_authentication_token_length, Sequel.function(:char_length, :cookie_authentication_token) => config.cookie_authentication_token_length)
|
98
|
+
index [:identifier, :type], unique: true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
module CallbackOverrides
|
104
|
+
def identify_user(credentials)
|
105
|
+
plugins[ROM::Auth::Plugins::AuthenticationCredentialsPlugin].identify_user(credentials)
|
106
|
+
end
|
107
|
+
|
108
|
+
def run_authentication_check(credentials)
|
109
|
+
plugins[ROM::Auth::Plugins::AuthenticationCredentialsPlugin].authenticate(credentials)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|