api_authentication_gem 0.1.3 → 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: 5ef82ce77c6130e40ba62ffa3eb29c4c0536de438168d59052f3e4ac0abcba90
4
- data.tar.gz: a4d81cc6a0b0ad3b21130868b6b3cdebde7026680bb7403e67fa5fe873c432b0
3
+ metadata.gz: 47dd157bb015ccb6a5874bfc4201f1df1b458a91da0bdb182249817f8faa2cda
4
+ data.tar.gz: 41a333d3705fb4c37ae5e2d07cc13dabfbe120fc9ed633321de0358c61c3ab98
5
5
  SHA512:
6
- metadata.gz: 8f4b52e667b8f7de5bea164d3bf201bfe4b69a098d0cbe40dcd82c0b971e952ffba32e91e89306d85c984c9cd11563138f5d5ea0366d1b42d3ac912b2638d4db
7
- data.tar.gz: 8f4a182fb392ad8d2fe2289f96cbe2d25620a566295ad1a82aa857cec9702a1cf1a309bd8886bb5fdfd415c739025ff680e547bb13d049c75d5283b911bb0bda
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,70 +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
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)
15
22
 
16
23
  ---
17
24
 
18
- ## 📦 Installation
25
+ ## Installation
19
26
 
20
- Add this line to your Rails application's `Gemfile`:
27
+ ```ruby
28
+ # Gemfile
21
29
  gem 'api_authentication_gem'
22
- Then run:
30
+ ```
31
+
32
+ ```bash
23
33
  bundle install
34
+ ```
35
+
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
51
+ 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
+ end
69
+ add_index :api_auth_password_resets, :token, unique: true
70
+ add_index :api_auth_password_resets, :identifier
71
+
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
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
103
+ end
104
+ end
105
+ ```
24
106
 
25
- 🛠 Setup in Your Rails API
26
- 1. Generate the User Model (if not already created)
27
- rails generate model User email:string password_digest:string
107
+ ```bash
28
108
  rails db:migrate
109
+ ```
110
+
111
+ ---
29
112
 
30
- 2. Create a Controller to Handle Auth Actions
113
+ ## Setup
31
114
 
32
- class UsersController < ApplicationController
33
- def signup
34
- result = ApiAuthenticationGem::Auth.signup(
35
- email: params[:email],
36
- password: params[:password],
37
- user_class: User
38
- )
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
+ ```
39
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])
40
169
  if result[:error]
41
- render json: { error: result[:error] }, status: :unprocessable_entity
170
+ render json: result, status: :unprocessable_entity
42
171
  else
43
- render json: { message: result[:message], user: result[:user] }, status: :created
172
+ render json: result, status: :created
44
173
  end
45
174
  end
46
175
 
47
176
  def login
48
177
  result = ApiAuthenticationGem::Auth.login(
49
- email: params[:email],
50
- password: params[:password],
51
- user_class: User
178
+ email: params[:email],
179
+ password: params[:password],
180
+ ip_address: request.remote_ip,
181
+ user_agent: request.user_agent
52
182
  )
183
+ result[:error] ? render(json: result, status: :unauthorized) : render(json: result)
184
+ end
53
185
 
54
- if result[:error]
55
- render json: { error: result[:error] }, status: :unauthorized
56
- else
57
- render json: { token: result[:token] }, status: :ok
58
- end
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)
59
236
  end
60
237
  end
238
+ ```
61
239
 
240
+ ### 4. Routes
62
241
 
63
- 3. Define Routes
64
- In your config/routes.rb file:
242
+ ```ruby
65
243
  Rails.application.routes.draw do
66
- post 'signup', to: 'users#signup'
67
- post 'login', to: 'users#login'
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"
68
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
+ ---
69
439
 
440
+ ## License
70
441
 
442
+ MIT