securial 0.5.0 → 0.6.1

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.
@@ -3,13 +3,14 @@ require "securial/logger"
3
3
  module Securial
4
4
  module Config
5
5
  module Validation
6
- class << self
6
+ class << self # rubocop:disable Metrics/ClassLength
7
7
  def validate_all!(config)
8
8
  validate_admin_role!(config)
9
9
  validate_session_config!(config)
10
10
  validate_mailer_sender!(config)
11
11
  validate_password_config!(config)
12
- validate_timestamps_in_response!(config)
12
+ validate_response_config!(config)
13
+ validate_security_config!(config)
13
14
  end
14
15
 
15
16
  private
@@ -17,19 +18,19 @@ module Securial
17
18
  def validate_admin_role!(config)
18
19
  if config.admin_role.nil? || config.admin_role.to_s.strip.empty?
19
20
  error_message = "Admin role is not set."
20
- Securial::ENGINE_LOGGER.error(error_message)
21
+ Securial::ENGINE_LOGGER.fatal(error_message)
21
22
  raise Securial::Config::Errors::ConfigAdminRoleError, error_message
22
23
  end
23
24
 
24
25
  unless config.admin_role.is_a?(Symbol) || config.admin_role.is_a?(String)
25
26
  error_message = "Admin role must be a Symbol or String."
26
- Securial::ENGINE_LOGGER.error(error_message)
27
+ Securial::ENGINE_LOGGER.fatal(error_message)
27
28
  raise Securial::Config::Errors::ConfigAdminRoleError, error_message
28
29
  end
29
30
 
30
31
  if config.admin_role.to_s.pluralize.downcase == "accounts"
31
32
  error_message = "The admin role cannot be 'account' or 'accounts' as it conflicts with the default routes."
32
- Securial::ENGINE_LOGGER.error(error_message)
33
+ Securial::ENGINE_LOGGER.fatal(error_message)
33
34
  raise Securial::Config::Errors::ConfigAdminRoleError, error_message
34
35
  end
35
36
  end
@@ -43,16 +44,16 @@ module Securial
43
44
  def validate_session_expiry_duration!(config)
44
45
  if config.session_expiration_duration.nil?
45
46
  error_message = "Session expiration duration is not set."
46
- Securial::ENGINE_LOGGER.error(error_message)
47
+ Securial::ENGINE_LOGGER.fatal(error_message)
47
48
  raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
48
49
  end
49
50
  if config.session_expiration_duration.class != ActiveSupport::Duration
50
51
  error_message = "Session expiration duration must be an ActiveSupport::Duration."
51
- Securial::ENGINE_LOGGER.error(error_message)
52
+ Securial::ENGINE_LOGGER.fatal(error_message)
52
53
  raise Securial::Config::Errors::ConfigSessionExpirationDurationError, error_message
53
54
  end
54
55
  if config.session_expiration_duration <= 0
55
- Securial::ENGINE_LOGGER.error("Session expiration duration must be greater than 0.")
56
+ Securial::ENGINE_LOGGER.fatal("Session expiration duration must be greater than 0.")
56
57
  raise Securial::Config::Errors::ConfigSessionExpirationDurationError, "Session expiration duration must be greater than 0."
57
58
  end
58
59
  end
@@ -60,18 +61,18 @@ module Securial
60
61
  def validate_session_algorithm!(config)
61
62
  if config.session_algorithm.blank?
62
63
  error_message = "Session algorithm is not set."
63
- Securial::ENGINE_LOGGER.error(error_message)
64
+ Securial::ENGINE_LOGGER.fatal(error_message)
64
65
  raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
65
66
  end
66
67
  unless config.session_algorithm.is_a?(Symbol)
67
68
  error_message = "Session algorithm must be a Symbol."
68
- Securial::ENGINE_LOGGER.error(error_message)
69
+ Securial::ENGINE_LOGGER.fatal(error_message)
69
70
  raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
70
71
  end
71
72
  valid_algorithms = Securial::Config::VALID_SESSION_ENCRYPTION_ALGORITHMS
72
73
  unless valid_algorithms.include?(config.session_algorithm)
73
74
  error_message = "Invalid session algorithm. Valid options are: #{valid_algorithms.map(&:inspect).join(', ')}."
74
- Securial::ENGINE_LOGGER.error(error_message)
75
+ Securial::ENGINE_LOGGER.fatal(error_message)
75
76
  raise Securial::Config::Errors::ConfigSessionAlgorithmError, error_message
76
77
  end
77
78
  end
@@ -79,12 +80,12 @@ module Securial
79
80
  def validate_session_secret!(config)
80
81
  if config.session_secret.blank?
81
82
  error_message = "Session secret is not set."
82
- Securial::ENGINE_LOGGER.error(error_message)
83
+ Securial::ENGINE_LOGGER.fatal(error_message)
83
84
  raise Securial::Config::Errors::ConfigSessionSecretError, error_message
84
85
  end
85
86
  unless config.session_secret.is_a?(String)
86
87
  error_message = "Session secret must be a String."
87
- Securial::ENGINE_LOGGER.error(error_message)
88
+ Securial::ENGINE_LOGGER.fatal(error_message)
88
89
  raise Securial::Config::Errors::ConfigSessionSecretError, error_message
89
90
  end
90
91
  end
@@ -92,12 +93,12 @@ module Securial
92
93
  def validate_mailer_sender!(config)
93
94
  if config.mailer_sender.blank?
94
95
  error_message = "Mailer sender is not set."
95
- Securial::ENGINE_LOGGER.error(error_message)
96
+ Securial::ENGINE_LOGGER.fatal(error_message)
96
97
  raise Securial::Config::Errors::ConfigMailerSenderError, error_message
97
98
  end
98
99
  if config.mailer_sender !~ URI::MailTo::EMAIL_REGEXP
99
100
  error_message = "Mailer sender is not a valid email address."
100
- Securial::ENGINE_LOGGER.error(error_message)
101
+ Securial::ENGINE_LOGGER.fatal(error_message)
101
102
  raise Securial::Config::Errors::ConfigMailerSenderError, error_message
102
103
  end
103
104
  end
@@ -113,12 +114,12 @@ module Securial
113
114
  def validate_password_reset_token!(config)
114
115
  if config.reset_password_token_secret.blank?
115
116
  error_message = "Reset password token secret is not set."
116
- Securial::ENGINE_LOGGER.error(error_message)
117
+ Securial::ENGINE_LOGGER.fatal(error_message)
117
118
  raise Securial::Config::Errors::ConfigPasswordError, error_message
118
119
  end
119
120
  unless config.reset_password_token_secret.is_a?(String)
120
121
  error_message = "Reset password token secret must be a String."
121
- Securial::ENGINE_LOGGER.error(error_message)
122
+ Securial::ENGINE_LOGGER.fatal(error_message)
122
123
  raise Securial::Config::Errors::ConfigPasswordError, error_message
123
124
  end
124
125
  end
@@ -126,12 +127,12 @@ module Securial
126
127
  def validate_password_reset_subject!(config)
127
128
  if config.password_reset_email_subject.blank?
128
129
  error_message = "Password reset email subject is not set."
129
- Securial::ENGINE_LOGGER.error(error_message)
130
+ Securial::ENGINE_LOGGER.fatal(error_message)
130
131
  raise Securial::Config::Errors::ConfigPasswordError, error_message
131
132
  end
132
133
  unless config.password_reset_email_subject.is_a?(String)
133
134
  error_message = "Password reset email subject must be a String."
134
- Securial::ENGINE_LOGGER.error(error_message)
135
+ Securial::ENGINE_LOGGER.fatal(error_message)
135
136
  raise Securial::Config::Errors::ConfigPasswordError, error_message
136
137
  end
137
138
  end
@@ -139,12 +140,12 @@ module Securial
139
140
  def validate_password_min_max_length!(config)
140
141
  unless config.password_min_length.is_a?(Integer) && config.password_min_length > 0
141
142
  error_message = "Password minimum length must be a positive integer."
142
- Securial::ENGINE_LOGGER.error(error_message)
143
+ Securial::ENGINE_LOGGER.fatal(error_message)
143
144
  raise Securial::Config::Errors::ConfigPasswordError, error_message
144
145
  end
145
146
  unless config.password_max_length.is_a?(Integer) && config.password_max_length >= config.password_min_length
146
147
  error_message = "Password maximum length must be an integer greater than or equal to the minimum length."
147
- Securial::ENGINE_LOGGER.error(error_message)
148
+ Securial::ENGINE_LOGGER.fatal(error_message)
148
149
  raise Securial::Config::Errors::ConfigPasswordError, error_message
149
150
  end
150
151
  end
@@ -152,25 +153,94 @@ module Securial
152
153
  def validate_password_complexity!(config)
153
154
  if config.password_complexity.nil? || !config.password_complexity.is_a?(Regexp)
154
155
  error_message = "Password complexity regex is not set or is not a valid Regexp."
155
- Securial::ENGINE_LOGGER.error(error_message)
156
+ Securial::ENGINE_LOGGER.fatal(error_message)
156
157
  raise Securial::Config::Errors::ConfigPasswordError, error_message
157
158
  end
158
159
  end
159
160
 
160
161
  def validate_password_expiration!(config)
161
- if config.password_expires_in.nil? || !config.password_expires_in.is_a?(ActiveSupport::Duration) || config.password_expires_in <= 0
162
+ unless config.password_expires.is_a?(TrueClass) || config.password_expires.is_a?(FalseClass)
163
+ error_message = "Password expiration must be a boolean value."
164
+ Securial::ENGINE_LOGGER.fatal(error_message)
165
+ raise Securial::Config::Errors::ConfigPasswordError, error_message
166
+ end
167
+
168
+ if config.password_expires == true && (
169
+ config.password_expires_in.nil? ||
170
+ !config.password_expires_in.is_a?(ActiveSupport::Duration) ||
171
+ config.password_expires_in <= 0
172
+ )
162
173
  error_message = "Password expiration duration is not set or is not a valid ActiveSupport::Duration."
163
- Securial::ENGINE_LOGGER.error(error_message)
174
+ Securial::ENGINE_LOGGER.fatal(error_message)
164
175
  raise Securial::Config::Errors::ConfigPasswordError, error_message
165
176
  end
166
177
  end
167
178
 
179
+ def validate_response_config!(config)
180
+ validate_response_keys_format!(config)
181
+ validate_timestamps_in_response!(config)
182
+ end
183
+
184
+ def validate_response_keys_format!(config)
185
+ valid_formats = Securial::Config::VALID_RESPONSE_KEYS_FORMATS
186
+ unless valid_formats.include?(config.response_keys_format)
187
+ error_message = "Invalid response_keys_format option. Valid options are: #{valid_formats.map(&:inspect).join(', ')}."
188
+ Securial::ENGINE_LOGGER.fatal(error_message)
189
+ raise Securial::Config::Errors::ConfigResponseError, error_message
190
+ end
191
+ end
192
+
168
193
  def validate_timestamps_in_response!(config)
169
194
  valid_options = Securial::Config::VALID_TIMESTAMP_OPTIONS
170
195
  unless valid_options.include?(config.timestamps_in_response)
171
196
  error_message = "Invalid timestamps_in_response option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
172
- Securial::ENGINE_LOGGER.error(error_message)
173
- raise Securial::Config::Errors::ConfigTimestampsInResponseError, error_message
197
+ Securial::ENGINE_LOGGER.fatal(error_message)
198
+ raise Securial::Config::Errors::ConfigResponseError, error_message
199
+ end
200
+ end
201
+
202
+ def validate_security_config!(config)
203
+ validate_security_headers!(config)
204
+ validate_rate_limiting!(config)
205
+ end
206
+
207
+ def validate_security_headers!(config)
208
+ valid_options = Securial::Config::VALID_SECURITY_HEADERS
209
+ unless valid_options.include?(config.security_headers)
210
+ error_message = "Invalid security_headers option. Valid options are: #{valid_options.map(&:inspect).join(', ')}."
211
+ Securial::ENGINE_LOGGER.fatal(error_message)
212
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
213
+ end
214
+ end
215
+
216
+ def validate_rate_limiting!(config) # rubocop:disable Metrics/MethodLength
217
+ unless config.rate_limiting_enabled.is_a?(TrueClass) || config.rate_limiting_enabled.is_a?(FalseClass)
218
+ error_message = "rate_limiting_enabled must be a boolean value."
219
+ Securial::ENGINE_LOGGER.fatal(error_message)
220
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
221
+ end
222
+
223
+ return unless config.rate_limiting_enabled
224
+
225
+ unless
226
+ config.rate_limit_requests_per_minute.is_a?(Integer) &&
227
+ config.rate_limit_requests_per_minute > 0
228
+
229
+ error_message = "rate_limit_requests_per_minute must be a positive integer when rate limiting is enabled."
230
+ Securial::ENGINE_LOGGER.fatal(error_message)
231
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
232
+ end
233
+
234
+ unless config.rate_limit_response_status.is_a?(Integer) && config.rate_limit_response_status.between?(400, 599)
235
+ error_message = "rate_limit_response_status must be an HTTP status code between 4xx and 5xx."
236
+ Securial::ENGINE_LOGGER.fatal(error_message)
237
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
238
+ end
239
+
240
+ unless config.rate_limit_response_message.is_a?(String) && !config.rate_limit_response_message.strip.empty?
241
+ error_message = "rate_limit_response_message must be a non-empty String."
242
+ Securial::ENGINE_LOGGER.fatal(error_message)
243
+ raise Securial::Config::Errors::ConfigSecurityError, error_message
174
244
  end
175
245
  end
176
246
  end
@@ -1,16 +1,22 @@
1
1
  require_relative "./logger"
2
+ require_relative "./key_transformer"
2
3
  require_relative "./config/_index"
3
4
  require_relative "./helpers/_index"
4
- require_relative "./sessions/_index"
5
+ require_relative "./auth/_index"
5
6
  require_relative "./inspectors/_index"
6
7
 
7
- require_relative "./middleware/request_logger_tag"
8
+ require_relative "./middleware/_index"
8
9
  require "jwt"
9
10
 
10
11
  module Securial
11
12
  class Engine < ::Rails::Engine
12
13
  isolate_namespace Securial
13
14
 
15
+ # Set API-only mode and autoload custom generator paths
16
+ config.api_only = true
17
+ config.generators.api_only = true
18
+ config.autoload_paths += Dir["#{config.root}/lib/generators"]
19
+
14
20
  initializer "securial.filter_parameters" do |app|
15
21
  app.config.filter_parameters += [
16
22
  :password,
@@ -24,42 +30,47 @@ module Securial
24
30
  Securial.const_set(:ENGINE_LOGGER, Securial::Logger.build)
25
31
  end
26
32
 
27
- initializer "securial.engine_initialized" do |app|
28
- Securial::ENGINE_LOGGER.info("[Securial] Initializing Engine... Host app: #{app.class.name}")
33
+ initializer "securial.log_initialization" do |app|
34
+ log "[Initializing Engine] Host app: #{app.class.name}"
29
35
  end
30
36
 
31
- initializer "securial.config" do
32
- Securial::ENGINE_LOGGER.info("[Securial] Validating configuration in `config/initializers/securial.rb`...")
37
+ initializer "securial.validate_config" do
38
+ log "[Validating configuration] from `config/initializers/securial.rb`..."
33
39
  Securial::Config::Validation.validate_all!(Securial.configuration)
34
40
  end
35
41
 
36
- initializer "securial.factories", after: "factory_bot.set_factory_paths" do
37
- if defined?(FactoryBot)
38
- FactoryBot.definition_file_paths << Engine.root.join("lib", "securial", "factories")
39
- end
42
+ initializer "securial.extend_application_controller" do
43
+ ActiveSupport.on_load(:action_controller_base) { include Securial::Identity }
44
+ ActiveSupport.on_load(:action_controller_api) { include Securial::Identity }
40
45
  end
41
46
 
42
- initializer "securial.load_factory_bot_generator" do
47
+ initializer "securial.factory_bot", after: "factory_bot.set_factory_paths" do
48
+ FactoryBot.definition_file_paths << Engine.root.join("lib", "securial", "factories") if defined?(FactoryBot)
49
+ end
50
+
51
+ initializer "securial.factory_bot_generator" do
43
52
  require_relative "../generators/factory_bot/model/model_generator"
44
53
  end
45
54
 
46
- initializer "securial.extend_application_controller" do
47
- ActiveSupport.on_load(:action_controller_base) do
48
- include Securial::Identity
55
+ initializer "securial.middleware" do |app|
56
+ middleware.use Securial::Middleware::RequestLoggerTag
57
+ middleware.use Securial::Middleware::TransformRequestKeys
58
+ middleware.use Securial::Middleware::TransformResponseKeys
59
+ if Securial.configuration.rate_limiting_enabled
60
+ require "rack/attack"
61
+ require_relative "./rack_attack"
62
+ middleware.use Rack::Attack
49
63
  end
64
+ end
50
65
 
51
- ActiveSupport.on_load(:action_controller_api) do
52
- include Securial::Identity
66
+ initializer "securial.log_ready", after: :load_config_initializers do
67
+ Rails.application.config.after_initialize do
68
+ log "[Engine fully initialized] Environment: #{Rails.env}"
53
69
  end
54
70
  end
55
71
 
56
- config.generators.api_only = true
57
-
58
- config.autoload_paths += Dir["#{config.root}/lib/generators"]
59
-
60
72
  config.generators do |g|
61
73
  g.orm :active_record, primary_key_type: :string
62
-
63
74
  g.test_framework :rspec,
64
75
  fixtures: false,
65
76
  view_specs: false,
@@ -68,21 +79,14 @@ module Securial
68
79
  controller_specs: true,
69
80
  request_specs: true,
70
81
  model_specs: true
71
-
72
82
  g.fixture_replacement :factory_bot, dir: "lib/securial/factories"
73
-
74
- # Add JBuilder configuration
75
83
  g.template_engine :jbuilder
76
84
  end
77
85
 
78
- initializer "securial.middleware" do |app|
79
- app.middleware.use Securial::Middleware::RequestLoggerTag
80
- end
86
+ private
81
87
 
82
- initializer "securial.log_engine_loaded", after: :load_config_initializers do
83
- Rails.application.config.after_initialize do
84
- Securial::ENGINE_LOGGER.info("[Securial] Engine fully initialized in #{Rails.env} environment.")
85
- end
88
+ def log(message)
89
+ Securial::ENGINE_LOGGER.info("[Securial] #{message}")
86
90
  end
87
91
  end
88
92
  end
@@ -0,0 +1,32 @@
1
+ # lib/securial/key_transformer.rb
2
+ module Securial
3
+ module KeyTransformer
4
+ def self.camelize(str, format)
5
+ return str unless str.is_a?(String)
6
+
7
+ case format
8
+ when :lowerCamelCase
9
+ str.camelize(:lower)
10
+ when :UpperCamelCase
11
+ str.camelize
12
+ else
13
+ str
14
+ end
15
+ end
16
+
17
+ def self.underscore(str)
18
+ str.to_s.underscore
19
+ end
20
+
21
+ def self.deep_transform_keys(obj, &block)
22
+ case obj
23
+ when Hash
24
+ obj.transform_keys(&block).transform_values { |v| deep_transform_keys(v, &block) }
25
+ when Array
26
+ obj.map { |e| deep_transform_keys(e, &block) }
27
+ else
28
+ obj
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "./transform_request_keys"
2
+ require_relative "./transform_response_keys"
3
+ require_relative "./request_logger_tag"
@@ -0,0 +1,33 @@
1
+ # lib/securial/middleware/transform_request_keys.rb
2
+ require "json"
3
+
4
+ module Securial
5
+ module Middleware
6
+ class TransformRequestKeys
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ if env["CONTENT_TYPE"]&.include?("application/json") && Securial.configuration.response_keys_format != :snake_case
13
+ req = Rack::Request.new(env)
14
+ if (req.body&.size || 0) > 0
15
+ raw = req.body.read
16
+ req.body.rewind
17
+ begin
18
+ parsed = JSON.parse(raw)
19
+ transformed = Securial::KeyTransformer.deep_transform_keys(parsed) do |key|
20
+ Securial::KeyTransformer.underscore(key)
21
+ end
22
+ env["rack.input"] = StringIO.new(JSON.dump(transformed))
23
+ env["rack.input"].rewind
24
+ rescue JSON::ParserError
25
+ # noop
26
+ end
27
+ end
28
+ end
29
+ @app.call(env)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # lib/securial/middleware/transform_response_keys.rb
2
+ require "json"
3
+
4
+ module Securial
5
+ module Middleware
6
+ class TransformResponseKeys
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ status, headers, response = @app.call(env)
13
+
14
+ if json_response?(headers)
15
+ body = extract_body(response)
16
+
17
+ if body.present?
18
+ format = Securial.configuration.response_keys_format
19
+ transformed = KeyTransformer.deep_transform_keys(JSON.parse(body)) do |key|
20
+ KeyTransformer.camelize(key, format)
21
+ end
22
+
23
+ new_body = [JSON.generate(transformed)]
24
+ headers["Content-Length"] = new_body.first.bytesize.to_s
25
+ return [status, headers, new_body]
26
+ end
27
+ end
28
+
29
+ [status, headers, response]
30
+ end
31
+
32
+ private
33
+
34
+ def json_response?(headers)
35
+ headers["Content-Type"]&.include?("application/json")
36
+ end
37
+
38
+ def extract_body(response)
39
+ response_body = ""
40
+ response.each { |part| response_body << part }
41
+ response_body
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ Rails.application.config.to_prepare do
2
+ class Rack::Attack
3
+ limit = Securial.configuration.rate_limit_requests_per_minute
4
+ resp_status = Securial.configuration.rate_limit_response_status
5
+ resp_message = Securial.configuration.rate_limit_response_message
6
+
7
+ ### Throttle login attempts by IP ###
8
+ throttle("securial/logins/ip", limit: limit, period: 1.minute) do |req|
9
+ if req.path.include?("sessions/login") && req.post?
10
+ req.ip
11
+ end
12
+ end
13
+
14
+ ### Throttle login attempts by username ###
15
+ throttle("securial/logins/email", limit: 5, period: 1.minute) do |req|
16
+ if req.path.include?("sessions/login") && req.post?
17
+ req.params["email_address"].to_s.downcase.strip
18
+ end
19
+ end
20
+
21
+ ### TODO: Add throttling for other endpoints as needed ###
22
+ # Example: Throttle password reset requests by email
23
+ throttle("securial/password_resets/ip", limit: 5, period: 1.minute) do |req|
24
+ if req.path.include?("password/forgot") && req.post?
25
+ req.ip
26
+ end
27
+ end
28
+
29
+ throttle("securial/password_resets/email", limit: 5, period: 1.minute) do |req|
30
+ if req.path.include?("password/forgot") && req.post?
31
+ req.params["email_address"].to_s.downcase.strip
32
+ end
33
+ end
34
+
35
+ ### Custom response ###
36
+ self.throttled_responder = lambda do |request|
37
+ retry_after = (request.env["rack.attack.match_data"] || {})[:period]
38
+ [
39
+ resp_status,
40
+ {
41
+ "Content-Type" => "application/json",
42
+ "Retry-After" => retry_after.to_s,
43
+ },
44
+ [{ error: resp_message }.to_json]
45
+ ]
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module Securial
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.1".freeze
3
3
  end
data/lib/securial.rb CHANGED
@@ -5,12 +5,17 @@ require "jbuilder"
5
5
 
6
6
  module Securial
7
7
  class << self
8
- attr_writer :configuration
8
+ attr_accessor :configuration
9
9
 
10
10
  def configuration
11
11
  @configuration ||= Securial::Config::Configuration.new
12
12
  end
13
13
 
14
+ def configuration=(config)
15
+ @configuration = config
16
+ Securial::Config::Validation.validate_all!(configuration)
17
+ end
18
+
14
19
  def configure
15
20
  yield(configuration)
16
21
  end