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 +4 -4
- data/LICENSE.txt +10 -0
- data/README.md +420 -73
- data/lib/api_authentication_gem/auth.rb +422 -26
- data/lib/api_authentication_gem/concerns/authenticatable.rb +79 -0
- data/lib/api_authentication_gem/configuration.rb +107 -4
- data/lib/api_authentication_gem/errors.rb +5 -0
- data/lib/api_authentication_gem/models/auth_session.rb +18 -0
- data/lib/api_authentication_gem/models/lockout.rb +21 -0
- data/lib/api_authentication_gem/models/magic_link.rb +22 -0
- data/lib/api_authentication_gem/models/password_reset.rb +23 -0
- data/lib/api_authentication_gem/models/refresh_token.rb +18 -0
- data/lib/api_authentication_gem/models/revoked_token.rb +19 -0
- data/lib/api_authentication_gem/version.rb +1 -1
- data/lib/api_authentication_gem.rb +14 -0
- metadata +106 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47dd157bb015ccb6a5874bfc4201f1df1b458a91da0bdb182249817f8faa2cda
|
|
4
|
+
data.tar.gz: 41a333d3705fb4c37ae5e2d07cc13dabfbe120fc9ed633321de0358c61c3ab98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
1
|
+
# api_authentication_gem
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
5
|
+
[](https://rubygems.org/gems/api_authentication_gem)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## What's included
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
##
|
|
25
|
+
## Installation
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
```ruby
|
|
28
|
+
# Gemfile
|
|
29
|
+
gem 'api_authentication_gem'
|
|
30
|
+
```
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# config/initializers/api_authentication_gem.rb
|
|
32
|
+
```bash
|
|
33
|
+
bundle install
|
|
34
|
+
```
|
|
32
35
|
|
|
33
|
-
|
|
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
|
|
36
51
|
end
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|