negroni-lite 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +63 -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
|