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.
- checksums.yaml +4 -4
- data/README.md +50 -14
- data/app/controllers/concerns/securial/identity.rb +4 -12
- data/app/controllers/securial/sessions_controller.rb +20 -8
- data/app/models/concerns/securial/password_resettable.rb +15 -0
- data/app/models/securial/session.rb +2 -2
- data/app/views/securial/roles/_securial_role.json.jbuilder +1 -2
- data/app/views/securial/sessions/_session.json.jbuilder +1 -2
- data/app/views/securial/shared/_timestamps.json.jbuilder +8 -0
- data/app/views/securial/users/_securial_user.json.jbuilder +3 -2
- data/config/routes.rb +0 -1
- data/db/migrate/20250517155521_create_securial_users.rb +10 -7
- data/lib/generators/securial/install/templates/securial_initializer.erb +53 -12
- data/lib/generators/securial/install/views_generastor.rb +25 -0
- data/lib/generators/securial/jbuilder/templates/_resource.json.erb +1 -2
- data/lib/securial/auth/_index.rb +3 -0
- data/lib/securial/auth/auth_encoder.rb +56 -0
- data/lib/securial/auth/errors.rb +15 -0
- data/lib/securial/auth/session_creator.rb +21 -0
- data/lib/securial/config/_index.rb +3 -0
- data/lib/securial/config/configuration.rb +62 -0
- data/lib/securial/config/errors.rb +20 -0
- data/lib/securial/config/validation.rb +249 -0
- data/lib/securial/engine.rb +41 -38
- data/lib/securial/helpers/_index.rb +2 -0
- data/lib/securial/helpers/regex_helper.rb +7 -7
- data/lib/securial/inspectors/_index.rb +1 -0
- data/lib/securial/inspectors/route_inspector.rb +52 -0
- data/lib/securial/key_transformer.rb +32 -0
- data/lib/securial/logger.rb +2 -2
- data/lib/securial/middleware/_index.rb +3 -0
- data/lib/securial/middleware/transform_request_keys.rb +33 -0
- data/lib/securial/middleware/transform_response_keys.rb +45 -0
- data/lib/securial/rack_attack.rb +48 -0
- data/lib/securial/version.rb +1 -1
- data/lib/securial.rb +7 -72
- metadata +37 -154
- data/app/views/securial/passwords/_password.json.jbuilder +0 -2
- data/app/views/securial/passwords/index.json.jbuilder +0 -1
- data/app/views/securial/passwords/show.json.jbuilder +0 -1
- data/db/migrate/20250524210207_add_password_reset_fields_to_securial_users.rb +0 -6
- data/lib/securial/configuration.rb +0 -35
- data/lib/securial/errors/config_errors.rb +0 -12
- data/lib/securial/errors/session_errors.rb +0 -6
- data/lib/securial/helpers/auth_helper.rb +0 -46
- data/lib/securial/route_inspector.rb +0 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1628d056fdb0a93bc954107766c8f5ed2975d4690c58dbdbe56a0d45e63eefa3
|
4
|
+
data.tar.gz: 166ce5d80dc63642239a0f6c26702e4bbca469c2f7d886d322aa07e119ef8928
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 997ba8dc253bf3772ef806e9c81cce2fb7a7b0dd263527f0896badf74e5ea09ab5560ca804162cde01441054ac5b5d0fca9c39dfc2607cf18086ec6bb4667af2
|
7
|
+
data.tar.gz: b7ff4072c37645e9dc6f1bd5551bb47ec829cbefd412221bcdf13e5a2e788450243c1e7853e918248a2b1faaea61aec40122ddea32f1ef80a1ae013367d9d349
|
data/README.md
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-

|
2
|
-
|
3
|
-
---
|
4
1
|
# Securial
|
5
2
|
|
6
3
|
[](https://rubygems.org/gems/securial)
|
@@ -10,6 +7,16 @@
|
|
10
7
|
[](https://github.com/alybadawy/securial/actions)
|
11
8
|
[](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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
-
|
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
|
-
$
|
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
|
+

|
@@ -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 =
|
35
|
+
decoded_token = Securial::Auth::AuthEncoder.decode(token)
|
36
36
|
Current.session = Session.find_by!(id: decoded_token["jti"], revoked: false)
|
37
|
-
rescue
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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::
|
19
|
-
raise Securial::
|
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.
|
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.
|
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.
|
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
|
5
|
-
t.string
|
6
|
-
t.string
|
7
|
-
t.string
|
8
|
-
t.string
|
9
|
-
t.string
|
10
|
-
t.string
|
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 = :
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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,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,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
|