securial 0.4.2 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -14
  3. data/app/controllers/concerns/securial/identity.rb +4 -12
  4. data/app/controllers/securial/sessions_controller.rb +20 -8
  5. data/app/models/concerns/securial/password_resettable.rb +15 -0
  6. data/app/models/securial/session.rb +2 -2
  7. data/app/views/securial/roles/_securial_role.json.jbuilder +1 -2
  8. data/app/views/securial/sessions/_session.json.jbuilder +1 -2
  9. data/app/views/securial/shared/_timestamps.json.jbuilder +8 -0
  10. data/app/views/securial/users/_securial_user.json.jbuilder +3 -2
  11. data/config/routes.rb +0 -1
  12. data/db/migrate/20250517155521_create_securial_users.rb +10 -7
  13. data/lib/generators/securial/install/templates/securial_initializer.erb +53 -12
  14. data/lib/generators/securial/install/views_generastor.rb +25 -0
  15. data/lib/generators/securial/jbuilder/templates/_resource.json.erb +1 -2
  16. data/lib/securial/auth/_index.rb +3 -0
  17. data/lib/securial/auth/auth_encoder.rb +56 -0
  18. data/lib/securial/auth/errors.rb +15 -0
  19. data/lib/securial/auth/session_creator.rb +21 -0
  20. data/lib/securial/config/_index.rb +3 -0
  21. data/lib/securial/config/configuration.rb +62 -0
  22. data/lib/securial/config/errors.rb +20 -0
  23. data/lib/securial/config/validation.rb +249 -0
  24. data/lib/securial/engine.rb +41 -38
  25. data/lib/securial/helpers/_index.rb +2 -0
  26. data/lib/securial/helpers/regex_helper.rb +7 -7
  27. data/lib/securial/inspectors/_index.rb +1 -0
  28. data/lib/securial/inspectors/route_inspector.rb +52 -0
  29. data/lib/securial/key_transformer.rb +32 -0
  30. data/lib/securial/logger.rb +2 -2
  31. data/lib/securial/middleware/_index.rb +3 -0
  32. data/lib/securial/middleware/transform_request_keys.rb +33 -0
  33. data/lib/securial/middleware/transform_response_keys.rb +45 -0
  34. data/lib/securial/rack_attack.rb +48 -0
  35. data/lib/securial/version.rb +1 -1
  36. data/lib/securial.rb +7 -72
  37. metadata +37 -154
  38. data/app/views/securial/passwords/_password.json.jbuilder +0 -2
  39. data/app/views/securial/passwords/index.json.jbuilder +0 -1
  40. data/app/views/securial/passwords/show.json.jbuilder +0 -1
  41. data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +0 -6
  42. data/lib/securial/configuration.rb +0 -35
  43. data/lib/securial/errors/config_errors.rb +0 -12
  44. data/lib/securial/errors/session_errors.rb +0 -6
  45. data/lib/securial/helpers/auth_helper.rb +0 -46
  46. data/lib/securial/route_inspector.rb +0 -50
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e0abc0d4c0a4b2a56220ab298edd28f767b4d8909a6af78f3097421cfc2fdda
4
- data.tar.gz: 9230d0f2c93d1899cb642206adf79ceec04936894720dc989cc2b25c873f8774
3
+ metadata.gz: 1628d056fdb0a93bc954107766c8f5ed2975d4690c58dbdbe56a0d45e63eefa3
4
+ data.tar.gz: 166ce5d80dc63642239a0f6c26702e4bbca469c2f7d886d322aa07e119ef8928
5
5
  SHA512:
6
- metadata.gz: 04102e38f339f980e2792c6dcc0559ef68a48506c31fc6b6625f44f7cc8f0a668d32c7e42583dfb7abe2d45690ca3e0df9ed9acb916f5c1630c651966413f8b2
7
- data.tar.gz: 7db35473e9c983e9c6f803889da638ac210795ea8a9bc1270efd8136fc6a7c293ac01b7b8ca53b43fa695b33e872884e215a0496763222a0159a8c0dd5efe3d0
6
+ metadata.gz: 997ba8dc253bf3772ef806e9c81cce2fb7a7b0dd263527f0896badf74e5ea09ab5560ca804162cde01441054ac5b5d0fca9c39dfc2607cf18086ec6bb4667af2
7
+ data.tar.gz: b7ff4072c37645e9dc6f1bd5551bb47ec829cbefd412221bcdf13e5a2e788450243c1e7853e918248a2b1faaea61aec40122ddea32f1ef80a1ae013367d9d349
data/README.md CHANGED
@@ -1,6 +1,3 @@
1
- ![image](https://github.com/user-attachments/assets/d7cb9645-c28e-4cca-9c1b-5085a91c11d4)
2
-
3
- ---
4
1
  # Securial
5
2
 
6
3
  [![Gem Version](https://img.shields.io/gem/v/securial?logo=rubygems&logoColor=ffffff&logoSize=auto&label=version&color=violet&cacheSeconds=120)](https://rubygems.org/gems/securial)
@@ -10,6 +7,16 @@
10
7
  [![Tests](https://github.com/alybadawy/securial/actions/workflows/ci.yml/badge.svg)](https://github.com/alybadawy/securial/actions)
11
8
  [![Coverage Status](https://coveralls.io/repos/github/AlyBadawy/Securial/badge.svg?branch=main)](https://coveralls.io/github/AlyBadawy/Securial?branch=main)
12
9
 
10
+ > [!WARNING]
11
+ >
12
+ > **Securial is currently in active development (major version zero).**
13
+ >
14
+ > While the gem is functional and versioned, it is not yet considered stable. Until v1.0.0 is released, any updates may introduce breaking changes as the API and features continue to evolve. If you plan to use Securial in production, please do so with caution and pin a specific version.
15
+ >
16
+ > You can track the roadmap and remaining tasks for the v1.0.0 release in [this GitHub issue](https://github.com/AlyBadawy/Securial/issues/64).
17
+
18
+ ---
19
+
13
20
  **Securial** is a mountable Rails engine that provides robust, extensible authentication and access control for Rails applications. It supports:
14
21
 
15
22
  - ✅ JWT-based authentication
@@ -19,15 +26,35 @@
19
26
  - ✅ Clean, JSON-based API responses
20
27
  - ✅ Database-agnostic support
21
28
 
22
- Next, mount the engine in `config/routes.rb`:
23
29
 
24
- ```ruby
25
- Rails.application.routes.draw do
26
- mount Securial::Engine => "/securial"
27
- end
28
- ```
30
+ ### 🚀 Why Securial?
31
+
32
+ Securial was built to offer a clean, modular, and API-first authentication system for Rails developers who want full control without the black-box complexity. Whether you're building for the web, mobile, or both, Securial gives you the flexibility to implement exactly what you need — from simple JWT authentication to more advanced setups involving sessions, API tokens, and role-based access.
33
+
34
+ It follows familiar Rails conventions, stays lightweight and database-agnostic, and keeps security at the core. With fully customizable controllers, serializers, and logic, Securial is designed to grow with your project — making it an ideal choice for everything from side projects to production-grade APIs.
35
+
36
+
29
37
 
30
- Full installation steps are available in the [Wiki › Installation](https://github.com/AlyBadawy/Securial/wiki/Installation).
38
+ ## 🚀 Installation
39
+
40
+ Securial can be installed on an existing Rails application or use the `securial new app_name` command to create a new Securial-ready Rails app.
41
+
42
+ ### Installation on an existing Rails app:
43
+
44
+ To add Securial to an existing Rails app:
45
+
46
+ - Add `gem "securial"` to your GemFile
47
+ - Run `bundle install`
48
+ - Run `rails generate securial:install`
49
+ - Mount the Securial engine in your Rails application `config/routes.rb` file:
50
+ ```ruby
51
+ Rails.application.routes.draw do
52
+ mount Securial::Engine => "/securial"
53
+ end
54
+ ```
55
+ - Run the migrations by running the command: `rails db:migrate`
56
+
57
+ 💡 Full installation steps are available in the [Wiki › Installation](https://github.com/AlyBadawy/Securial/wiki/Installation).
31
58
 
32
59
  ## ⚙️ Configuration
33
60
 
@@ -49,24 +76,29 @@ After installation and mounting, **Securial** exposes endpoints like:
49
76
 
50
77
  Full details, including authentication flows and protected routes, are available in the [Wiki › Authentication module docs](https://github.com/AlyBadawy/Securial/wiki/Authentication).
51
78
 
52
- 🧩 Modules
79
+ ## 🧩 Modules
53
80
 
54
81
  **Securial** is organized into modular components including:
55
82
 
56
83
  - Authentication
57
84
  - User Management
58
85
  - Generators
59
- - Securial::Identity concern
86
+ - Identity concern
87
+ - Configuration
60
88
 
61
89
  Explore all modules in the [Wiki](https://github.com/AlyBadawy/Securial/wiki).
62
90
 
63
91
  ## 🛠 Development & Testing
64
92
 
93
+ - Clone the repo on your computer
94
+ - Run `bundle install`
95
+ - Start coding right away 🏃‍♂️
96
+
97
+
65
98
  To run the test suite:
66
99
 
67
100
  ```bash
68
- $ RAILS_ENV=test bundle db:schema:load
69
- $ bundle exec rspec
101
+ $ bin/test
70
102
  ```
71
103
 
72
104
  View the coverage report:
@@ -88,3 +120,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/alybad
88
120
  ## ⚖️ License
89
121
 
90
122
  The gem is available as open source under the terms of the [MIT license](https://github.com/AlyBadawy/Securial?tab=MIT-1-ov-file#readme).
123
+
124
+ ---
125
+
126
+ ![image](https://github.com/user-attachments/assets/d7cb9645-c28e-4cca-9c1b-5085a91c11d4)
@@ -32,9 +32,9 @@ module Securial
32
32
  if auth_header.present? && auth_header.start_with?("Bearer ")
33
33
  token = auth_header.split(" ").last
34
34
  begin
35
- decoded_token = AuthHelper.decode(token)
35
+ decoded_token = Securial::Auth::AuthEncoder.decode(token)
36
36
  Current.session = Session.find_by!(id: decoded_token["jti"], revoked: false)
37
- rescue JWT::DecodeError, ActiveRecord::RecordNotFound => e
37
+ rescue Securial::Auth::Errors::AuthDecodeError, ActiveRecord::RecordNotFound => e
38
38
  render status: :unauthorized, json: { error: "Invalid token: #{e.message}" } and return
39
39
  end
40
40
  else
@@ -43,19 +43,11 @@ module Securial
43
43
  end
44
44
 
45
45
  def start_new_session_for(user)
46
- user.sessions.create!(
47
- user_agent: request.user_agent,
48
- ip_address: request.remote_ip,
49
- refresh_token: SecureRandom.hex(64),
50
- last_refreshed_at: Time.current,
51
- refresh_token_expires_at: 1.week.from_now,
52
- ).tap do |session|
53
- Current.session = session
54
- end
46
+ Securial::Auth::SessionCreator.create_session(user, request)
55
47
  end
56
48
 
57
49
  def create_jwt_for_current_session
58
- AuthHelper.encode(Current.session)
50
+ Securial::Auth::AuthEncoder.encode(Current.session)
59
51
  end
60
52
 
61
53
  def internal_rails_request?
@@ -14,18 +14,12 @@ module Securial
14
14
  def login
15
15
  params.require([:email_address, :password])
16
16
  if user = User.authenticate_by(params.permit([:email_address, :password]))
17
- start_new_session_for user
18
- render status: :created,
19
- json: {
20
- access_token: create_jwt_for_current_session,
21
- refresh_token: Current.session.refresh_token,
22
- refresh_token_expires_at: Current.session.refresh_token_expires_at,
23
- }
17
+ render_login_response(user)
24
18
  else
25
19
  render status: :unauthorized,
26
20
  json: {
27
21
  error: "Invalid email address or password.",
28
- fix: "Make sure to send the correct 'email_address' and 'password' in the payload",
22
+ instructions: "Make sure to send the correct 'email_address' and 'password' in the payload",
29
23
  }
30
24
  end
31
25
  end
@@ -72,5 +66,23 @@ module Securial
72
66
  id = params[:id]
73
67
  @securial_session = id ? Current.user.sessions.find(params.expect(:id)) : Current.session
74
68
  end
69
+
70
+ def render_login_response(user)
71
+ if user.password_expired?
72
+ render status: :forbidden,
73
+ json: {
74
+ error: "Password expired",
75
+ instructions: "Please reset your password before logging in.",
76
+ }
77
+ else
78
+ start_new_session_for user
79
+ render status: :created,
80
+ json: {
81
+ access_token: create_jwt_for_current_session,
82
+ refresh_token: Current.session.refresh_token,
83
+ refresh_token_expires_at: Current.session.refresh_token_expires_at,
84
+ }
85
+ end
86
+ end
75
87
  end
76
88
  end
@@ -5,6 +5,8 @@ module Securial
5
5
  included do
6
6
  has_secure_password
7
7
 
8
+ before_save :update_password_changed_at, if: :will_save_change_to_password_digest?
9
+
8
10
  validates :password,
9
11
  length: {
10
12
  minimum: Securial.configuration.password_min_length,
@@ -43,5 +45,18 @@ module Securial
43
45
  reset_password_token_created_at: nil
44
46
  )
45
47
  end
48
+
49
+ def password_expired?
50
+ return false unless Securial.configuration.password_expires
51
+ return true unless password_changed_at
52
+
53
+ password_changed_at < Securial.configuration.password_expires_in.ago
54
+ end
55
+
56
+ private
57
+
58
+ def update_password_changed_at
59
+ self.password_changed_at = Time.current
60
+ end
46
61
  end
47
62
  end
@@ -15,8 +15,8 @@ module Securial
15
15
  end
16
16
 
17
17
  def refresh!
18
- raise Securial::SessionErrors::SessionRevokedError, "Session is revoked" if revoked
19
- raise Securial::SessionErrors::SessionExpiredError, "Session is expired" if refresh_token_expires_at < Time.current
18
+ raise Securial::Auth::Errors::AuthRevokedError, "Session is revoked" if revoked
19
+ raise Securial::Auth::Errors::AuthExpiredError, "Session is expired" if refresh_token_expires_at < Time.current
20
20
 
21
21
  update!(refresh_token: SecureRandom.hex(64),
22
22
  refresh_count: self.refresh_count + 1,
@@ -3,7 +3,6 @@ json.id securial_role.id
3
3
  json.role_name securial_role.role_name
4
4
  json.hide_from_profile securial_role.hide_from_profile
5
5
 
6
- json.created_at securial_role.created_at
7
- json.updated_at securial_role.updated_at
6
+ json.partial! "securial/shared/timestamps", record: securial_role
8
7
 
9
8
  json.url securial.roles_url(securial_role, format: :json)
@@ -9,7 +9,6 @@ json.refresh_token_expires_at securial_session.refresh_token_expires_at
9
9
  json.revoked securial_session.revoked
10
10
  json.user_id securial_session.user_id
11
11
 
12
- json.created_at securial_session.created_at
13
- json.updated_at securial_session.updated_at
12
+ json.partial! "securial/shared/timestamps", record: securial_session
14
13
 
15
14
  json.url "No URL available for this action"
@@ -0,0 +1,8 @@
1
+ if Securial.configuration.timestamps_in_response == :all ||
2
+ (
3
+ Securial.configuration.timestamps_in_response == :admins_only &&
4
+ current_user&.admin?
5
+ )
6
+ json.created_at record.created_at if record.respond_to?(:created_at)
7
+ json.updated_at record.updated_at if record.respond_to?(:updated_at)
8
+ end
@@ -6,9 +6,10 @@ json.phone securial_user.phone
6
6
  json.username securial_user.username
7
7
  json.bio securial_user.bio
8
8
 
9
+ json.password_expired securial_user.password_expired?
10
+
9
11
  json.roles securial_user.roles, partial: "securial/roles/securial_role", as: :securial_role
10
12
 
11
- json.created_at securial_user.created_at
12
- json.updated_at securial_user.updated_at
13
+ json.partial! "securial/shared/timestamps", record: securial_user
13
14
 
14
15
  json.url securial.user_url(securial_user, format: :json)
data/config/routes.rb CHANGED
@@ -28,7 +28,6 @@ Securial::Engine.routes.draw do
28
28
  post "login", to: "sessions#login", as: :login
29
29
  delete "logout", to: "sessions#logout", as: :logout
30
30
  put "refresh", to: "sessions#refresh", as: :refresh_session
31
- delete "revoke", to: "sessions#revoke", as: :revoke_current_session
32
31
  delete "id/:id/revoke", to: "sessions#revoke", as: :revoke_session_by_id
33
32
  delete "revoke_all", to: "sessions#revoke_all", as: :revoke_all_sessions
34
33
  end
@@ -1,13 +1,16 @@
1
1
  class CreateSecurialUsers < ActiveRecord::Migration[8.0]
2
2
  def change
3
3
  create_table :securial_users, id: :string do |t|
4
- t.string :email_address
5
- t.string :password_digest
6
- t.string :first_name
7
- t.string :last_name
8
- t.string :phone
9
- t.string :username
10
- t.string :bio
4
+ t.string :email_address
5
+ t.string :first_name
6
+ t.string :last_name
7
+ t.string :phone
8
+ t.string :username
9
+ t.string :bio
10
+ t.string :password_digest
11
+ t.string :reset_password_token
12
+ t.datetime :reset_password_token_created_at
13
+ t.datetime :password_changed_at
11
14
 
12
15
  t.timestamps
13
16
  end
@@ -14,7 +14,7 @@ Securial.configure do |config|
14
14
  config.log_file_level = :info
15
15
 
16
16
  # Set log level for stdout logger
17
- config.log_stdout_level = :info
17
+ config.log_stdout_level = :debug
18
18
 
19
19
  ##### User Roles
20
20
  ## Set the role for admin users
@@ -26,7 +26,7 @@ Securial.configure do |config|
26
26
  # in the `/securial/superusers` namespace.
27
27
  config.admin_role = :admin
28
28
 
29
- #### Session Configuration
29
+ ##### Session Configuration
30
30
  ## Set the session expiration duration
31
31
  # This is the time after which a session will be considered expired.
32
32
  # After this time, the session will be invalidated and the user
@@ -35,14 +35,6 @@ Securial.configure do |config|
35
35
  # The default is 3 minutes.
36
36
  config.session_expiration_duration = 3.minutes
37
37
 
38
- ## Set the session renewal duration
39
- # This is the time after which a session will be renewed.
40
- # After this time, the session will be renewed and the expiration
41
- # time will be extended. This is useful for keeping users logged in
42
- # without requiring them to log in again. The renewal time is set
43
- # in seconds, minutes, or hours. The default is 3 days.
44
- config.session_renewal_duration = 3.days
45
-
46
38
  ## Set the session secret
47
39
  # This secret is used to sign the session tokens and ensure
48
40
  # that they cannot be tampered with. It is important to keep this
@@ -56,7 +48,7 @@ Securial.configure do |config|
56
48
  # Other options include :hs256, :hs384, and :hs512
57
49
  config.session_algorithm = :hs256
58
50
 
59
- #### Securial Mailer Configuration
51
+ ##### Securial Mailer Configuration
60
52
  ## Set the mailer sender address
61
53
  # This is the email address that will be used as the sender
62
54
  # for all emails sent by the Securial engine. This includes
@@ -64,7 +56,7 @@ Securial.configure do |config|
64
56
  # notifications.
65
57
  config.mailer_sender = "no-reply@example.com"
66
58
 
67
- #### Password configuration
59
+ ##### Password configuration
68
60
  ## Set the password reset email subject
69
61
  # This is the subject line that will be used for the password reset
70
62
  # email. The default is "Password Reset Instructions".
@@ -106,4 +98,53 @@ Securial.configure do |config|
106
98
  # that they cannot be tampered with. It is important to keep this
107
99
  # secret secure and not share it with anyone.
108
100
  config.reset_password_token_secret = "reset_secret"
101
+
102
+ ##### Timestamp Configuration
103
+ ## Set whether to use timestamps in the json responses.
104
+ # The options are:
105
+ # :none - no timestamps will be included in the json responses.
106
+ # :admins_only - the created_at and updated_at timestamps will be included
107
+ # for admin users. This is useful for keeping the json responses clean
108
+ # for regular users while still providing timestamps for admin users.
109
+ # :all - the created_at and updated_at timestamps will be included
110
+ # for all users. This is useful for debugging and development purposes.
111
+ config.timestamps_in_response = Rails.env.production? ? :admins_only : :all
112
+
113
+ ##### Response Configuration
114
+ ## Set the format of the JSON keys in the responses.
115
+ # The options are:
116
+ # :snake_case - the keys will be in snake_case format.
117
+ # :lowerCamelCase - the keys will be in lowerCamelCase format.
118
+ # :upperCamelCase - the keys will be in UpperCamelCase format.
119
+ config.response_keys_format = :snake_case
120
+
121
+ ##### Security Configuration
122
+ ## Set the security headers to be included in the responses.
123
+ # Read more about security headers here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers or in the gem documentation.
124
+ # The options are:
125
+ # :default - the default security headers will be included.
126
+ # :strict - the strict security headers will be included.
127
+ config.security_headers = :strict
128
+ ## set whether to enable request rate limiting
129
+ # This is useful for preventing abuse and denial of service attacks.
130
+ config.rate_limiting_enabled = true
131
+ ## Set the rate limit for requests
132
+ # This is the maximum number of requests that a user can make
133
+ # in a given time period. The default is 60 requests per minute.
134
+ # This is only applied if `rate_limiting_enabled` is set to true.
135
+ config.rate_limit_requests_per_minute = 60
136
+ ## Set the rate limit response status code
137
+ # This is the status code that will be returned when a user exceeds
138
+ # the rate limit. The status code should be a 4xx or 5xx code
139
+ # to indicate an error. Commonly used codes are 429 Too Many Requests
140
+ # or 503 Service Unavailable. The default is 429 Too Many Requests.
141
+ # This is only applied if `rate_limiting_enabled` is set to true.
142
+ config.rate_limit_response_status = 429
143
+ ## Set the rate limit response message
144
+ # This is the message that will be returned when a user exceeds
145
+ # the rate limit. The default is "Too many requests, please try again later."
146
+ # This is only applied if `rate_limiting_enabled` is set to true.
147
+ config.rate_limit_response_message = "Too many requests, please try again later."
148
+
149
+
109
150
  end
@@ -0,0 +1,25 @@
1
+ require "rails/generators"
2
+ require "rake"
3
+
4
+ module Securial
5
+ module Generators
6
+ module Install
7
+ class ViewsGenerator < Rails::Generators::Base
8
+ source_root Securial::Engine.root.join("app", "views", "securial").to_s
9
+ desc "Copies Securial model-related views to your application for customization."
10
+
11
+ def copy_model_views
12
+ Dir.glob(File.join(self.class.source_root, "**/*")).each do |path|
13
+ relative_path = Pathname.new(path).relative_path_from(Pathname.new(self.class.source_root))
14
+
15
+ if File.directory?(path)
16
+ empty_directory "app/views/securial/#{relative_path}"
17
+ else
18
+ copy_file path, "app/views/securial/#{relative_path}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -4,7 +4,6 @@ json.id <%= singular_table_name %>.id
4
4
  json.<%= attr %> <%= singular_table_name %>.<%= attr %>
5
5
  <% end -%>
6
6
 
7
- json.created_at <%= singular_table_name %>.created_at
8
- json.updated_at <%= singular_table_name %>.updated_at
7
+ json.partial! "securial/shared/timestamps", record: <%= singular_table_name %>
9
8
 
10
9
  json.url securial.<%= name.pluralize.downcase %>_url(<%= singular_table_name %>, format: :json)
@@ -0,0 +1,3 @@
1
+ require_relative "errors"
2
+ require_relative "auth_encoder"
3
+ require_relative "session_creator"
@@ -0,0 +1,56 @@
1
+ module Securial
2
+ module Auth
3
+ module AuthEncoder
4
+ class << self
5
+ def encode(session)
6
+ return nil unless session && session.class == Securial::Session
7
+
8
+ base_payload = {
9
+ jti: session.id,
10
+ exp: expiry_duration.from_now.to_i,
11
+ sub: "session-access-token",
12
+ refresh_count: session.refresh_count,
13
+ }
14
+
15
+ session_payload = {
16
+ ip: session.ip_address,
17
+ agent: session.user_agent,
18
+ }
19
+
20
+ payload = base_payload.merge(session_payload)
21
+ begin
22
+ JWT.encode(payload, secret, algorithm, { kid: "hmac" })
23
+ rescue JWT::EncodeError => e
24
+ raise Errors::AuthEncodeError, "Failed to encode session: #{e.message}"
25
+ end
26
+ end
27
+
28
+ def decode(token)
29
+ begin
30
+ decoded = JWT.decode(token, secret, true, { algorithm: algorithm, verify_jti: true, iss: "securial" })
31
+ rescue JWT::DecodeError => e
32
+ raise Securial::Auth::Errors::AuthDecodeError, "Failed to decode session token: #{e.message}"
33
+ end
34
+ decoded.first
35
+ end
36
+
37
+ private
38
+
39
+ def secret
40
+ # Config::Validation.validate_session_secret!(Securial.configuration)
41
+ Securial.configuration.session_secret
42
+ end
43
+
44
+ def algorithm
45
+ # Config::Validation.validate_session_algorithm!(Securial.configuration)
46
+ Securial.configuration.session_algorithm.to_s.upcase
47
+ end
48
+
49
+ def expiry_duration
50
+ # Config::Validation.validate_session_expiry_duration!(Securial.configuration)
51
+ Securial.configuration.session_expiration_duration
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ module Securial
2
+ module Auth
3
+ module Errors
4
+ class BaseAuthError < StandardError
5
+ def backtrace; []; end
6
+ end
7
+
8
+ class AuthEncodeError < BaseAuthError; end
9
+ class AuthDecodeError < BaseAuthError; end
10
+
11
+ class AuthRevokedError < BaseAuthError; end
12
+ class AuthExpiredError < BaseAuthError; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Securial
2
+ module Auth
3
+ module SessionCreator
4
+ class << self
5
+ def create_session(user, request)
6
+ return nil unless user && user.persisted? && request.is_a?(ActionDispatch::Request)
7
+
8
+ user.sessions.create!(
9
+ user_agent: request.user_agent,
10
+ ip_address: request.remote_ip,
11
+ refresh_token: SecureRandom.hex(64),
12
+ last_refreshed_at: Time.current,
13
+ refresh_token_expires_at: 1.week.from_now,
14
+ ).tap do |session|
15
+ Current.session = session
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "./configuration"
2
+ require_relative "./validation"
3
+ require_relative "./errors"
@@ -0,0 +1,62 @@
1
+ require_relative "../helpers/_index"
2
+ module Securial
3
+ module Config
4
+ VALID_SESSION_ENCRYPTION_ALGORITHMS = [:hs256, :hs384, :hs512].freeze
5
+ VALID_TIMESTAMP_OPTIONS = [:all, :admins_only, :none].freeze
6
+ VALID_RESPONSE_KEYS_FORMATS = [:snake_case, :lowerCamelCase, :UpperCamelCase].freeze
7
+ VALID_SECURITY_HEADERS = [:strict, :default, :none].freeze
8
+
9
+ class Configuration
10
+ def self.config_attributes # rubocop:disable Metrics/MethodLength
11
+ {
12
+ log_to_file: !Rails.env.test?,
13
+ log_to_stdout: !Rails.env.test?,
14
+ log_file_level: :info,
15
+ log_stdout_level: :debug,
16
+ admin_role: :admin,
17
+ session_expiration_duration: 3.minutes,
18
+ session_secret: "secret",
19
+ session_algorithm: :hs256,
20
+ mailer_sender: "no-reply@example.com",
21
+ password_reset_email_subject: "SECURIAL: Password Reset Instructions",
22
+ password_min_length: 8,
23
+ password_max_length: 128,
24
+ password_complexity: Securial::RegexHelper::PASSWORD_REGEX,
25
+ password_expires: true,
26
+ password_expires_in: 90.days,
27
+ reset_password_token_expires_in: 2.hours,
28
+ reset_password_token_secret: "reset_secret",
29
+ timestamps_in_response: :all,
30
+ response_keys_format: :snake_case,
31
+ security_headers: :strict,
32
+ rate_limiting_enabled: true,
33
+ rate_limit_requests_per_minute: 60,
34
+ rate_limit_response_status: 429,
35
+ rate_limit_response_message: "Too many requests, please try again later.",
36
+ }
37
+ end
38
+
39
+ def initialize
40
+ self.class.config_attributes.each do |attr, default|
41
+ instance_variable_set("@#{attr}", default)
42
+ end
43
+ validate!
44
+ end
45
+
46
+ config_attributes.each_key do |attr|
47
+ define_method(attr) { instance_variable_get("@#{attr}") }
48
+
49
+ define_method("#{attr}=") do |value|
50
+ instance_variable_set("@#{attr}", value)
51
+ validate!
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def validate!
58
+ Securial::Config::Validation.validate_all!(self)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,20 @@
1
+ module Securial
2
+ module Config
3
+ module Errors
4
+ class BaseConfigError < StandardError
5
+ def backtrace; []; end
6
+ end
7
+
8
+ class ConfigAdminRoleError < BaseConfigError; end
9
+
10
+ class ConfigSessionExpirationDurationError < BaseConfigError; end
11
+ class ConfigSessionAlgorithmError < BaseConfigError; end
12
+ class ConfigSessionSecretError < BaseConfigError; end
13
+
14
+ class ConfigMailerSenderError < BaseConfigError; end
15
+ class ConfigPasswordError < BaseConfigError; end
16
+ class ConfigResponseError < BaseConfigError; end
17
+ class ConfigSecurityError < BaseConfigError; end
18
+ end
19
+ end
20
+ end