jwt_auth_engine 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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +37 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +422 -0
  5. data/app/controllers/concerns/jwt_auth_engine/authenticatable.rb +52 -0
  6. data/app/controllers/concerns/jwt_auth_engine/rescuable.rb +23 -0
  7. data/app/controllers/concerns/jwt_auth_engine/response_renderable.rb +29 -0
  8. data/app/controllers/concerns/jwt_auth_engine/serializable.rb +21 -0
  9. data/app/controllers/concerns/jwt_auth_engine/tokenizable.rb +22 -0
  10. data/app/controllers/jwt_auth_engine/application_controller.rb +21 -0
  11. data/app/controllers/jwt_auth_engine/passwords_controller.rb +26 -0
  12. data/app/controllers/jwt_auth_engine/ping_controller.rb +21 -0
  13. data/app/controllers/jwt_auth_engine/profiles_controller.rb +15 -0
  14. data/app/controllers/jwt_auth_engine/registrations_controller.rb +33 -0
  15. data/app/controllers/jwt_auth_engine/sessions_controller.rb +41 -0
  16. data/app/controllers/jwt_auth_engine/tokens_controller.rb +21 -0
  17. data/app/services/jwt_auth_engine/base_service.rb +26 -0
  18. data/app/services/jwt_auth_engine/change_password_service.rb +48 -0
  19. data/app/services/jwt_auth_engine/login_service.rb +46 -0
  20. data/app/services/jwt_auth_engine/refresh_token_service.rb +35 -0
  21. data/app/services/jwt_auth_engine/signup_service.rb +23 -0
  22. data/app/services/jwt_auth_engine/token_service.rb +72 -0
  23. data/config/routes.rb +47 -0
  24. data/lib/generators/jwt_auth_engine/install/install_generator.rb +95 -0
  25. data/lib/generators/jwt_auth_engine/install/templates/add_jwt_auth_engine_columns_migration.rb.tt +11 -0
  26. data/lib/generators/jwt_auth_engine/install/templates/auth_model_concern.rb.tt +32 -0
  27. data/lib/generators/jwt_auth_engine/install/templates/jwt_auth_engine_initializer.rb.tt +22 -0
  28. data/lib/jwt_auth_engine/configuration.rb +50 -0
  29. data/lib/jwt_auth_engine/constants.rb +16 -0
  30. data/lib/jwt_auth_engine/engine.rb +10 -0
  31. data/lib/jwt_auth_engine/errors.rb +14 -0
  32. data/lib/jwt_auth_engine/version.rb +5 -0
  33. data/lib/jwt_auth_engine.rb +79 -0
  34. data/sig/jwt_auth_engine.rbs +4 -0
  35. metadata +137 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b8a125215a3bf059193a53e3da267d0f1a184fb4bf2fd1164496cdbd50f19771
4
+ data.tar.gz: 9bb10ed91224204e9cb0b56ba11233e3eb7ec4a3899f34568cae4d5bd77356f7
5
+ SHA512:
6
+ metadata.gz: 462499366a8d8eeb8c4b80e5d6c912eeb869d944a6dd101cfceb2a6b5fef66803a40431b346f5271eb558c43f3a65e1cbab21ef142a4cc6f19b70404a860fdf7
7
+ data.tar.gz: fbc43a2bacb4add9e4c600705fcb50e4be24dc768b7327c03abbf1760f68eb9b654c0896e8dafbec66a39e740f76a8497891f7dbdaa49f6b764a500f9b180e33
data/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ---
8
+
9
+ ## [Unreleased]
10
+
11
+ ---
12
+
13
+ ## [1.0.0] - 2026-06-28
14
+
15
+ ### Added
16
+
17
+ - Initial release of JwtAuthEngine
18
+ - Stateless JWT access + refresh token pair issuance
19
+ - Configurable auth model (`auth_model`), identifier field, and password field
20
+ - Install generator with migration, initializer, and model concern templates
21
+ - Endpoints: `signup`, `login`, `logout`, `refresh_token`, `change_password`, `me`, `ping`, `authenticated_ping`
22
+ - Service layer: `SignupService`, `LoginService`, `RefreshTokenService`, `ChangePasswordService`, `TokenService`
23
+ - Controller concerns: `Authenticatable`, `Rescuable`, `ResponseRenderable`, `Serializable`, `Tokenizable`
24
+ - Case-insensitive identifier lookup for string/text columns
25
+ - Password field type validation before update
26
+ - Global exception handling scoped to engine endpoints only
27
+ - Dynamic JWT payload keys based on configured model and identifier
28
+ - `JwtAuthEngine::MissingSecretKey` guard before any API call
29
+ - Comprehensive compatibility testing infrastructure:
30
+ - Appraisal boundary strategy covering Rails 6.1-8.1 (12 boundary versions)
31
+ - `bin/appraisal_rvm` helper for RVM-isolated gemset testing
32
+ - Two-tier CI workflows: fast required checks + scheduled full matrix
33
+ - GitHub Actions workflows for automated compatibility testing
34
+ - Rails 6.1 boot compatibility fix (explicit `require 'logger'`)
35
+ - Rails 8.x sqlite3 2.1+ support with conditional gem version override
36
+ - Complete test coverage (95% line, 90% branch) with SimpleCov enforcement
37
+ - RSpec test suite with 175 examples across all endpoints and services
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Abhishek Kushwaha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,422 @@
1
+ # JwtAuthEngine
2
+
3
+ [![Ruby](https://img.shields.io/badge/Ruby-%3E%3D%203.0-red)](https://www.ruby-lang.org)
4
+ [![Rails](https://img.shields.io/badge/Rails-%3E%3D%206.1-red)](https://rubyonrails.org)
5
+ [![CI](https://github.com/abhiskush/jwt_auth_engine/actions/workflows/ci.yml/badge.svg)](https://github.com/abhiskush/jwt_auth_engine/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
7
+
8
+ A mountable Rails engine that provides **stateless JWT authentication** endpoints for API-only Rails applications. It authenticates against your host application's existing model — no separate user table required.
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ - [Features](#features)
15
+ - [Requirements](#requirements)
16
+ - [Installation](#installation)
17
+ - [Configuration](#configuration)
18
+ - [Mounting the Engine](#mounting-the-engine)
19
+ - [API Endpoints](#api-endpoints)
20
+ - [Authentication](#authentication)
21
+ - [Customization](#customization)
22
+ - [Response Shapes](#response-shapes)
23
+ - [Testing](#testing)
24
+ - [Contributing](#contributing)
25
+ - [License](#license)
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ - 🔐 Stateless JWT access & refresh token pair
32
+ - 🧩 Plugs into any ActiveRecord model (`User`, `Account`, `Admin::User`, etc.)
33
+ - ⚙️ Fully configurable identifier field (`email`, `username`, etc.)
34
+ - ⚙️ Fully configurable password field (`password`, `pin`, etc.)
35
+ - 🏗️ Install generator that scaffolds migration, initializer, and model concern
36
+ - 🔒 Case-insensitive identifier lookup for string/text columns
37
+ - 🛡️ Global exception handling scoped only to engine endpoints
38
+ - 📦 Zero host-app pollution — uses `isolate_namespace`
39
+
40
+ ---
41
+
42
+ ## Requirements
43
+
44
+ | Dependency | Version |
45
+ |------------|-------------|
46
+ | Ruby | `>= 3.0.0` |
47
+ | Rails | `>= 6.1.0` |
48
+ | bcrypt | `>= 3.1.18` |
49
+ | jwt | `>= 2.9.0` |
50
+
51
+ ---
52
+
53
+ ## Installation
54
+
55
+ ### 1. Add the gem
56
+
57
+ Add this line to your application's Gemfile:
58
+
59
+ ```ruby
60
+ gem 'jwt_auth_engine'
61
+ ```
62
+
63
+ ### 2. Install dependencies
64
+
65
+ ```zsh
66
+ bundle install
67
+ ```
68
+
69
+ ### 3. Run the install generator
70
+
71
+ ```zsh
72
+ bundle exec rails generate jwt_auth_engine:install
73
+ ```
74
+
75
+ The generator accepts options to customize the auth model, identifier field, and password field. See [Generator Options](#generator-options).
76
+
77
+ This generates:
78
+
79
+ | File | Purpose |
80
+ |--------------------------------------------------------------------|-----------------------------------------------------|
81
+ | `config/initializers/jwt_auth_engine.rb` | Engine configuration |
82
+ | `db/migrate/<timestamp>_add_jwt_auth_engine_columns_to_<table>.rb` | Adds identifier + password digest columns |
83
+ | `app/models/concerns/jwt_auth_engine/auth_model_concern.rb` | Model behavior (`has_secure_password`, validations) |
84
+
85
+ The generator also attempts to inject `include JwtAuthEngine::AuthModelConcern` into your model file automatically.
86
+
87
+ ### 4. Run migration
88
+
89
+ ```zsh
90
+ bundle exec rails db:migrate
91
+ ```
92
+
93
+ ### 5. Set your JWT secret key
94
+
95
+ Open the generated initializer and set `config.jwt_secret_key` to a secure random string.
96
+
97
+ > ⚠️ The engine will raise `JwtAuthEngine::MissingSecretKey` at runtime if this value is `nil`.
98
+
99
+ Generate a secret:
100
+
101
+ ```zsh
102
+ bundle exec rails secret
103
+ ```
104
+
105
+ You can source it any way that suits your setup:
106
+
107
+ ```ruby
108
+ # From Rails credentials
109
+ config.jwt_secret_key = Rails.application.credentials.dig(:jwt_auth_engine, :secret_key)
110
+
111
+ # From an environment variable
112
+ config.jwt_secret_key = ENV['JWT_SECRET_KEY']
113
+
114
+ # From Rails secrets
115
+ config.jwt_secret_key = Rails.application.secrets.jwt_secret_key
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Configuration
121
+
122
+ The install generator creates an initializer with sensible defaults:
123
+
124
+ ```ruby
125
+ # config/initializers/jwt_auth_engine.rb
126
+ JwtAuthEngine.configure do |config|
127
+ config.auth_model = 'User' # Your ActiveRecord model class name
128
+ config.identifier_field = :email # Field used for login/signup lookup
129
+ config.password_field = :password # Base attribute for has_secure_password
130
+ config.jwt_secret_key = nil # REQUIRED: Set to a secure random string
131
+ config.access_token_expiry = 8.hours # Access token expiry duration
132
+ config.refresh_token_expiry = 7.days # Refresh token expiry duration
133
+ end
134
+ ```
135
+
136
+ ### Generator Options
137
+
138
+ All options are optional — defaults are applied if not provided.
139
+ E.g., to use an `Account` model with `username` as the identifier and `pin` as the password field:
140
+
141
+ ```zsh
142
+ bundle exec rails generate jwt_auth_engine:install \
143
+ --auth-model=Account \
144
+ --identifier-field=username \
145
+ --password-field=pin
146
+ ```
147
+
148
+ | Option | Default | Description |
149
+ |----------------------|------------|-----------------------------------------------------------------------------|
150
+ | `--auth-model` | `User` | ActiveRecord model class name |
151
+ | `--identifier-field` | `email` | Login/signup lookup field |
152
+ | `--password-field` | `password` | Password attribute (digest column = `<field>_digest`, required by `bcrypt`) |
153
+
154
+ ---
155
+
156
+ ## Mounting the Engine
157
+
158
+ Add to your host app's routes:
159
+
160
+ ```ruby
161
+ # config/routes.rb
162
+ Rails.application.routes.draw do
163
+ # other routes...
164
+ mount JwtAuthEngine::Engine, at: '/auth'
165
+ end
166
+ ```
167
+
168
+ All engine endpoints will be available under `/auth/` path.
169
+
170
+ ---
171
+
172
+ ## API Endpoints
173
+
174
+ You may see [JWT Auth Engine API Collection](https://abhiskush.github.io/jwt_auth_engine/api_collection.html) for detailed information on endpoints, request/response shapes, and example payloads.
175
+
176
+ A summary of endpoints is provided below.
177
+
178
+ ### Public Endpoints
179
+
180
+ | Method | Path | Description |
181
+ |--------|----------------|-------------------------------------------------------------------|
182
+ | `GET` | `/auth/ping` | Health check — returns `pong!` |
183
+ | `POST` | `/auth/signup` | Register a new account and returns new access and refresh tokens |
184
+ | `POST` | `/auth/login` | Authenticate and returns new access and refresh tokens |
185
+
186
+ ### Protected Endpoints (require access token)
187
+
188
+ | Method | Path | Description |
189
+ |----------|----------------------------|-------------------------------------------|
190
+ | `GET` | `/auth/authenticated_ping` | Verify token is valid |
191
+ | `DELETE` | `/auth/logout` | Stateless logout (client discards tokens) |
192
+ | `POST` | `/auth/change_password` | Change password for authenticated account |
193
+ | `GET` | `/auth/me` | Retrieve authenticated account profile |
194
+
195
+ ### Token Endpoints (require refresh token)
196
+
197
+ | Method | Path | Description |
198
+ |--------|-----------------------|---------------------------------------------|
199
+ | `POST` | `/auth/refresh_token` | Exchange refresh token for a new token pair |
200
+
201
+ > Send the refresh token as `Authorization: Bearer <refresh_token>` — not the access token.
202
+
203
+ ---
204
+
205
+ ## Authentication
206
+
207
+ ### How It Works
208
+
209
+ 1. On login/signup, the engine issues a **token pair** (access + refresh)
210
+ 2. The client sends the access token as `Authorization: Bearer <token>` on protected requests
211
+ 3. The engine decodes the token, extracts the model ID, and loads the authenticated record
212
+ 4. When the access token expires, use the refresh token at `/auth/refresh_token` to get a new pair
213
+
214
+ ### JWT Payload Structure
215
+
216
+ ```json
217
+ {
218
+ "user_id": 1,
219
+ "email": "user@example.com",
220
+ "exp": 1717200000,
221
+ "token_type": "access"
222
+ }
223
+ ```
224
+
225
+ Payload keys are dynamic: `user_id` becomes `account_id` if `auth_model = 'Account'` and `email` becomes `username` if `identifier_field = :username`.
226
+
227
+ ### Token Expiry Defaults
228
+
229
+ | Token | Default Expiry |
230
+ |---------------|----------------|
231
+ | Access token | 8 hours |
232
+ | Refresh token | 7 days |
233
+
234
+ ---
235
+
236
+ ## Customization
237
+
238
+ ### Auth Model Concern
239
+
240
+ The generator creates `app/models/concerns/jwt_auth_engine/auth_model_concern.rb` in your host app. This file is **yours to customize**:
241
+
242
+ - Modify validations
243
+ - Change normalization behavior
244
+ - Add additional callbacks
245
+
246
+ If the generator can't find your model file, include the concern manually:
247
+
248
+ ```ruby
249
+ class User < ApplicationRecord
250
+ include JwtAuthEngine::AuthModelConcern
251
+ end
252
+ ```
253
+
254
+ The generated concern includes a `before_validation` callback that strips and downcases string identifiers. You can customize or remove this in the generated concern file to match your identifier semantics.
255
+
256
+ ---
257
+
258
+ ## Response Shapes
259
+
260
+ All engine responses follow a consistent JSON shape, scoped only to engine endpoints (no impact on host app error handling).
261
+
262
+ | HTTP Status | Shape |
263
+ |--------------|----------------------------------------------------------------------------|
264
+ | `200` | `{ "success": true, "data": { ... } }` |
265
+ | `201` | `{ "success": true, "data": { ... } }` |
266
+ | `401` | `{ "success": false, "errors": "..." }` |
267
+ | `422` | `{ "success": false, "errors": [...] }` |
268
+ | `500` | `{ "success": false, "errors": { "message": "...", "backtrace": [...] } }` |
269
+
270
+ ## Custom Error Classes
271
+
272
+ | Error | When Raised |
273
+ |------------------------------------|--------------------------------------------------------|
274
+ | `JwtAuthEngine::MissingSecretKey` | `jwt_secret_key` is not configured |
275
+ | `JwtAuthEngine::AuthModelNotFound` | Configured model class cannot be resolved |
276
+ | `JwtAuthEngine::InvalidAuthModel` | Model does not inherit from `ActiveRecord::Base` |
277
+ | `JwtAuthEngine::TokenExpired` | JWT has passed its expiry time |
278
+ | `JwtAuthEngine::InvalidToken` | JWT cannot be decoded |
279
+ | `JwtAuthEngine::InvalidTokenType` | Token type does not match expected (access vs refresh) |
280
+
281
+ ---
282
+
283
+ ## Testing
284
+
285
+ The gem uses **RSpec** for testing and **SimpleCov** for code coverage enforcement. Tests run against a minimal dummy Rails app (`spec/dummy/`) with an in-memory SQLite database.
286
+
287
+ ### Coverage Thresholds
288
+
289
+ | Metric | Minimum Required |
290
+ |--------|-----------------|
291
+ | Line coverage | 95% |
292
+ | Branch coverage | 90% |
293
+
294
+ SimpleCov will fail the test suite if coverage drops below these thresholds.
295
+
296
+ ### Running Tests
297
+
298
+ ```zsh
299
+ # Run the full test suite
300
+ bundle exec rspec
301
+
302
+ # Run with verbose output
303
+ bundle exec rspec --format documentation
304
+
305
+ # Run a specific spec file
306
+ bundle exec rspec spec/services/jwt_auth_engine/login_service_spec.rb
307
+
308
+ # Run a specific example by line number
309
+ bundle exec rspec spec/requests/jwt_auth_engine/sessions_spec.rb:12
310
+ ```
311
+
312
+ After each run, a coverage report is generated at `coverage/index.html`.
313
+
314
+ ### Test Structure
315
+
316
+ ```
317
+ spec/
318
+ ├── spec_helper.rb # SimpleCov setup + coverage thresholds
319
+ ├── rails_helper.rb # Boots dummy app, DatabaseCleaner, JwtAuthEngine config
320
+ ├── dummy/ # Minimal Rails API app for integration testing
321
+ ├── lib/ # Unit tests for core library modules
322
+ ├── services/ # Unit tests for service objects
323
+ └── requests/ # Integration tests for all API endpoints
324
+ ```
325
+
326
+ ### Multi-Version Testing with Appraisal
327
+
328
+ The gem supports Rails 6.1 through 8.1. We use
329
+ [Appraisal](https://github.com/thoughtbot/appraisal) with a boundary strategy:
330
+ minimum and latest patch for each supported Rails minor line.
331
+
332
+ #### Setup
333
+
334
+ ```zsh
335
+ # Install base dependencies
336
+ bundle install
337
+
338
+ # Generate appraisal gemfiles (run after editing Appraisals file)
339
+ bundle exec appraisal generate
340
+ ```
341
+
342
+ #### Listing Available Appraisals
343
+
344
+ ```zsh
345
+ bundle exec appraisal list
346
+ ```
347
+
348
+ #### Running Tests Against a Specific Boundary
349
+
350
+ ```zsh
351
+ bundle exec appraisal rails-6.1-min rspec
352
+ bundle exec appraisal rails-7.2-max rspec
353
+ bundle exec appraisal rails-8.1-max rspec
354
+ ```
355
+
356
+ #### Running Tests Against ALL Rails Versions
357
+
358
+ ```zsh
359
+ bundle exec appraisal rspec
360
+ ```
361
+
362
+ #### RVM Isolated Gemsets (One Per Appraisal)
363
+
364
+ If you use RVM, this project includes a helper that keeps dependencies isolated
365
+ with one gemset per appraisal gemfile.
366
+
367
+ ```zsh
368
+ # Show all appraisal targets
369
+ bin/appraisal_rvm list
370
+
371
+ # Create Ruby + gemset + install dependencies for one appraisal
372
+ bin/appraisal_rvm bootstrap rails-7.2-max
373
+
374
+ # Run specs for one appraisal in its dedicated gemset
375
+ bin/appraisal_rvm run rails-7.2-max
376
+
377
+ # Pre-install all appraisals (can take time)
378
+ bin/appraisal_rvm bootstrap-all
379
+ ```
380
+
381
+ > **Note:** Rails and Ruby compatibility is constrained by both Rails requirements
382
+ > and this gem's minimum Ruby requirement (`>= 3.0.0`).
383
+
384
+ #### Compatibility Matrix
385
+
386
+ | Rails Line | Tested Boundaries | Effective Min Ruby |
387
+ |---|---|---|
388
+ | Rails 6.1.x | 6.1.0 and 6.1.7.10 | 3.0.0 |
389
+ | Rails 7.0.x | 7.0.0 and 7.0.10 | 3.0.0 |
390
+ | Rails 7.1.x | 7.1.0 and 7.1.6 | 3.0.0 |
391
+ | Rails 7.2.x | 7.2.0 and 7.2.3.1 | 3.1.0 |
392
+ | Rails 8.0.x | 8.0.0 and 8.0.5 | 3.2.0 |
393
+ | Rails 8.1.x | 8.1.0 and 8.1.3 | 3.2.0 |
394
+
395
+ ### CI (GitHub Actions)
396
+
397
+ We run a two-tier matrix:
398
+
399
+ - `.github/workflows/ci.yml`: required PR/push checks with boundary coverage.
400
+ - `.github/workflows/compatibility-full.yml`: scheduled/manual broad matrix
401
+ across Ruby 3.0-3.5 and all Rails boundary appraisals.
402
+
403
+ This is a standard professional setup: fast required feedback plus deeper
404
+ periodic compatibility surveillance.
405
+
406
+ ---
407
+
408
+ ## Contributing
409
+
410
+ 1. Fork the repository
411
+ 2. Create your feature branch (`git checkout -b feature/my-feature`)
412
+ 3. Commit your changes (`git commit -am 'Add my feature'`)
413
+ 4. Push to the branch (`git push origin feature/my-feature`)
414
+ 5. Open a Pull Request
415
+
416
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct and contribution process.
417
+
418
+ ---
419
+
420
+ ## License
421
+
422
+ This gem is available as open source under the terms of the [MIT License](LICENSE.txt).
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Controller concern for extracting and validating bearer access tokens.
5
+ module Authenticatable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_accessor :current_auth_model_instance
10
+ end
11
+
12
+ private
13
+
14
+ def authenticate_auth_model!
15
+ token = bearer_token
16
+ return render_unauthorized('Authorization header missing or malformed') unless token
17
+
18
+ assign_current_auth_model(token)
19
+ rescue StandardError => e
20
+ handle_authentication_error(e)
21
+ end
22
+
23
+ def assign_current_auth_model(token)
24
+ payload = TokenService.decode_access(token)
25
+ payload_key = JwtAuthEngine.auth_model_token_payload_key
26
+ model_id = payload[payload_key]
27
+ self.current_auth_model_instance = JwtAuthEngine.auth_model_class.find(model_id)
28
+ end
29
+
30
+ def handle_authentication_error(error)
31
+ message = authentication_error_message(error)
32
+ return render_unauthorized(message) if message
33
+
34
+ raise error
35
+ end
36
+
37
+ def authentication_error_message(error)
38
+ return 'Access token has expired' if error.is_a?(JwtAuthEngine::TokenExpired)
39
+ return error.message if error.is_a?(JwtAuthEngine::InvalidToken)
40
+ return 'Access token expected' if error.is_a?(JwtAuthEngine::InvalidTokenType)
41
+
42
+ "#{JwtAuthEngine.auth_model_name.to_s.humanize} not found" if error.is_a?(ActiveRecord::RecordNotFound)
43
+ end
44
+
45
+ def bearer_token
46
+ header = request.headers['Authorization']
47
+ return nil unless header&.start_with?('Bearer ')
48
+
49
+ header.split(' ', 2).last.presence
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Concern that converts unhandled exceptions to structured JSON responses.
5
+ module Rescuable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ rescue_from StandardError, with: :render_unhandled_exception
10
+ end
11
+
12
+ private
13
+
14
+ def render_unhandled_exception(exception)
15
+ render_internal_server_error(
16
+ errors: {
17
+ message: exception.message,
18
+ backtrace: Array(exception.backtrace).first(10)
19
+ }
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Concern with shared JSON response helpers for engine controllers.
5
+ module ResponseRenderable
6
+ def render_success(**data)
7
+ render json: { success: true, data: data.except(:status) },
8
+ status: data[:status] || :ok
9
+ end
10
+
11
+ def render_unauthorized(message = 'Unauthorized')
12
+ render_error(errors: message, status: :unauthorized)
13
+ end
14
+
15
+ def render_validation_error(errors:)
16
+ render_error(errors: errors, status: :unprocessable_entity)
17
+ end
18
+
19
+ def render_internal_server_error(errors:)
20
+ render_error(errors: errors, status: :internal_server_error)
21
+ end
22
+
23
+ private
24
+
25
+ def render_error(errors:, status:)
26
+ render json: { success: false, errors: errors }, status: status
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Concern that serializes auth model fields for API responses.
5
+ module Serializable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def serialize_auth_model_instance(auth_model_instance)
11
+ if auth_model_instance.respond_to?(JwtAuthEngine.identifier_field)
12
+ identifier_value = auth_model_instance.public_send(JwtAuthEngine.identifier_field)
13
+ end
14
+
15
+ primary_key = auth_model_instance.class.primary_key
16
+ primary_key_value = auth_model_instance.public_send(primary_key)
17
+
18
+ { primary_key => primary_key_value, JwtAuthEngine.identifier_field => identifier_value }.compact
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Concern that builds token payloads and issues access/refresh token pairs.
5
+ module Tokenizable
6
+ extend ActiveSupport::Concern
7
+
8
+ private
9
+
10
+ def issue_tokens(auth_model_instance)
11
+ payload = {
12
+ JwtAuthEngine.auth_model_token_payload_key => auth_model_instance.id,
13
+ JwtAuthEngine.identifier_field => auth_model_instance.public_send(JwtAuthEngine.identifier_field)
14
+ }
15
+
16
+ {
17
+ access_token: TokenService.encode_access(payload),
18
+ refresh_token: TokenService.encode_refresh(payload)
19
+ }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Base API controller for all engine endpoints.
5
+ class ApplicationController < ActionController::API
6
+ include Authenticatable
7
+ include ResponseRenderable
8
+ include Rescuable
9
+
10
+ before_action :ensure_jwt_secret_key!
11
+ before_action :authenticate_auth_model!
12
+
13
+ private
14
+
15
+ def ensure_jwt_secret_key!
16
+ JwtAuthEngine.configuration.jwt_secret_key
17
+ rescue JwtAuthEngine::MissingSecretKey => e
18
+ render_internal_server_error(errors: e.message)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JwtAuthEngine
4
+ # Handles password rotation for authenticated users.
5
+ class PasswordsController < ApplicationController
6
+ # ── POST /change_password ─────────────────────────────────────────────────
7
+ def change_password
8
+ result = ChangePasswordService.new(
9
+ auth_model_instance: current_auth_model_instance,
10
+ change_password_params: change_password_params
11
+ ).call
12
+
13
+ return render_success(message: result[:message]) if result[:success]
14
+
15
+ return render_unauthorized(result[:invalid]) if result[:invalid]
16
+
17
+ render_validation_error(errors: result[:errors])
18
+ end
19
+
20
+ private
21
+
22
+ def change_password_params
23
+ params.permit(JwtAuthEngine.current_password_field, JwtAuthEngine.new_password_field)
24
+ end
25
+ end
26
+ end