negroni 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +64 -0
- data/app/mailers/negroni/mailer.rb +45 -0
- data/app/views/negroni/mailer/password_change.html.erb +5 -0
- data/app/views/negroni/mailer/reset_password_instructions.html.erb +8 -0
- data/app/views/negroni/mailer/unlock_instructions.html.erb +7 -0
- data/config/locales/en.yml +9 -0
- data/config/routes.rb +4 -0
- data/lib/negroni.rb +209 -0
- data/lib/negroni/configuration.rb +231 -0
- data/lib/negroni/controllers/helpers.rb +29 -0
- data/lib/negroni/controllers/token_authenticable.rb +20 -0
- data/lib/negroni/encryptor.rb +35 -0
- data/lib/negroni/engine.rb +35 -0
- data/lib/negroni/mailers/helpers.rb +112 -0
- data/lib/negroni/models.rb +138 -0
- data/lib/negroni/models/authenticable.rb +197 -0
- data/lib/negroni/models/base.rb +318 -0
- data/lib/negroni/models/lockable.rb +216 -0
- data/lib/negroni/models/omniauthable.rb +33 -0
- data/lib/negroni/models/recoverable.rb +204 -0
- data/lib/negroni/models/registerable.rb +14 -0
- data/lib/negroni/models/validatable.rb +63 -0
- data/lib/negroni/modules.rb +12 -0
- data/lib/negroni/omniauth.rb +25 -0
- data/lib/negroni/omniauth/config.rb +81 -0
- data/lib/negroni/orm/active_record.rb +7 -0
- data/lib/negroni/orm/mongoid.rb +6 -0
- data/lib/negroni/param_filter.rb +53 -0
- data/lib/negroni/resolver.rb +17 -0
- data/lib/negroni/token_generator.rb +58 -0
- data/lib/negroni/token_not_found.rb +13 -0
- data/lib/negroni/version.rb +6 -0
- data/lib/tasks/negroni_tasks.rake +5 -0
- metadata +169 -0
@@ -0,0 +1,231 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Negroni
|
4
|
+
# `Configuration` encapsulates all of the configuration for Negroni,
|
5
|
+
# keeping it out of the main module.
|
6
|
+
class Configuration
|
7
|
+
# The default email validation regex.
|
8
|
+
DEFAULT_EMAIL_VALIDATION_REGEX = /\A[^@\s]+@[^@\s]+\z/i
|
9
|
+
# /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
|
10
|
+
|
11
|
+
# @!group Authenticable Configuration
|
12
|
+
|
13
|
+
# Keys used when authenticating
|
14
|
+
# @return [Array<Symbol>]
|
15
|
+
attr_accessor :authentication_keys
|
16
|
+
|
17
|
+
# Keys that should be treated as case-insensitive
|
18
|
+
# @return [Array<Symbol>]
|
19
|
+
attr_accessor :case_insensitive_keys
|
20
|
+
|
21
|
+
# Keys that should have whitespace skipped
|
22
|
+
# @return [Array<Symbol>]
|
23
|
+
attr_accessor :strip_whitespace_keys
|
24
|
+
|
25
|
+
# When true, send an email to notify password changes
|
26
|
+
# @return [Boolean]
|
27
|
+
attr_accessor :send_password_change_notification
|
28
|
+
|
29
|
+
# How long before a token is expired. If nil is provided, token will
|
30
|
+
# last forever.
|
31
|
+
#
|
32
|
+
# @return [ActiveSupport::Duration]
|
33
|
+
attr_accessor :token_lifetime
|
34
|
+
|
35
|
+
# The audience claim to identify the recipients that the token
|
36
|
+
# is intended for.
|
37
|
+
#
|
38
|
+
# @return [Object]
|
39
|
+
attr_accessor :token_audience
|
40
|
+
|
41
|
+
# The name of the exception class that will be raised upon receiving an
|
42
|
+
# invalid auth token.
|
43
|
+
#
|
44
|
+
# Default: {Negroni::TokenNotFound}
|
45
|
+
#
|
46
|
+
# @return [Class, String]
|
47
|
+
attr_accessor :not_found_exception
|
48
|
+
|
49
|
+
# @!group Validatable Configuration
|
50
|
+
|
51
|
+
# Regular expression to validate emails
|
52
|
+
# @return [RegExp]
|
53
|
+
attr_accessor :email_regexp
|
54
|
+
|
55
|
+
# Range validation for password
|
56
|
+
# @return [Range]
|
57
|
+
attr_accessor :password_length
|
58
|
+
|
59
|
+
# @!group Encryption Configuration
|
60
|
+
|
61
|
+
# The number of times to hash the password
|
62
|
+
# @return [Integer]
|
63
|
+
attr_accessor :stretches
|
64
|
+
|
65
|
+
# Used to hash the password. Generate one with `rake secret`.
|
66
|
+
# @return [String] the pepper used to hash the password
|
67
|
+
attr_accessor :pepper
|
68
|
+
|
69
|
+
# The algorithm used to encode the token. Default: 'HS256'
|
70
|
+
#
|
71
|
+
# @return [String]
|
72
|
+
attr_accessor :token_algorithm
|
73
|
+
|
74
|
+
# The secret key that will be used for the token. Default:
|
75
|
+
# {Negroni::secret_key}.
|
76
|
+
#
|
77
|
+
# @return [String]
|
78
|
+
attr_accessor :token_secret
|
79
|
+
|
80
|
+
# An optional public key used to decode tokens.
|
81
|
+
#
|
82
|
+
# @return [String]
|
83
|
+
attr_accessor :token_public_key
|
84
|
+
|
85
|
+
# @!group Lockable Configuration
|
86
|
+
|
87
|
+
# Defines which strategy will be used to lock an account.
|
88
|
+
#
|
89
|
+
# * `:failed_attempts` = Locks an account after a number of failed
|
90
|
+
# attempts.
|
91
|
+
# * `:none` = No lock strategy. You should handle locking by
|
92
|
+
# yourself.
|
93
|
+
#
|
94
|
+
# @return [Symbol] the name of the lock strategy.
|
95
|
+
attr_accessor :lock_strategy
|
96
|
+
|
97
|
+
# Defines which key will be used when locking and unlocking an account
|
98
|
+
# @return [Array<Symbol>]
|
99
|
+
attr_accessor :unlock_keys
|
100
|
+
|
101
|
+
# Defines which strategy can be used to unlock an account.
|
102
|
+
# Valid values: `:email,` `:time,` `:both`
|
103
|
+
# @return [Symbol]
|
104
|
+
attr_accessor :unlock_strategy
|
105
|
+
|
106
|
+
# Number of authentication tries before locking an account
|
107
|
+
# @return [Integer]
|
108
|
+
attr_accessor :maximum_attempts
|
109
|
+
|
110
|
+
# Time interval to unlock the account if `:time` is defined as
|
111
|
+
# `unlock_strategy`.
|
112
|
+
#
|
113
|
+
# @return [ActiveSupport::Duration]
|
114
|
+
attr_accessor :unlock_in
|
115
|
+
|
116
|
+
# Defines which key will be used when recovering the password for an account
|
117
|
+
# @return [Array<Symbol>]
|
118
|
+
attr_accessor :reset_password_keys
|
119
|
+
|
120
|
+
# Time interval you can reset your password with a reset password key
|
121
|
+
# @return [ActiveSupport::Duration]
|
122
|
+
attr_accessor :reset_password_within
|
123
|
+
|
124
|
+
# @!group Mailer Configuration
|
125
|
+
|
126
|
+
# The sender for all mailers
|
127
|
+
# @return [String] the email address for the sender
|
128
|
+
attr_accessor :mailer_sender
|
129
|
+
|
130
|
+
# The class `Mailer` should inherit from (`'ActionMailer::Base'` by default)
|
131
|
+
# @return [String]
|
132
|
+
attr_accessor :parent_mailer
|
133
|
+
|
134
|
+
# @!group Controller Configuration
|
135
|
+
|
136
|
+
# The class `Negroni::BaseController` should inherit from.
|
137
|
+
#
|
138
|
+
# Default: ActionController::API
|
139
|
+
#
|
140
|
+
# @return [String, Class]
|
141
|
+
attr_accessor :parent_controller
|
142
|
+
|
143
|
+
# @!endgroup
|
144
|
+
|
145
|
+
# Create a new instance of `Configuration`, using default values for all
|
146
|
+
# attributes.
|
147
|
+
def initialize # rubocop:disable Metrics/AbcSize,MethodLength
|
148
|
+
@authentication_keys = [:email]
|
149
|
+
@case_insensitive_keys = [:email]
|
150
|
+
@strip_whitespace_keys = [:email]
|
151
|
+
@send_password_change_notification = false
|
152
|
+
@token_lifetime = 1.day
|
153
|
+
@token_algorithm = 'HS256'
|
154
|
+
@not_found_exception = 'Negroni::TokenNotFound'
|
155
|
+
@email_regexp = DEFAULT_EMAIL_VALIDATION_REGEX
|
156
|
+
@password_length = 8..72
|
157
|
+
@stretches = 11
|
158
|
+
@pepper = nil
|
159
|
+
@lock_strategy = :failed_attempts
|
160
|
+
@unlock_keys = [:email]
|
161
|
+
@unlock_strategy = :both
|
162
|
+
@maximum_attempts = 20
|
163
|
+
@unlock_in = 1.hour
|
164
|
+
@reset_password_keys = [:email]
|
165
|
+
@reset_password_within = 6.hours
|
166
|
+
@mailer_sender = nil
|
167
|
+
@parent_mailer = 'ActionMailer::Base'
|
168
|
+
@parent_controller = 'ActionController::API'
|
169
|
+
end
|
170
|
+
|
171
|
+
# `Delegation` adds methods to the including or extending class to delegate
|
172
|
+
# parameters to an instance of {Configuration}.
|
173
|
+
#
|
174
|
+
# Additionally, it provides a method `{#configuration}`, which is simply a
|
175
|
+
# lazily-instantiated instance of {Configuration}.
|
176
|
+
#
|
177
|
+
module Delegation
|
178
|
+
# Create delegation method for configuration
|
179
|
+
#
|
180
|
+
def self.config_delegator(*attrs)
|
181
|
+
attrs.each do |attribute|
|
182
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
183
|
+
def #{attribute}
|
184
|
+
configuration.#{attribute}
|
185
|
+
end
|
186
|
+
|
187
|
+
def #{attribute}=(new_value)
|
188
|
+
configuration.#{attribute} = new_value
|
189
|
+
end
|
190
|
+
METHOD
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# The configuration object
|
195
|
+
# @return [Configuration]
|
196
|
+
def configuration
|
197
|
+
@configuration ||= Configuration.new
|
198
|
+
end
|
199
|
+
|
200
|
+
# @!macro [attach] cfg.delegator
|
201
|
+
# Delegates `$1` to #configuration.
|
202
|
+
#
|
203
|
+
# {render:Configuration#$1}
|
204
|
+
#
|
205
|
+
config_delegator :authentication_keys
|
206
|
+
config_delegator :case_insensitive_keys
|
207
|
+
config_delegator :strip_whitespace_keys
|
208
|
+
config_delegator :send_password_change_notification
|
209
|
+
config_delegator :token_lifetime
|
210
|
+
config_delegator :token_audience
|
211
|
+
config_delegator :not_found_exception
|
212
|
+
config_delegator :email_regexp
|
213
|
+
config_delegator :password_length
|
214
|
+
config_delegator :stretches
|
215
|
+
config_delegator :pepper
|
216
|
+
config_delegator :token_algorithm
|
217
|
+
config_delegator :token_secret
|
218
|
+
config_delegator :token_public_key
|
219
|
+
config_delegator :lock_strategy
|
220
|
+
config_delegator :unlock_keys
|
221
|
+
config_delegator :unlock_strategy
|
222
|
+
config_delegator :maximum_attempts
|
223
|
+
config_delegator :unlock_in
|
224
|
+
config_delegator :reset_password_keys
|
225
|
+
config_delegator :reset_password_within
|
226
|
+
config_delegator :mailer_sender
|
227
|
+
config_delegator :parent_mailer
|
228
|
+
config_delegator :parent_controller
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Negroni
|
4
|
+
module Controllers
|
5
|
+
# Helpers for controllers
|
6
|
+
module Helpers
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
prepend PrependedMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
# Methods that are prepended into the class that includes {Helpers}
|
14
|
+
module PrependedMethods
|
15
|
+
extend ActiveSupport::Concern
|
16
|
+
|
17
|
+
included do
|
18
|
+
rescue_from Negroni.not_found_exception, with: :not_found
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def not_found
|
24
|
+
head :not_found
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'knock/authenticable'
|
4
|
+
|
5
|
+
module Negroni
|
6
|
+
module Controllers
|
7
|
+
# TokenAuthenticable adds methods to the including controller class to
|
8
|
+
# enable authentication via json web tokens. Basically, it just includes
|
9
|
+
# {Knock::Authenticable} in the class.
|
10
|
+
#
|
11
|
+
# @see Knock::Authenticable
|
12
|
+
module TokenAuthenticable
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
include Knock::Authenticable
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
module Negroni
|
6
|
+
# Handles password encryption and digestion
|
7
|
+
module Encryptor
|
8
|
+
class << self
|
9
|
+
# Digest a password
|
10
|
+
#
|
11
|
+
# @param klass [Class] the class for which to digest
|
12
|
+
# @param password [String] the password to digest
|
13
|
+
def digest(klass, password)
|
14
|
+
password = "#{password}#{klass.pepper}" if klass.pepper.present?
|
15
|
+
|
16
|
+
::BCrypt::Password.create(password, cost: klass.stretches).to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
# Compare two passwords
|
20
|
+
#
|
21
|
+
# @param klass [Class] the class for which to digest
|
22
|
+
# @param hashed_password [String] the hashed password to compare
|
23
|
+
# @param password [String] the password to compare with `hashed_password`
|
24
|
+
def compare(klass, hashed_password, password)
|
25
|
+
return false if hashed_password.blank?
|
26
|
+
|
27
|
+
bcrypt = ::BCrypt::Password.new(hashed_password)
|
28
|
+
password = "#{password}#{klass.pepper}" if klass.pepper.present?
|
29
|
+
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
|
30
|
+
|
31
|
+
Negroni.secure_compare(password, hashed_password)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Negroni
|
4
|
+
# The Rails engine
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace Negroni
|
7
|
+
|
8
|
+
config.generators do |g|
|
9
|
+
g.api_only = true
|
10
|
+
g.test_framework :rspec, fixture_replacement: :factory_girl
|
11
|
+
g.factory_girl dir: 'spec/factories'
|
12
|
+
end
|
13
|
+
|
14
|
+
config.negroni = Negroni
|
15
|
+
|
16
|
+
initializer 'negroni.omniauth', after: :load_config_initializers,
|
17
|
+
before: :build_middleware_stack do |app|
|
18
|
+
Negroni.omniauth_configs.each do |_provider, config|
|
19
|
+
app.middleware.use config.strategy_class, *config.args do |strategy|
|
20
|
+
config.strategy = strategy
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
initializer 'negroni.secret_key' do |app|
|
26
|
+
Negroni.secret_key ||= app.secrets.secret_key_base
|
27
|
+
Negroni.token_secret ||= app.secrets.secret_key_base
|
28
|
+
|
29
|
+
Negroni.token_generator ||=
|
30
|
+
if (secret_key = Negroni.secret_key)
|
31
|
+
Negroni::TokenGenerator.new(secret_key: secret_key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Negroni
|
4
|
+
# Namespace for Mailer-related code
|
5
|
+
module Mailers
|
6
|
+
# Common helpers for mailers
|
7
|
+
module Helpers
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
# @!attribute [r] resource
|
13
|
+
# @return [Object, #email] the resource object that we are mailing to
|
14
|
+
attr_reader :resource
|
15
|
+
|
16
|
+
# @!attribute [r] scope_name
|
17
|
+
# @return [String] the name of the record's class, underscored
|
18
|
+
attr_reader :scope_name
|
19
|
+
|
20
|
+
# Configure default email options
|
21
|
+
#
|
22
|
+
# @param record [Object] the object (record) that we are mailing to
|
23
|
+
# @param action [Symbol] the mail action to perform
|
24
|
+
# @param opts [Hash] a hash of options that will be sent to `headers_for`
|
25
|
+
#
|
26
|
+
def negroni_mail(record, action, opts = {}, &block)
|
27
|
+
initialize_from_record(record)
|
28
|
+
mail headers_for(action, opts), &block
|
29
|
+
end
|
30
|
+
|
31
|
+
# Initialize the including class with a record
|
32
|
+
#
|
33
|
+
# @param record [Object] the record to send the email to
|
34
|
+
#
|
35
|
+
def initialize_from_record(record)
|
36
|
+
@scope_name = record.class.to_s.underscore
|
37
|
+
@resource = instance_variable_set("@#{scope_name}", record)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generate a set of mail headers for a given action
|
41
|
+
#
|
42
|
+
# @param action [String, Symbol, #to_s] The mail action that will occur
|
43
|
+
# @param opts [Hash] a hash of additional options to merge with the
|
44
|
+
# headers
|
45
|
+
def headers_for(action, opts = {})
|
46
|
+
headers = {
|
47
|
+
subject: subject_for(action),
|
48
|
+
to: resource.email,
|
49
|
+
from: mailer_from,
|
50
|
+
reply_to: mailer_reply_to,
|
51
|
+
template_path: template_paths,
|
52
|
+
template_name: action
|
53
|
+
}.merge(opts)
|
54
|
+
|
55
|
+
@email = headers[:to]
|
56
|
+
headers
|
57
|
+
end
|
58
|
+
|
59
|
+
def mailer_reply_to
|
60
|
+
mailer_sender(:reply_to)
|
61
|
+
end
|
62
|
+
|
63
|
+
def mailer_from
|
64
|
+
mailer_sender(:from)
|
65
|
+
end
|
66
|
+
|
67
|
+
def mailer_sender(sender = :from)
|
68
|
+
default_sender = default_params[sender]
|
69
|
+
|
70
|
+
unless default_sender.present?
|
71
|
+
ms = Negroni.mailer_sender
|
72
|
+
return ms.is_a?(Proc) ? instance_eval(&ms) : ms
|
73
|
+
end
|
74
|
+
|
75
|
+
if default_sender.respond_to?(:to_proc)
|
76
|
+
instance_eval(&default_sender)
|
77
|
+
else
|
78
|
+
default_sender
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def template_paths
|
83
|
+
_prefixes.dup
|
84
|
+
end
|
85
|
+
|
86
|
+
# Set up a subject doing an I18n lookup. At first, it attempts to set a
|
87
|
+
# subject based on the current mapping:
|
88
|
+
#
|
89
|
+
# en:
|
90
|
+
# negroni:
|
91
|
+
# mailer:
|
92
|
+
# confirmation_instructions:
|
93
|
+
# user_subject: '...'
|
94
|
+
#
|
95
|
+
# If one does not exist, it fallbacks to ActionMailer default:
|
96
|
+
#
|
97
|
+
# en:
|
98
|
+
# negroni:
|
99
|
+
# mailer:
|
100
|
+
# confirmation_instructions:
|
101
|
+
# subject: '...'
|
102
|
+
#
|
103
|
+
# @param key [Symbol, String] the key to lookup for I18n
|
104
|
+
# @return [String]
|
105
|
+
def subject_for(key)
|
106
|
+
I18n.t(:"#{@scope_name}_subject",
|
107
|
+
scope: [:negroni, :mailer, key],
|
108
|
+
default: [:subject, key.to_s.humanize])
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|