api_authentication_gem 0.1.4 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec9f993dd7036eee23f50421a5ef5e197370ba2349abcacf95bcf0eb81b8370c
4
- data.tar.gz: 52e9145805cd449d3e4ee0959a48b217e3bea0da412587fabe4c0e36db9ed89c
3
+ metadata.gz: 47dd157bb015ccb6a5874bfc4201f1df1b458a91da0bdb182249817f8faa2cda
4
+ data.tar.gz: 41a333d3705fb4c37ae5e2d07cc13dabfbe120fc9ed633321de0358c61c3ab98
5
5
  SHA512:
6
- metadata.gz: 452a6501dc344085c0e30e9550fe046b115b7c69c87a70fa49e74e3711211428ada98d69a119e3a0c913eb4c73a7ae82d19d9132b85dce8fab921d4f856b1c5d
7
- data.tar.gz: 956f9e09e241360429e214e1b96e79ea2c0f9f775c0889df36a33c44d876c239ef5e26f60b68d9b04a54d683399c4fcadd8ed38bbf18bab10deb52f85b757016
6
+ metadata.gz: 8dc506d8a848fd2ad2d7edecae50cc96fc7cba4f2761d0aece27958c010daf7e5c97444b287dfffcb8fbd9c47f81f0704ad2acdabb085dcf6994169e1301bc45
7
+ data.tar.gz: de88c45204cede6225eb7db6dc07c5ba912c05a2495e68e4a72f058b73219eca2d43c1a89f79eee873e1ac0ae46576f4ef3e0d99889570ba2896836e9d39a1de
data/LICENSE.txt ADDED
@@ -0,0 +1,10 @@
1
+ Custom License - For Testing Purposes Only
2
+
3
+ Copyright (c) 2025 [Adarsh Mishra]
4
+
5
+ Permission is granted, free of charge, to use the software for testing purposes only.
6
+ This software should not be used in production environments, deployed in commercial
7
+ applications, or redistributed without explicit permission.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
10
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,95 +1,442 @@
1
- # ApiAuthenticationGem
1
+ # api_authentication_gem
2
2
 
3
- A lightweight and easy-to-use Ruby gem that adds **user signup and login** functionality to your **Rails API** using **JWT authentication**.
3
+ A Ruby gem that handles JWT authentication for Rails APIs. It covers the usual stuff — signup, login, refresh tokens, password reset, lockout — plus some extras like magic links, RBAC, session tracking, hooks, and RS256.
4
4
 
5
- No need to write boilerplate authentication code again — just plug it in, and you're good to go!
5
+ [![Gem Version](https://badge.fury.io/rb/api_authentication_gem.svg)](https://rubygems.org/gems/api_authentication_gem)
6
6
 
7
7
  ---
8
8
 
9
- ## ✨ Features
9
+ ## What's included
10
10
 
11
- - ✅ User signup with email uniqueness and password validation (handled by the gem)
12
- - ✅ Login with JWT token generation
13
- - ✅ Easy integration with any Rails API
14
- - ✅ Customizable user model support
15
- - ✅ Configurable `secret_key` and `user_class`
16
- - ✅ Tested with RSpec
17
- - ✅ Rails-friendly design
11
+ - Signup and login with JWT
12
+ - Refresh tokens (rotated on every use)
13
+ - Token revocation
14
+ - Password reset
15
+ - Account lockout after failed attempts
16
+ - `authenticate_user!` controller concern
17
+ - Role-based access control
18
+ - Passwordless magic link login
19
+ - Per-device session tracking
20
+ - Hooks for auth events (signup, login, logout, etc.)
21
+ - RS256 support (asymmetric keys)
18
22
 
19
23
  ---
20
24
 
21
- ## 📦 Installation
25
+ ## Installation
22
26
 
23
- Add this line to your Rails application's `Gemfile`:
24
- `gem 'api_authentication_gem'`
25
- Then run:
26
- `bundle install`
27
+ ```ruby
28
+ # Gemfile
29
+ gem 'api_authentication_gem'
30
+ ```
27
31
 
28
- 🛠 Setup in Your Rails API
29
- 1. Add gem configs to your app:
30
- ```ruby
31
- # config/initializers/api_authentication_gem.rb
32
+ ```bash
33
+ bundle install
34
+ ```
32
35
 
33
- ApiAuthenticationGem.configure do |config|
34
- config.secret_key = ENV["SECRET_KEY_BASE"] || "fallback-key" # Always set your secret_key via environment variable in production
35
- config.user_class = "User"
36
+ ---
37
+
38
+ ## Database migration
39
+
40
+ Run this migration once. You only need the tables for the features you actually use.
41
+
42
+ ```ruby
43
+ class CreateApiAuthTables < ActiveRecord::Migration[7.0]
44
+ def change
45
+ create_table :api_auth_refresh_tokens do |t|
46
+ t.integer :user_id, null: false
47
+ t.string :token, null: false
48
+ t.datetime :expires_at, null: false
49
+ t.datetime :revoked_at
50
+ t.timestamps
36
51
  end
37
- ```
38
-
39
- 2. Generate the User Model (if not already created)
40
- ```bash
41
- rails generate model User email:string password_digest:string
42
- rails db:migrate
43
- ```
44
-
45
- Your User model must use has_secure_password:
46
- ```ruby
47
- class User < ApplicationRecord
48
- has_secure_password
49
- end
50
- ```
51
- No need to manually validate emails or handle password_digest — the gem takes care of it internally.
52
-
53
- 3. Create a Controller to Handle Auth Actions
54
-
55
- ```ruby
56
- class UsersController < ApplicationController
57
- def signup
58
- result = ApiAuthenticationGem::Auth.signup(
59
- email: params[:email],
60
- password: params[:password]
61
- )
62
-
63
- if result[:error]
64
- render json: { error: result[:error] }, status: :unprocessable_entity
65
- else
66
- render json: { message: result[:message], user: result[:user] }, status: :created
67
- end
52
+ add_index :api_auth_refresh_tokens, :token, unique: true
53
+ add_index :api_auth_refresh_tokens, :user_id
54
+
55
+ create_table :api_auth_revoked_tokens do |t|
56
+ t.string :jti, null: false
57
+ t.datetime :expires_at, null: false
58
+ t.timestamps
59
+ end
60
+ add_index :api_auth_revoked_tokens, :jti, unique: true
61
+
62
+ create_table :api_auth_password_resets do |t|
63
+ t.string :identifier, null: false
64
+ t.string :token, null: false
65
+ t.datetime :expires_at, null: false
66
+ t.datetime :used_at
67
+ t.timestamps
68
68
  end
69
+ add_index :api_auth_password_resets, :token, unique: true
70
+ add_index :api_auth_password_resets, :identifier
69
71
 
70
- def login
71
- result = ApiAuthenticationGem::Auth.login(
72
- email: params[:email],
73
- password: params[:password]
74
- )
75
-
76
- if result[:error]
77
- render json: { error: result[:error] }, status: :unauthorized
78
- else
79
- render json: { token: result[:token] }, status: :ok
80
- end
72
+ create_table :api_auth_lockouts do |t|
73
+ t.string :identifier, null: false
74
+ t.integer :failed_attempts, default: 0, null: false
75
+ t.datetime :locked_until
76
+ t.timestamps
81
77
  end
78
+ add_index :api_auth_lockouts, :identifier, unique: true
79
+
80
+ # only needed if you use magic links
81
+ create_table :api_auth_magic_links do |t|
82
+ t.string :identifier, null: false
83
+ t.string :token, null: false
84
+ t.datetime :expires_at, null: false
85
+ t.datetime :used_at
86
+ t.timestamps
87
+ end
88
+ add_index :api_auth_magic_links, :token, unique: true
89
+ add_index :api_auth_magic_links, :identifier
90
+
91
+ # only needed if you use session tracking
92
+ create_table :api_auth_sessions do |t|
93
+ t.integer :user_id, null: false
94
+ t.string :jti, null: false
95
+ t.string :ip_address
96
+ t.string :user_agent
97
+ t.datetime :expires_at, null: false
98
+ t.datetime :revoked_at
99
+ t.timestamps
100
+ end
101
+ add_index :api_auth_sessions, :jti, unique: true
102
+ add_index :api_auth_sessions, :user_id
82
103
  end
83
- ```
104
+ end
105
+ ```
106
+
107
+ ```bash
108
+ rails db:migrate
109
+ ```
110
+
111
+ ---
84
112
 
113
+ ## Setup
85
114
 
86
- 3. Define Routes
87
- In your config/routes.rb file:
88
- ```ruby
89
- Rails.application.routes.draw do
90
- post 'signup', to: 'users#signup'
91
- post 'login', to: 'users#login'
115
+ ### 1. Initializer
116
+
117
+ ```ruby
118
+ # config/initializers/api_auth.rb
119
+
120
+ ApiAuthenticationGem.configure do |config|
121
+ config.secret_key = ENV["API_AUTH_SECRET_KEY"]
122
+
123
+ config.user_class = "User"
124
+ config.token_expiry = 15 * 60 # 15 min
125
+ config.min_password_length = 8
126
+
127
+ config.refresh_tokens_enabled = true
128
+ config.refresh_token_expiry = 30 * 24 * 60 * 60 # 30 days
129
+
130
+ config.revocation_enabled = true
131
+
132
+ config.lockout_enabled = true
133
+ config.max_login_attempts = 5
134
+ config.lockout_duration = 15 * 60
135
+
136
+ # optional features
137
+ config.magic_link_enabled = false
138
+ config.session_tracking_enabled = false
139
+
140
+ # hooks — all optional
141
+ config.on_signup = ->(user) { puts "signed up: #{user.email}" }
142
+ config.on_login = ->(user) { puts "logged in: #{user.email}" }
143
+ config.on_login_failure = ->(email) { puts "failed login: #{email}" }
144
+ config.on_logout = ->(jti) { puts "logged out: #{jti}" }
145
+ config.on_password_reset = ->(user) { puts "password reset: #{user.email}" }
146
+ end
147
+ ```
148
+
149
+ ### 2. User model
150
+
151
+ ```bash
152
+ rails generate model User email:string:uniq password_digest:string role:string
153
+ rails db:migrate
154
+ ```
155
+
156
+ ```ruby
157
+ class User < ApplicationRecord
158
+ has_secure_password
159
+ validates :email, presence: true, uniqueness: true
160
+ end
161
+ ```
162
+
163
+ ### 3. Auth controller
164
+
165
+ ```ruby
166
+ class AuthController < ApplicationController
167
+ def signup
168
+ result = ApiAuthenticationGem::Auth.signup(email: params[:email], password: params[:password])
169
+ if result[:error]
170
+ render json: result, status: :unprocessable_entity
171
+ else
172
+ render json: result, status: :created
92
173
  end
93
- ```
174
+ end
175
+
176
+ def login
177
+ result = ApiAuthenticationGem::Auth.login(
178
+ email: params[:email],
179
+ password: params[:password],
180
+ ip_address: request.remote_ip,
181
+ user_agent: request.user_agent
182
+ )
183
+ result[:error] ? render(json: result, status: :unauthorized) : render(json: result)
184
+ end
185
+
186
+ def refresh
187
+ result = ApiAuthenticationGem::Auth.refresh(refresh_token: params[:refresh_token])
188
+ result[:error] ? render(json: result, status: :unauthorized) : render(json: result)
189
+ end
190
+
191
+ def logout
192
+ token = request.headers["Authorization"]&.split(" ")&.last
193
+ render json: ApiAuthenticationGem::Auth.revoke(token: token)
194
+ end
195
+
196
+ def forgot_password
197
+ result = ApiAuthenticationGem::Auth.request_password_reset(email: params[:email])
198
+ # send result[:reset_token] by email — don't expose it in the response
199
+ render json: { message: result[:message] }
200
+ end
201
+
202
+ def reset_password
203
+ result = ApiAuthenticationGem::Auth.reset_password(
204
+ reset_token: params[:token],
205
+ new_password: params[:password]
206
+ )
207
+ result[:error] ? render(json: result, status: :unprocessable_entity) : render(json: result)
208
+ end
209
+
210
+ # magic links
211
+ def send_magic_link
212
+ result = ApiAuthenticationGem::Auth.send_magic_link(email: params[:email])
213
+ # send result[:magic_token] by email — don't expose it in the response
214
+ render json: { message: result[:message] }
215
+ end
216
+
217
+ def verify_magic_link
218
+ result = ApiAuthenticationGem::Auth.verify_magic_link(token: params[:token])
219
+ result[:error] ? render(json: result, status: :unauthorized) : render(json: result)
220
+ end
221
+
222
+ # session tracking
223
+ def sessions
224
+ authenticate_user!
225
+ render json: ApiAuthenticationGem::Auth.active_sessions(user_id: @current_user.id)
226
+ end
227
+
228
+ def revoke_session
229
+ authenticate_user!
230
+ render json: ApiAuthenticationGem::Auth.revoke_session(jti: params[:jti])
231
+ end
232
+
233
+ def revoke_all_sessions
234
+ authenticate_user!
235
+ render json: ApiAuthenticationGem::Auth.revoke_all_sessions(user_id: @current_user.id)
236
+ end
237
+ end
238
+ ```
239
+
240
+ ### 4. Routes
241
+
242
+ ```ruby
243
+ Rails.application.routes.draw do
244
+ post "signup", to: "auth#signup"
245
+ post "login", to: "auth#login"
246
+ post "refresh", to: "auth#refresh"
247
+ delete "logout", to: "auth#logout"
248
+ post "forgot_password", to: "auth#forgot_password"
249
+ post "reset_password", to: "auth#reset_password"
250
+ post "magic_link", to: "auth#send_magic_link"
251
+ post "magic_link/verify", to: "auth#verify_magic_link"
252
+ get "sessions", to: "auth#sessions"
253
+ delete "sessions/:jti", to: "auth#revoke_session"
254
+ delete "sessions", to: "auth#revoke_all_sessions"
255
+ end
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Protecting routes
261
+
262
+ Include the concern and call `authenticate_user!`:
263
+
264
+ ```ruby
265
+ class ApplicationController < ActionController::API
266
+ include ApiAuthenticationGem::Concerns::Authenticatable
267
+ end
268
+
269
+ class PostsController < ApplicationController
270
+ before_action :authenticate_user!
271
+
272
+ def index
273
+ render json: current_user.posts
274
+ end
275
+ end
276
+ ```
277
+
278
+ ---
279
+
280
+ ## RBAC
281
+
282
+ Add a `role` column to users, then use `authorize_role!` inside any action:
283
+
284
+ ```ruby
285
+ class AdminController < ApplicationController
286
+ before_action :authenticate_user!
287
+ before_action -> { authorize_role!("admin") }
288
+
289
+ def dashboard
290
+ render json: { message: "welcome #{current_user.email}" }
291
+ end
292
+ end
293
+ ```
294
+
295
+ Returns HTTP 403 if the role doesn't match:
296
+
297
+ ```json
298
+ { "error": "Forbidden", "required": ["admin"], "current": "viewer" }
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Magic links
304
+
305
+ ```ruby
306
+ # step 1 — request a link
307
+ result = ApiAuthenticationGem::Auth.send_magic_link(email: "user@example.com")
308
+ # deliver result[:magic_token] by email
309
+
310
+ # step 2 — user clicks the link
311
+ result = ApiAuthenticationGem::Auth.verify_magic_link(token: params[:token])
312
+ # => { token: "...", user: ..., refresh_token: "..." }
313
+ ```
314
+
315
+ Tokens are single-use. Requesting a new one invalidates the previous one.
316
+
317
+ ---
318
+
319
+ ## Session tracking
320
+
321
+ ```ruby
322
+ # list active sessions
323
+ ApiAuthenticationGem::Auth.active_sessions(user_id: user.id)
324
+
325
+ # revoke one session (sign out a specific device)
326
+ ApiAuthenticationGem::Auth.revoke_session(jti: "some-jti")
327
+
328
+ # sign out everywhere
329
+ ApiAuthenticationGem::Auth.revoke_all_sessions(user_id: user.id)
330
+ ```
331
+
332
+ ---
333
+
334
+ ## RS256
335
+
336
+ If you want asymmetric keys instead of a shared secret:
337
+
338
+ ```bash
339
+ openssl genrsa -out private.pem 2048
340
+ openssl rsa -in private.pem -pubout -out public.pem
341
+ ```
342
+
343
+ ```ruby
344
+ ApiAuthenticationGem.configure do |config|
345
+ config.algorithm = "RS256"
346
+ config.private_key = OpenSSL::PKey::RSA.new(File.read("private.pem"))
347
+ config.public_key = OpenSSL::PKey::RSA.new(File.read("public.pem"))
348
+ config.user_class = "User"
349
+ end
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Method reference
355
+
356
+ ### `Auth.signup(email:, password:)`
357
+ | Result | Keys |
358
+ |---|---|
359
+ | Success | `message`, `user`, `token`, `refresh_token` |
360
+ | Blank email | `error: "Email is required"` |
361
+ | Weak password | `error: "Password must be at least N characters"` |
362
+ | Duplicate email | `error: "Email has already been taken"` |
363
+
364
+ ### `Auth.login(email:, password:, ip_address: nil, user_agent: nil)`
365
+ | Result | Keys |
366
+ |---|---|
367
+ | Success | `token`, `user`, `refresh_token` |
368
+ | Wrong credentials | `error: "Invalid email or password"` |
369
+ | Locked | `error: "Account locked. Try again in N minute(s)."` |
370
+
371
+ ### `Auth.refresh(refresh_token:)`
372
+ | Result | Keys |
373
+ |---|---|
374
+ | Success | `token`, `refresh_token`, `user` |
375
+ | Expired | `error: "Refresh token has expired"` |
376
+ | Revoked | `error: "Refresh token has been revoked"` |
377
+
378
+ ### `Auth.revoke(token:)`
379
+ | Result | Keys |
380
+ |---|---|
381
+ | Success | `message: "Token revoked successfully"` |
382
+ | Invalid | `error: "Invalid token"` |
383
+
384
+ ### `Auth.request_password_reset(email:)`
385
+ Returns `{ message:, reset_token: }` for known emails, same message for unknown ones (no enumeration).
386
+
387
+ ### `Auth.reset_password(reset_token:, new_password:)`
388
+ | Result | Keys |
389
+ |---|---|
390
+ | Success | `message: "Password has been reset successfully"` |
391
+ | Bad token | `error: "Invalid or expired reset token"` |
392
+
393
+ ### `Auth.send_magic_link(email:)`
394
+ Returns `{ message:, magic_token: }` for known emails, same message for unknown ones.
395
+
396
+ ### `Auth.verify_magic_link(token:)`
397
+ | Result | Keys |
398
+ |---|---|
399
+ | Success | `token`, `user`, `refresh_token` |
400
+ | Bad token | `error: "Invalid or expired magic link"` |
401
+
402
+ ### `Auth.active_sessions(user_id:)` / `Auth.revoke_session(jti:)` / `Auth.revoke_all_sessions(user_id:)`
403
+ All return `{ error: }` when `session_tracking_enabled` is false.
404
+
405
+ ### `Auth.decode(token)`
406
+ Returns the JWT payload hash, or `nil` if invalid/expired/revoked.
407
+
408
+ ---
409
+
410
+ ## JWT claims
411
+
412
+ | Claim | Value |
413
+ |---|---|
414
+ | `sub` | User ID |
415
+ | `iat` | Issued at |
416
+ | `exp` | Expires at |
417
+ | `jti` | Unique token ID |
418
+
419
+ ---
420
+
421
+ ## Cron cleanup
422
+
423
+ ```ruby
424
+ ApiAuthenticationGem::RevokedToken.cleanup!
425
+ ```
426
+
427
+ Run this periodically to prune expired entries from the revoked tokens table.
428
+
429
+ ---
430
+
431
+ ## Running tests
432
+
433
+ ```bash
434
+ bundle exec rspec
435
+ # 186 examples, 0 failures
436
+ ```
437
+
438
+ ---
94
439
 
440
+ ## License
95
441
 
442
+ MIT