rails_auth_generator 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +129 -23
- data/README_NEW.md +0 -59
- data/lib/generators/auth/auth_generator.rb +57 -68
- data/lib/generators/auth/templates/concerns/authenticatable.rb +77 -0
- data/lib/generators/auth/templates/controllers/auth_controller.rb +84 -0
- data/lib/generators/auth/templates/{users_controller.rb → controllers/users_controller.rb} +1 -1
- data/lib/generators/auth/templates/initializers/rails_auth_generator.rb +6 -0
- data/lib/generators/auth/templates/migrations/create_refresh_token.rb +14 -0
- data/lib/generators/auth/templates/models/refresh_token.rb +9 -0
- data/lib/generators/auth/templates/models/user.rb +33 -0
- data/lib/generators/auth/templates/serializers/user_serializer.rb +3 -0
- data/lib/rails_auth_generator/configuration.rb +15 -0
- data/lib/rails_auth_generator/version.rb +1 -1
- data/lib/rails_auth_generator.rb +10 -0
- metadata +53 -6
- data/lib/generators/auth/templates/auth_controller.rb +0 -27
- data/lib/generators/auth/templates/user.rb +0 -15
- data/lib/generators/auth/templates/user_serializer.rb +0 -3
- /data/lib/generators/auth/templates/{password_resets_controller.rb → controllers/password_resets_controller.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf69a30ca1854528205157aa7d45cf0138f78b50dc87b86e3db9a803e1368443
|
4
|
+
data.tar.gz: de809c1c82d18fae7af04964caf5d07c26db2178cd4e4cd40407a7debaf78d7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0203aa88a66a391c60837ecd69dee7a69b813b407e840256cdbd891e10b3f29a354450811dcbeaca3f50588e48b369aba92b41c96cf781ce130aae1630a35717
|
7
|
+
data.tar.gz: c3fbeee8f0b7f1ba781d14872b43e33571826866c868a51df2c5e316ac2471a6b2e564555bc854be9f0691076f5e795125e3f9457541449a87bb63941cd2c086
|
data/README.md
CHANGED
@@ -1,43 +1,149 @@
|
|
1
1
|
# RailsAuthGenerator
|
2
2
|
|
3
|
-
|
3
|
+
**RailsAuthGenerator** is a Rails generator that scaffolds a **JWT-based authentication system** with user management, password resets, refresh token rotation, and secure cookie handling. It saves you weeks of setup by providing all the models, controllers, serializers, and mailers you need for a robust, production-ready authentication flow.
|
4
4
|
|
5
|
-
|
5
|
+
---
|
6
6
|
|
7
|
-
##
|
7
|
+
## ✨ Features
|
8
8
|
|
9
|
-
|
9
|
+
- 🔑 **JWT Authentication**
|
10
|
+
- Access tokens (short-lived, default 15 min)
|
11
|
+
- Refresh tokens (stored securely in HttpOnly cookies)
|
12
|
+
- Token rotation + reuse detection
|
13
|
+
- Logout everywhere
|
14
|
+
- 👤 **User management**
|
15
|
+
- User model with secure password
|
16
|
+
- Role support (admin, user)
|
17
|
+
- ✉️ **Password reset**
|
18
|
+
- Password reset tokens sent via email
|
19
|
+
- 🛠️ **Rails Generators**
|
20
|
+
- User model + migrations
|
21
|
+
- Auth controllers (`auth`, `users`, `password_resets`)
|
22
|
+
- Serializers and mailers
|
23
|
+
- ⚡ Works with **Rails 6.0+**
|
10
24
|
|
11
|
-
|
25
|
+
---
|
12
26
|
|
13
|
-
|
14
|
-
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
15
|
-
```
|
27
|
+
## 📦 Installation
|
16
28
|
|
17
|
-
|
29
|
+
Add this line to your application's Gemfile:
|
30
|
+
`gem 'rails_auth_generator', '~> 0.2.1'`
|
18
31
|
|
19
|
-
|
20
|
-
|
21
|
-
```
|
32
|
+
and then run:
|
33
|
+
`bundle install`
|
22
34
|
|
23
|
-
|
35
|
+
Or install it manually:
|
36
|
+
`gem install rails_auth_generator`
|
24
37
|
|
25
|
-
|
38
|
+
If you want the latest version from GitHub:
|
39
|
+
`gem 'rails_auth_generator', git: 'https://github.com/Zeyad-Hassan-1/authJWT.git'`
|
26
40
|
|
27
|
-
|
41
|
+
---
|
28
42
|
|
29
|
-
|
43
|
+
## 🚀 Usage
|
30
44
|
|
31
|
-
|
45
|
+
Generate the full authentication system:
|
46
|
+
`rails generate auth`
|
32
47
|
|
33
|
-
|
48
|
+
Then run:
|
49
|
+
`bundle install`
|
50
|
+
`rails db:migrate`
|
34
51
|
|
35
|
-
|
52
|
+
This scaffolds:
|
53
|
+
- User model & migrations
|
54
|
+
- Controllers for authentication, users, and password resets
|
55
|
+
- Mailers for password reset
|
56
|
+
- Serializers for user data
|
36
57
|
|
37
|
-
|
58
|
+
You can freely customize the generated files to match your app’s requirements.
|
38
59
|
|
39
|
-
|
60
|
+
---
|
40
61
|
|
41
|
-
##
|
62
|
+
## 🔧 Additional Setup
|
42
63
|
|
43
|
-
|
64
|
+
### 1. Enable CORS
|
65
|
+
Uncomment the CORS config in `config/initializers/cors.rb` if building an API:
|
66
|
+
|
67
|
+
`Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
68
|
+
allow do
|
69
|
+
origins '*'
|
70
|
+
resource '*',
|
71
|
+
headers: :any,
|
72
|
+
methods: [:get, :post, :put, :patch, :delete, :options, :head],
|
73
|
+
credentials: true
|
74
|
+
end
|
75
|
+
end`
|
76
|
+
|
77
|
+
### 2. Set JWT Secret
|
78
|
+
Edit your Rails credentials:
|
79
|
+
`VISUAL="code --wait" bin/rails credentials:edit`
|
80
|
+
|
81
|
+
Add:
|
82
|
+
|
83
|
+
`jwt:
|
84
|
+
secret: <your_generated_secret>`
|
85
|
+
|
86
|
+
Generate a secret key:
|
87
|
+
`rails secret`
|
88
|
+
|
89
|
+
Replace `<your_generated_secret>` with the generated key.
|
90
|
+
|
91
|
+
---
|
92
|
+
|
93
|
+
## 📚 API Overview
|
94
|
+
|
95
|
+
| Route | Method | Description |
|
96
|
+
|-------------------|--------|-------------|
|
97
|
+
| `/signup` | POST | Create a new user |
|
98
|
+
| `/login` | POST | Authenticate user, return JWT + set refresh cookie |
|
99
|
+
| `/me` | GET | Get current logged-in user |
|
100
|
+
| `/refresh` | POST | Rotate refresh token + issue new JWT |
|
101
|
+
| `/logout` | DELETE | Revoke refresh token + clear cookie |
|
102
|
+
| `/password_resets`| POST | Request a password reset |
|
103
|
+
| `/password_resets` | PUT | Reset password with token |
|
104
|
+
|
105
|
+
---
|
106
|
+
|
107
|
+
## 🧪 Example Usage
|
108
|
+
|
109
|
+
1. Sign up:
|
110
|
+
`curl -X POST http://localhost:3000/signup -H "Content-Type: application/json" -d '{"user": {"email":"test@example.com","password":"secret123"}}'`
|
111
|
+
|
112
|
+
2. Login:
|
113
|
+
`curl -X POST http://localhost:3000/login -H "Content-Type: application/json" -d '{"email":"test@example.com","password":"secret123"}'`
|
114
|
+
➡️ Returns `{ "token": "...", "user": {...} }`
|
115
|
+
Refresh token is stored in an **HttpOnly cookie**.
|
116
|
+
|
117
|
+
3. Access protected route:
|
118
|
+
`curl -H "Authorization: Bearer <your_token>" http://localhost:3000/me`
|
119
|
+
|
120
|
+
4. Refresh token:
|
121
|
+
`curl -X POST http://localhost:3000/refresh`
|
122
|
+
➡️ Returns new access token, rotates refresh cookie.
|
123
|
+
|
124
|
+
5. Logout:
|
125
|
+
`curl -X DELETE http://localhost:3000/logout`
|
126
|
+
➡️ Revokes refresh token + clears cookie.
|
127
|
+
|
128
|
+
---
|
129
|
+
|
130
|
+
## 🛡️ Security Defaults
|
131
|
+
|
132
|
+
- Access tokens expire after **15 minutes**
|
133
|
+
- Refresh tokens expire after **7 days**
|
134
|
+
- Refresh tokens are **rotated on every use**
|
135
|
+
- Reused tokens trigger **global logout**
|
136
|
+
|
137
|
+
---
|
138
|
+
|
139
|
+
## 🤝 Contributing
|
140
|
+
|
141
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/Zeyad-Hassan-1/authJWT](https://github.com/Zeyad-Hassan-1/authJWT).
|
142
|
+
|
143
|
+
This project follows a [Code of Conduct](CODE_OF_CONDUCT.md). Please respect it in all interactions.
|
144
|
+
|
145
|
+
---
|
146
|
+
|
147
|
+
## 📄 License
|
148
|
+
|
149
|
+
This gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
data/README_NEW.md
CHANGED
@@ -1,59 +0,0 @@
|
|
1
|
-
# RailsAuthGenerator
|
2
|
-
|
3
|
-
RailsAuthGenerator provides Rails generators for authentication, user management, password resets, and mailers, streamlining the setup of secure user authentication in Rails applications. It helps you quickly scaffold all necessary models, controllers, mailers, and migrations for a robust authentication system.
|
4
|
-
|
5
|
-
## Features
|
6
|
-
|
7
|
-
- User model and migration generator
|
8
|
-
- Authentication controller and password reset controller
|
9
|
-
- User serializer for API responses
|
10
|
-
- Mailers for sending token to reset password
|
11
|
-
- Easy integration with Rails 6.0+
|
12
|
-
|
13
|
-
## Installation
|
14
|
-
|
15
|
-
Add this line to your application's Gemfile:
|
16
|
-
|
17
|
-
```ruby
|
18
|
-
bundle add rails_auth_generator
|
19
|
-
```
|
20
|
-
|
21
|
-
Or install it manually:
|
22
|
-
|
23
|
-
```bash
|
24
|
-
gem install rails_auth_generator
|
25
|
-
```
|
26
|
-
|
27
|
-
If you want to use the latest version from GitHub:
|
28
|
-
|
29
|
-
```ruby
|
30
|
-
gem 'rails_auth_generator', git: 'https://github.com/Zeyad-Hassan-1/authJWT.git'
|
31
|
-
```
|
32
|
-
|
33
|
-
## Usage
|
34
|
-
|
35
|
-
Run the generator to scaffold authentication features:
|
36
|
-
|
37
|
-
```bash
|
38
|
-
rails generate auth
|
39
|
-
```
|
40
|
-
|
41
|
-
This will create:
|
42
|
-
- User model and migration
|
43
|
-
- Authentication controllers (auth, password resets, users)
|
44
|
-
- Mailers for sending token to reset password
|
45
|
-
- Serializers for user data
|
46
|
-
|
47
|
-
You can customize the generated files as needed for your application.
|
48
|
-
|
49
|
-
## Contributing
|
50
|
-
|
51
|
-
Bug reports and pull requests are welcome on GitHub at [https://github.com/Zeyad-Hassan-1/authJWT](https://github.com/Zeyad-Hassan-1/authJWT). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
|
52
|
-
|
53
|
-
## License
|
54
|
-
|
55
|
-
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
56
|
-
|
57
|
-
## Code of Conduct
|
58
|
-
|
59
|
-
Everyone interacting in the RailsAuthGenerator project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
@@ -2,99 +2,88 @@ class AuthGenerator < Rails::Generators::Base
|
|
2
2
|
include Rails::Generators::Migration
|
3
3
|
source_root File.expand_path("templates", __dir__)
|
4
4
|
|
5
|
-
|
6
|
-
insert_into_file "Gemfile", after: /^source ['"].*['"]\n/ do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
def modify_gemfile
|
6
|
+
insert_into_file "Gemfile", after: /^source ['"].*['"]\n/ do
|
7
|
+
<<~RUBY
|
8
|
+
gem 'bcrypt', '~> 3.1', '>= 3.1.12'
|
9
|
+
gem 'jwt', '~> 2.5'
|
10
|
+
gem 'rack-cors'
|
11
|
+
gem 'active_model_serializers', '~> 0.10.12'
|
12
|
+
RUBY
|
13
|
+
end
|
13
14
|
end
|
14
15
|
|
16
|
+
def modify_application_rb
|
17
|
+
insert_into_file "config/application.rb", after: "config.api_only = true\n" do
|
18
|
+
<<~RUBY
|
19
|
+
config.middleware.use ActionDispatch::Cookies
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
|
15
23
|
end
|
16
24
|
|
17
25
|
def add_routes
|
18
26
|
route <<~RUBY
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
27
|
+
# config/routes.rb
|
28
|
+
post '/login', to: 'auth#login'
|
29
|
+
post '/refresh', to: 'auth#refresh'
|
30
|
+
post '/logout', to: 'auth#logout'
|
31
|
+
post '/signup', to: 'users#create'
|
32
|
+
get '/me', to: 'users#me'
|
33
|
+
resources :password_resets, only: [:create] do
|
34
|
+
collection do
|
35
|
+
put '/', to: 'password_resets#update' # PUT /password_resets
|
36
|
+
end
|
26
37
|
end
|
27
|
-
|
28
|
-
|
29
|
-
post '/users/:id/make_admin', to: 'users#make_admin'
|
38
|
+
# Admin routes
|
39
|
+
post '/users/:id/make_admin', to: 'users#make_admin'
|
30
40
|
RUBY
|
31
41
|
end
|
32
42
|
|
33
43
|
def create_auth_files
|
34
|
-
template "auth_controller.rb", "app/controllers/auth_controller.rb"
|
35
|
-
template "user_serializer.rb", "app/serializers/user_serializer.rb"
|
36
|
-
template "users_controller.rb", "app/controllers/users_controller.rb"
|
37
|
-
template "password_resets_controller.rb", "app/controllers/password_resets_controller.rb"
|
38
|
-
template "user.rb", "app/models/user.rb"
|
44
|
+
template "controllers/auth_controller.rb", "app/controllers/auth_controller.rb"
|
45
|
+
template "serializers/user_serializer.rb", "app/serializers/user_serializer.rb"
|
46
|
+
template "controllers/users_controller.rb", "app/controllers/users_controller.rb"
|
47
|
+
template "controllers/password_resets_controller.rb", "app/controllers/password_resets_controller.rb"
|
48
|
+
template "models/user.rb", "app/models/user.rb"
|
49
|
+
template "models/refresh_token.rb", "app/models/refresh_token.rb"
|
39
50
|
template "mailers/user_mailer.rb", "app/mailers/user_mailer.rb"
|
40
51
|
template "mailers/application_mailer.rb", "app/mailers/application_mailer.rb"
|
52
|
+
template "concerns/authenticatable.rb", "app/controllers/concerns/authenticatable.rb"
|
53
|
+
template "initializers/rails_auth_generator.rb", "config/initializers/rails_auth_generator.rb"
|
41
54
|
end
|
42
55
|
|
43
56
|
def modify_application_controller
|
44
57
|
inject_into_class "app/controllers/application_controller.rb", "ApplicationController" do
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
def encode_token(payload)
|
49
|
-
# Add admin status to the payload if user is admin
|
50
|
-
payload[:admin] = @user.admin? if @user.is_a?(User)
|
51
|
-
JWT.encode(payload, 'hellomars1211')
|
52
|
-
end
|
58
|
+
" include Authenticatable\n"
|
59
|
+
end
|
60
|
+
end
|
53
61
|
|
54
|
-
def decoded_token
|
55
|
-
header = request.headers['Authorization']
|
56
|
-
if header
|
57
|
-
token = header.split(" ")[1]
|
58
|
-
begin
|
59
|
-
JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256')
|
60
|
-
rescue JWT::DecodeError
|
61
|
-
nil
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
63
|
+
def self.next_migration_number(dirname)
|
64
|
+
@prev_migration_nr ||= Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
65
|
+
@prev_migration_nr += 1
|
66
|
+
@prev_migration_nr.to_s
|
67
|
+
end
|
72
68
|
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
def copy_migration
|
70
|
+
migration_template "migrations/create_user.rb", "db/migrate/create_users.rb"
|
71
|
+
migration_template "migrations/create_refresh_token.rb", "db/migrate/create_refresh_tokens.rb"
|
72
|
+
end
|
76
73
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
74
|
+
def enable_cors
|
75
|
+
insert_into_file "config/application.rb"do
|
76
|
+
<<~RUBY
|
77
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
78
|
+
allow do
|
79
|
+
origins "example.com"
|
82
80
|
|
83
|
-
|
84
|
-
|
85
|
-
|
81
|
+
resource "*",
|
82
|
+
headers: :any,
|
83
|
+
methods: [:get, :post, :put, :patch, :delete, :options, :head]
|
86
84
|
end
|
87
85
|
end
|
88
|
-
|
89
86
|
RUBY
|
90
87
|
end
|
91
88
|
end
|
92
|
-
|
93
|
-
def self.next_migration_number(dirname)
|
94
|
-
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
95
|
-
end
|
96
|
-
|
97
|
-
def copy_migration
|
98
|
-
migration_template "migrations/create_user.rb", "db/migrate/create_users.rb"
|
99
|
-
end
|
100
89
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Authenticatable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include ActionController::Cookies
|
8
|
+
before_action :authorized
|
9
|
+
|
10
|
+
SECRET_KEY = RailsAuthGenerator.configuration.jwt_secret
|
11
|
+
|
12
|
+
def encode_token(payload, exp = RailsAuthGenerator.configuration.access_token_expiry.from_now)
|
13
|
+
|
14
|
+
|
15
|
+
payload[:exp] = exp.to_i
|
16
|
+
payload[:admin] = @user.admin? if @user.is_a?(User) && RailsAuthGenerator.configuration.enable_roles
|
17
|
+
|
18
|
+
token = JWT.encode(payload, SECRET_KEY)
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def decoded_token
|
23
|
+
header = request.headers['Authorization']
|
24
|
+
return nil unless header
|
25
|
+
|
26
|
+
token = header.split(" ")[1]
|
27
|
+
puts "🔍 DECODE_TOKEN DEBUG: Token: #{token}"
|
28
|
+
|
29
|
+
begin
|
30
|
+
decoded = JWT.decode(token, SECRET_KEY, true, { algorithm: 'HS256', verify_expiration: true })
|
31
|
+
puts " Token valid, expires at: #{Time.at(decoded[0]['exp'])}" if decoded[0]['exp']
|
32
|
+
decoded
|
33
|
+
rescue JWT::ExpiredSignature => e
|
34
|
+
puts " ❌ Token expired: #{e.message}"
|
35
|
+
@token_expired = true
|
36
|
+
nil
|
37
|
+
rescue JWT::DecodeError => e
|
38
|
+
puts " ❌ Token decode error: #{e.message}"
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def current_user
|
44
|
+
return @current_user if defined?(@current_user)
|
45
|
+
|
46
|
+
if decoded_token
|
47
|
+
user_id = decoded_token[0]['user_id']
|
48
|
+
@current_user = User.find_by(id: user_id)
|
49
|
+
else
|
50
|
+
@current_user = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def current_admin
|
55
|
+
current_user && (current_user.admin? || (decoded_token && decoded_token[0]['admin']))
|
56
|
+
end
|
57
|
+
|
58
|
+
def authorized
|
59
|
+
# Check if token is expired first
|
60
|
+
if @token_expired
|
61
|
+
render json: { error: 'Token has expired' }, status: :unauthorized
|
62
|
+
return false
|
63
|
+
end
|
64
|
+
|
65
|
+
unless current_user
|
66
|
+
render json: { message: 'Please log in' }, status: :unauthorized
|
67
|
+
return false
|
68
|
+
end
|
69
|
+
|
70
|
+
true
|
71
|
+
end
|
72
|
+
|
73
|
+
def admin_authorized
|
74
|
+
authorized && current_admin
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "digest"
|
2
|
+
|
3
|
+
class AuthController < ApplicationController
|
4
|
+
skip_before_action :authorized, only: [:login, :refresh, :logout]
|
5
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found
|
6
|
+
|
7
|
+
def login
|
8
|
+
user = User.find_by!(username: params[:username])
|
9
|
+
if user.authenticate(params[:password])
|
10
|
+
access_token = encode_token({ user_id: user.id }, 15.minutes.from_now)
|
11
|
+
refresh_raw = user.generate_refresh_token
|
12
|
+
|
13
|
+
set_refresh_cookie(refresh_raw, 7.days.from_now)
|
14
|
+
|
15
|
+
render json: {
|
16
|
+
user: UserSerializer.new(user),
|
17
|
+
access_token: access_token
|
18
|
+
# (we're NOT returning refresh in JSON for security)
|
19
|
+
}, status: :ok
|
20
|
+
else
|
21
|
+
render json: { error: "Invalid credentials" }, status: :unauthorized
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def refresh
|
26
|
+
raw = cookies.encrypted[:refresh_token] || params[:refresh_token]
|
27
|
+
return render json: { error: "missing refresh token" }, status: :unauthorized if raw.blank?
|
28
|
+
|
29
|
+
digest = Digest::SHA256.hexdigest(raw)
|
30
|
+
rt = RefreshToken.find_by(token_digest: digest)
|
31
|
+
|
32
|
+
if rt.nil? || rt.revoked_at.present? || rt.expires_at.past?
|
33
|
+
rt&.user&.revoke_all_refresh_tokens!
|
34
|
+
cookies.delete(:refresh_token)
|
35
|
+
return render json: { error: "Invalid or reused refresh token. Logged out everywhere." }, status: :unauthorized
|
36
|
+
end
|
37
|
+
|
38
|
+
# Issue new access + refresh token
|
39
|
+
new_access_token = encode_token({ user_id: rt.user_id })
|
40
|
+
new_refresh_token = rt.user.generate_refresh_token
|
41
|
+
|
42
|
+
# revoke the old token
|
43
|
+
rt.update!(revoked_at: Time.current)
|
44
|
+
|
45
|
+
# 🔑 store new refresh token in HttpOnly cookie
|
46
|
+
set_refresh_cookie(new_refresh_token, 7.days.from_now)
|
47
|
+
|
48
|
+
render json: { access_token: new_access_token }, status: :ok
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
def logout
|
54
|
+
raw = cookies.encrypted[:refresh_token] || params[:refresh_token]
|
55
|
+
if raw.present?
|
56
|
+
digest = Digest::SHA256.hexdigest(raw)
|
57
|
+
RefreshToken.find_by(token_digest: digest)&.destroy
|
58
|
+
cookies.delete(:refresh_token)
|
59
|
+
end
|
60
|
+
render json: { message: "Logged out" }, status: :ok
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def set_refresh_cookie(raw_token, expires_at)
|
67
|
+
cookies.encrypted[:refresh_token] = {
|
68
|
+
value: raw_token,
|
69
|
+
httponly: true,
|
70
|
+
secure: Rails.env.production?,
|
71
|
+
same_site: :lax,
|
72
|
+
expires: expires_at
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def login_params
|
77
|
+
params.permit(:username, :password)
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_record_not_found(e)
|
81
|
+
render json: { message: "User doesn't exist" }, status: :unauthorized
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateRefreshTokens < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :refresh_tokens do |t|
|
4
|
+
t.references :user, null: false, foreign_key: true
|
5
|
+
t.string :token_digest, null: false
|
6
|
+
t.datetime :expires_at, null: false
|
7
|
+
t.datetime :revoked_at
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
|
12
|
+
add_index :refresh_tokens, :token_digest, unique: true
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "digest"
|
2
|
+
|
3
|
+
class User < ApplicationRecord
|
4
|
+
has_secure_password
|
5
|
+
has_many :refresh_tokens, dependent: :destroy
|
6
|
+
validates :username, uniqueness: true
|
7
|
+
|
8
|
+
# returns the RAW token (client uses this), stores only SHA256 digest
|
9
|
+
def generate_refresh_token
|
10
|
+
raw = SecureRandom.hex(64)
|
11
|
+
digest = Digest::SHA256.hexdigest(raw)
|
12
|
+
refresh_tokens.create!(
|
13
|
+
token_digest: digest,
|
14
|
+
expires_at: 7.days.from_now
|
15
|
+
)
|
16
|
+
raw
|
17
|
+
end
|
18
|
+
|
19
|
+
# revoke all tokens for this user
|
20
|
+
def revoke_all_refresh_tokens!
|
21
|
+
refresh_tokens.update_all(revoked_at: Time.current)
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_password_reset_token!
|
25
|
+
self.reset_token = SecureRandom.urlsafe_base64
|
26
|
+
self.reset_sent_at = Time.now.utc
|
27
|
+
save!(validate: false) # Skip validations for password reset
|
28
|
+
end
|
29
|
+
|
30
|
+
def password_reset_expired?
|
31
|
+
reset_sent_at < 1.hour.ago
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module RailsAuthGenerator
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :jwt_secret,
|
4
|
+
:access_token_expiry,
|
5
|
+
:refresh_token_expiry,
|
6
|
+
:enable_roles
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@jwt_secret = nil
|
10
|
+
@access_token_expiry = 15.minutes
|
11
|
+
@refresh_token_expiry = 30.days
|
12
|
+
@enable_roles = false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/rails_auth_generator.rb
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
require "rails_auth_generator/version"
|
2
|
+
require "rails_auth_generator/configuration"
|
2
3
|
|
3
4
|
module RailsAuthGenerator
|
4
5
|
class Error < StandardError; end
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :configuration
|
9
|
+
|
10
|
+
def configure
|
11
|
+
self.configuration ||= Configuration.new
|
12
|
+
yield(configuration)
|
13
|
+
end
|
14
|
+
end
|
5
15
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rails_auth_generator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zeyad Hassan
|
@@ -29,6 +29,48 @@ dependencies:
|
|
29
29
|
- - "<"
|
30
30
|
- !ruby/object:Gem::Version
|
31
31
|
version: '9.0'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: bcrypt
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - "~>"
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '3.1'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - "~>"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.1'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: devise
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - "~>"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '4.9'
|
53
|
+
type: :runtime
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '4.9'
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: jwt
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.5'
|
67
|
+
type: :runtime
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - "~>"
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '2.5'
|
32
74
|
email:
|
33
75
|
- " studying.zezo@gmail.com"
|
34
76
|
executables: []
|
@@ -46,15 +88,20 @@ files:
|
|
46
88
|
- bin/setup
|
47
89
|
- lib/generators/auth/USAGE
|
48
90
|
- lib/generators/auth/auth_generator.rb
|
49
|
-
- lib/generators/auth/templates/
|
91
|
+
- lib/generators/auth/templates/concerns/authenticatable.rb
|
92
|
+
- lib/generators/auth/templates/controllers/auth_controller.rb
|
93
|
+
- lib/generators/auth/templates/controllers/password_resets_controller.rb
|
94
|
+
- lib/generators/auth/templates/controllers/users_controller.rb
|
95
|
+
- lib/generators/auth/templates/initializers/rails_auth_generator.rb
|
50
96
|
- lib/generators/auth/templates/mailers/application_mailer.rb
|
51
97
|
- lib/generators/auth/templates/mailers/user_mailer.rb
|
98
|
+
- lib/generators/auth/templates/migrations/create_refresh_token.rb
|
52
99
|
- lib/generators/auth/templates/migrations/create_user.rb
|
53
|
-
- lib/generators/auth/templates/
|
54
|
-
- lib/generators/auth/templates/user.rb
|
55
|
-
- lib/generators/auth/templates/user_serializer.rb
|
56
|
-
- lib/generators/auth/templates/users_controller.rb
|
100
|
+
- lib/generators/auth/templates/models/refresh_token.rb
|
101
|
+
- lib/generators/auth/templates/models/user.rb
|
102
|
+
- lib/generators/auth/templates/serializers/user_serializer.rb
|
57
103
|
- lib/rails_auth_generator.rb
|
104
|
+
- lib/rails_auth_generator/configuration.rb
|
58
105
|
- lib/rails_auth_generator/version.rb
|
59
106
|
homepage: https://github.com/Zeyad-Hassan-1/authJWT.git
|
60
107
|
licenses:
|
@@ -1,27 +0,0 @@
|
|
1
|
-
class AuthController < ApplicationController
|
2
|
-
skip_before_action :authorized, only: [:login]
|
3
|
-
rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found
|
4
|
-
|
5
|
-
def login
|
6
|
-
@user = User.find_by!(username: login_params[:username])
|
7
|
-
if @user.authenticate(login_params[:password])
|
8
|
-
@token = encode_token(user_id: @user.id)
|
9
|
-
render json: {
|
10
|
-
user: UserSerializer.new(@user),
|
11
|
-
token: @token
|
12
|
-
}, status: :accepted
|
13
|
-
else
|
14
|
-
render json: {message: 'Incorrect password'}, status: :unauthorized
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def login_params
|
21
|
-
params.permit(:username, :password)
|
22
|
-
end
|
23
|
-
|
24
|
-
def handle_record_not_found(e)
|
25
|
-
render json: { message: "User doesn't exist" }, status: :unauthorized
|
26
|
-
end
|
27
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
class User < ApplicationRecord
|
2
|
-
has_secure_password
|
3
|
-
validates :username, uniqueness: true
|
4
|
-
# validates :email, presence: true, uniqueness: true
|
5
|
-
|
6
|
-
def generate_password_reset_token!
|
7
|
-
self.reset_token = SecureRandom.urlsafe_base64
|
8
|
-
self.reset_sent_at = Time.now.utc
|
9
|
-
save!(validate: false) # Skip validations for password reset
|
10
|
-
end
|
11
|
-
|
12
|
-
def password_reset_expired?
|
13
|
-
reset_sent_at < 1.hour.ago
|
14
|
-
end
|
15
|
-
end
|
File without changes
|