devise-api 0.2.0 → 0.3.1
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/.claude/commands/commit.md +38 -0
- data/.claude/commands/create-branch.md +57 -0
- data/.claude/commands/create-pr.md +41 -0
- data/CHANGELOG.md +60 -1
- data/CLAUDE.md +49 -0
- data/Gemfile +9 -3
- data/Gemfile.lock +257 -185
- data/README.md +320 -162
- data/Rakefile +6 -0
- data/app/controllers/devise/api/tokens_controller.rb +48 -108
- data/app/services/devise/api/resource_owner_service/authenticate.rb +9 -2
- data/app/services/devise/api/tokens_service/create.rb +16 -8
- data/app/services/devise/api/tokens_service/refresh.rb +17 -2
- data/app/services/devise/api/tokens_service/revoke.rb +1 -1
- data/config/locales/en.yml +1 -0
- data/docs/README.md +32 -0
- data/docs/analysis/known-issues.md +56 -0
- data/docs/analysis/security-review.md +53 -0
- data/docs/api-reference.md +95 -0
- data/docs/architecture.md +145 -0
- data/docs/configuration.md +84 -0
- data/docs/data-model.md +80 -0
- data/docs/development.md +52 -0
- data/docs/extending.md +75 -0
- data/docs/services.md +72 -0
- data/docs/testing.md +65 -0
- data/lib/devise/api/configuration.rb +7 -0
- data/lib/devise/api/controllers/helpers.rb +5 -3
- data/lib/devise/api/generators/templates/migration.rb.erb +2 -2
- data/lib/devise/api/rails/engine.rb +6 -0
- data/lib/devise/api/responses/error_response.rb +40 -29
- data/lib/devise/api/responses/token_response.rb +5 -1
- data/lib/devise/api/token.rb +34 -2
- data/lib/devise/api/version.rb +1 -1
- metadata +18 -7
- data/sig/devise/api.rbs +0 -6
data/README.md
CHANGED
|
@@ -1,205 +1,388 @@
|
|
|
1
|
+
# devise-api
|
|
2
|
+
|
|
3
|
+
Token-based API authentication for [Devise](https://github.com/heartcombo/devise). Opaque access + refresh tokens, one `devise :api` module, zero Warden strategies to write.
|
|
4
|
+
|
|
1
5
|
[](https://badge.fury.io/rb/devise-api)
|
|
2
6
|

|
|
3
7
|

|
|
4
8
|
[](https://github.com/rubocop/rubocop)
|
|
5
9
|

|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
`devise-api` is a Rails engine that plugs into Devise's own extension mechanism. Add `:api` to your Devise model and you get sign-up, sign-in, token refresh, revocation, and an authenticated info endpoint — plus controller helpers (`authenticate_devise_api_token!`, `current_devise_api_user`) for protecting the rest of your API.
|
|
13
|
+
|
|
14
|
+
**Highlights**
|
|
15
|
+
|
|
16
|
+
- 🔑 **Opaque access + refresh tokens** stored in your database — revocable at any time, no JWT invalidation headaches
|
|
17
|
+
- 🔁 **Refresh token rotation** with reuse detection (a replayed rotated token revokes the whole token family)
|
|
18
|
+
- 🧩 **Plays well with Devise modules** — `lockable`, `confirmable`, `trackable` are detected and honored automatically
|
|
19
|
+
- ⚙️ **Fully configurable** — token TTLs and generators, paranoid mode, header/params extraction, per-action callbacks, and swappable base classes for the token model and controller
|
|
20
|
+
- 🧱 **Service objects built on dry-monads** — every endpoint delegates to a composable, overridable service
|
|
21
|
+
- 📚 **Documented for humans and AI agents** — [`docs/`](docs/README.md) holds contractual architecture, API, and configuration references
|
|
22
|
+
|
|
23
|
+
## Table of contents
|
|
24
|
+
|
|
25
|
+
- [How it works](#how-it-works)
|
|
26
|
+
- [Requirements](#requirements)
|
|
27
|
+
- [Quick start](#quick-start)
|
|
28
|
+
- [Endpoints](#endpoints)
|
|
29
|
+
- [Protecting your own endpoints](#protecting-your-own-endpoints)
|
|
30
|
+
- [Response payloads](#response-payloads)
|
|
31
|
+
- [Configuration](#configuration)
|
|
32
|
+
- [Security checklist](#security-checklist)
|
|
33
|
+
- [Devise module compatibility](#devise-module-compatibility)
|
|
34
|
+
- [Customization](#customization)
|
|
35
|
+
- [Documentation](#documentation)
|
|
36
|
+
- [Development](#development)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
A client signs in once, then uses a short-lived access token per request and a longer-lived refresh token to get new access tokens without re-sending credentials:
|
|
43
|
+
|
|
44
|
+
```mermaid
|
|
45
|
+
sequenceDiagram
|
|
46
|
+
autonumber
|
|
47
|
+
participant Client
|
|
48
|
+
participant API as Your Rails API
|
|
49
|
+
participant DB as devise_api_tokens
|
|
50
|
+
|
|
51
|
+
Client->>API: POST /users/tokens/sign_in (email + password)
|
|
52
|
+
API->>DB: create token pair
|
|
53
|
+
API-->>Client: 200 { token, refresh_token, expires_in, resource_owner }
|
|
54
|
+
|
|
55
|
+
loop While access token is valid
|
|
56
|
+
Client->>API: GET /your/endpoints (Authorization: Bearer <access token>)
|
|
57
|
+
API-->>Client: 200 your data
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Client->>API: GET /your/endpoints (expired access token)
|
|
61
|
+
API-->>Client: 401 { "error": "expired_token" }
|
|
6
62
|
|
|
7
|
-
|
|
8
|
-
|
|
63
|
+
Client->>API: POST /users/tokens/refresh (Authorization: Bearer <refresh token>)
|
|
64
|
+
API->>DB: mint new pair (rotation: revoke presented token)
|
|
65
|
+
API-->>Client: 200 { token, refresh_token, ... }
|
|
66
|
+
|
|
67
|
+
Client->>API: POST /users/tokens/revoke (Authorization: Bearer <access token>)
|
|
68
|
+
API->>DB: mark revoked
|
|
69
|
+
API-->>Client: 204 No Content
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Tokens are opaque random strings (`Devise.friendly_token` by default) persisted in a `devise_api_tokens` table with a polymorphic `resource_owner`, so one table serves any number of Devise scopes (`User`, `Customer`, …). A token is **active** only while it is neither expired nor revoked:
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
stateDiagram-v2
|
|
76
|
+
[*] --> Active: sign_up / sign_in / refresh
|
|
77
|
+
Active --> Expired: access_token.expires_in elapses
|
|
78
|
+
Active --> Revoked: POST /tokens/revoke
|
|
79
|
+
Active --> Revoked: rotation on refresh
|
|
80
|
+
Expired --> [*]: refresh (mints a new pair)
|
|
81
|
+
Revoked --> [*]
|
|
82
|
+
note right of Revoked
|
|
83
|
+
Reuse detection: presenting a rotated
|
|
84
|
+
refresh token again revokes the
|
|
85
|
+
entire token family
|
|
86
|
+
end note
|
|
87
|
+
```
|
|
9
88
|
|
|
10
|
-
|
|
89
|
+
For the full component map and request lifecycle diagrams, see [docs/architecture.md](docs/architecture.md).
|
|
11
90
|
|
|
12
|
-
|
|
13
|
-
- The access token is included in the API request headers and is used to authenticate the user on each subsequent request.
|
|
14
|
-
- The refresh token is stored on the client side (e.g. in a browser cookie or on a mobile device) and is used to obtain a new access token when the original access token expires.
|
|
15
|
-
- This allows the user to remain logged in and make API requests without having to constantly re-enter their login credentials.
|
|
91
|
+
## Requirements
|
|
16
92
|
|
|
17
|
-
|
|
93
|
+
| Dependency | Version |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Ruby | >= 2.7 |
|
|
96
|
+
| Rails | >= 6.0 |
|
|
97
|
+
| Devise | >= 4.7.2 |
|
|
18
98
|
|
|
19
|
-
##
|
|
99
|
+
## Quick start
|
|
100
|
+
|
|
101
|
+
**1. Install the gem**
|
|
20
102
|
|
|
21
|
-
Install the gem and add to the application's Gemfile by executing:
|
|
22
103
|
```bash
|
|
23
|
-
|
|
104
|
+
bundle add devise-api
|
|
24
105
|
```
|
|
25
106
|
|
|
26
|
-
Or
|
|
107
|
+
Or track `main` from your Gemfile:
|
|
108
|
+
|
|
27
109
|
```ruby
|
|
28
110
|
gem 'devise-api', github: 'nejdetkadir/devise-api', branch: 'main'
|
|
29
111
|
```
|
|
30
112
|
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
gem install devise-api
|
|
34
|
-
```
|
|
113
|
+
**2. Generate the migration and locales**
|
|
35
114
|
|
|
36
|
-
After that, you need to generate relevant migrations and locales by executing:
|
|
37
115
|
```bash
|
|
38
|
-
|
|
116
|
+
rails generate devise_api:install
|
|
117
|
+
rails db:migrate
|
|
39
118
|
```
|
|
40
119
|
|
|
41
|
-
This
|
|
42
|
-
- Locale files in `config/locales/devise_api.en.yml`
|
|
43
|
-
- Migration file in `db/migrate` to create devise api tokens table
|
|
120
|
+
This copies a migration for the `devise_api_tokens` table and the locale file `config/locales/devise_api.en.yml` into your app.
|
|
44
121
|
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
$ rails db:migrate
|
|
48
|
-
```
|
|
122
|
+
**3. Add the `:api` module to your Devise model**
|
|
49
123
|
|
|
50
|
-
Finally, you need to add `:api` module to your devise model. For example:
|
|
51
124
|
```ruby
|
|
52
125
|
class User < ApplicationRecord
|
|
53
|
-
devise :database_authenticatable,
|
|
54
|
-
:registerable,
|
|
126
|
+
devise :database_authenticatable,
|
|
127
|
+
:registerable,
|
|
55
128
|
:recoverable,
|
|
56
129
|
:rememberable,
|
|
57
130
|
:validatable,
|
|
58
|
-
:api # <---
|
|
131
|
+
:api # <--- add this
|
|
59
132
|
end
|
|
60
133
|
```
|
|
61
134
|
|
|
62
|
-
|
|
135
|
+
That's it — your existing `devise_for :users` in `config/routes.rb` now draws the token endpoints automatically.
|
|
63
136
|
|
|
64
|
-
|
|
65
|
-
|--------|------|------------|--------------------------|
|
|
66
|
-
| revoke_user_tokens | POST | /users/tokens/revoke | devise/api/tokens#revoke |
|
|
67
|
-
| refresh_user_tokens | POST | /users/tokens/refresh | devise/api/tokens#refresh |
|
|
68
|
-
| sign_up_user_tokens | POST | /users/tokens/sign_up | devise/api/tokens#sign_up |
|
|
69
|
-
| sign_in_user_tokens | POST | /users/tokens/sign_in | devise/api/tokens#sign_in |
|
|
70
|
-
| info_user_tokens | GET | /users/tokens/info | devise/api/tokens#info |
|
|
137
|
+
**4. Try it**
|
|
71
138
|
|
|
72
|
-
|
|
139
|
+
```bash
|
|
140
|
+
curl -X POST http://localhost:3000/users/tokens/sign_in \
|
|
141
|
+
-H 'Content-Type: application/json' \
|
|
142
|
+
-d '{ "email": "test@example.com", "password": "123456" }'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"token": "ACCESS_TOKEN",
|
|
148
|
+
"refresh_token": "REFRESH_TOKEN",
|
|
149
|
+
"expires_in": 3600,
|
|
150
|
+
"token_type": "Bearer",
|
|
151
|
+
"resource_owner": { "id": 1, "email": "test@example.com", "created_at": "...", "updated_at": "..." }
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Endpoints
|
|
156
|
+
|
|
157
|
+
Drawn under `/<scope>/tokens` for every Devise scope whose model includes `:api` (examples use `devise_for :users`):
|
|
158
|
+
|
|
159
|
+
| Verb | Path | Purpose | Auth |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `POST` | `/users/tokens/sign_up` | Register and get a token pair | — |
|
|
162
|
+
| `POST` | `/users/tokens/sign_in` | Authenticate and get a token pair | — |
|
|
163
|
+
| `POST` | `/users/tokens/refresh` | Exchange a refresh token for a new pair | refresh token |
|
|
164
|
+
| `POST` | `/users/tokens/revoke` | Revoke the presented token | access token |
|
|
165
|
+
| `GET` | `/users/tokens/info` | Current resource owner details | access token |
|
|
166
|
+
|
|
167
|
+
All tokens — including the refresh token for `/refresh` — travel in the same slot: the `Authorization: Bearer <token>` header and/or an `access_token` param, depending on `authorization.location` (see [Configuration](#configuration)).
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Sign up
|
|
171
|
+
curl -X POST http://localhost:3000/users/tokens/sign_up \
|
|
172
|
+
-H 'Content-Type: application/json' \
|
|
173
|
+
-d '{ "email": "test@example.com", "password": "123456" }'
|
|
174
|
+
|
|
175
|
+
# Refresh (note: the REFRESH token goes in the Authorization header)
|
|
176
|
+
curl -X POST http://localhost:3000/users/tokens/refresh \
|
|
177
|
+
-H 'Authorization: Bearer REFRESH_TOKEN'
|
|
178
|
+
|
|
179
|
+
# Revoke
|
|
180
|
+
curl -X POST http://localhost:3000/users/tokens/revoke \
|
|
181
|
+
-H 'Authorization: Bearer ACCESS_TOKEN'
|
|
182
|
+
|
|
183
|
+
# Info
|
|
184
|
+
curl http://localhost:3000/users/tokens/info \
|
|
185
|
+
-H 'Authorization: Bearer ACCESS_TOKEN'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Route paths and the controller are customizable through the standard `devise_for` options:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# config/routes.rb
|
|
192
|
+
Rails.application.routes.draw do
|
|
193
|
+
devise_for :customers, controllers: { tokens: 'customers/api/tokens' }
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Protecting your own endpoints
|
|
198
|
+
|
|
199
|
+
The gem mixes three helpers into **every** controller:
|
|
200
|
+
|
|
201
|
+
| Helper | Returns |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `authenticate_devise_api_token!` | Renders a 401 error response unless a valid, active access token is presented |
|
|
204
|
+
| `current_devise_api_token` | The active `Devise::Api::Token` (or `nil`) |
|
|
205
|
+
| `current_devise_api_user` | The token's resource owner (or `nil`) — works for any scope, despite the name |
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# app/controllers/api/v1/orders_controller.rb
|
|
209
|
+
class Api::V1::OrdersController < ApplicationController
|
|
210
|
+
skip_before_action :verify_authenticity_token, raise: false
|
|
211
|
+
before_action :authenticate_devise_api_token!
|
|
212
|
+
|
|
213
|
+
def index
|
|
214
|
+
render json: current_devise_api_user.orders, status: :ok
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def show
|
|
218
|
+
order = current_devise_api_token.resource_owner.orders.find(params[:id])
|
|
219
|
+
render json: order, status: :ok
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Response payloads
|
|
225
|
+
|
|
226
|
+
**Success** (`sign_in` 200, `sign_up` 201, `refresh` 200):
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"token": "...",
|
|
231
|
+
"refresh_token": "...",
|
|
232
|
+
"expires_in": 3600,
|
|
233
|
+
"token_type": "Bearer",
|
|
234
|
+
"resource_owner": { "id": 1, "email": "...", "created_at": "...", "updated_at": "..." }
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`info` returns just the `resource_owner` object; `revoke` returns `204 No Content`.
|
|
239
|
+
|
|
240
|
+
**Errors** are consistent JSON with a symbolic type and human-readable descriptions (translated via i18n):
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"error": "expired_token",
|
|
245
|
+
"error_description": ["Your token has expired. Please sign in again."]
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Common error types: `invalid_authentication` (401), `invalid_token` (401), `expired_token` (401), `revoked_token` (401), `expired_refresh_token` (401), `invalid_refresh_token` (400), `sign_up_disabled` (400), `resource_owner_create_error` (422). The complete catalog — every type, status, and trigger — lives in [docs/api-reference.md](docs/api-reference.md).
|
|
73
250
|
|
|
74
251
|
## Configuration
|
|
75
252
|
|
|
76
|
-
|
|
253
|
+
Everything is configured on a single global inside `Devise.setup`. All values shown are the defaults:
|
|
77
254
|
|
|
78
255
|
```ruby
|
|
79
256
|
# config/initializers/devise.rb
|
|
80
257
|
Devise.setup do |config|
|
|
81
258
|
config.api.configure do |api|
|
|
82
|
-
# Access
|
|
259
|
+
# Access token
|
|
83
260
|
api.access_token.expires_in = 1.hour
|
|
84
261
|
api.access_token.expires_in_infinite = ->(_resource_owner) { false }
|
|
85
262
|
api.access_token.generator = ->(_resource_owner) { Devise.friendly_token(60) }
|
|
86
263
|
|
|
87
|
-
|
|
88
|
-
# Refresh Token
|
|
264
|
+
# Refresh token
|
|
89
265
|
api.refresh_token.enabled = true
|
|
90
266
|
api.refresh_token.expires_in = 1.week
|
|
91
|
-
api.refresh_token.generator = ->(_resource_owner) { Devise.friendly_token(60) }
|
|
92
267
|
api.refresh_token.expires_in_infinite = ->(_resource_owner) { false }
|
|
268
|
+
api.refresh_token.generator = ->(_resource_owner) { Devise.friendly_token(60) }
|
|
269
|
+
api.refresh_token.rotation_enabled = false # recommended: true (see Security checklist)
|
|
93
270
|
|
|
94
271
|
# Sign up
|
|
95
272
|
api.sign_up.enabled = true
|
|
96
|
-
api.sign_up.extra_fields = []
|
|
273
|
+
api.sign_up.extra_fields = [] # e.g. %i[first_name last_name] — writable at sign-up AND echoed in responses
|
|
97
274
|
|
|
98
|
-
#
|
|
275
|
+
# Error responses
|
|
276
|
+
api.error_response.verbose_account_state = true # false hides lockable/confirmable details from errors
|
|
277
|
+
api.paranoid = false # true makes unknown accounts indistinguishable from wrong passwords
|
|
278
|
+
|
|
279
|
+
# Token extraction
|
|
99
280
|
api.authorization.key = 'Authorization'
|
|
100
281
|
api.authorization.scheme = 'Bearer'
|
|
101
|
-
api.authorization.location = :both # :header
|
|
282
|
+
api.authorization.location = :both # :header, :params, or :both (params win)
|
|
102
283
|
api.authorization.params_key = 'access_token'
|
|
103
284
|
|
|
104
|
-
|
|
105
|
-
# Base classes
|
|
285
|
+
# Base classes (string names, constantized lazily — point at your own subclasses)
|
|
106
286
|
api.base_token_model = 'Devise::Api::Token'
|
|
107
287
|
api.base_controller = '::DeviseController'
|
|
108
288
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
api.
|
|
112
|
-
api.
|
|
113
|
-
api.
|
|
114
|
-
api.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
api.before_sign_in = ->(_params, _request, _resource_class) { }
|
|
119
|
-
api.before_sign_up = ->(_params, _request, _resource_class) { }
|
|
120
|
-
api.before_refresh = ->(_token_model, _request) { }
|
|
121
|
-
api.before_revoke = ->(_token_model, _request) { }
|
|
289
|
+
# Lifecycle hooks (all default to no-ops)
|
|
290
|
+
api.before_sign_in = ->(params, request, resource_class) {}
|
|
291
|
+
api.before_sign_up = ->(params, request, resource_class) {}
|
|
292
|
+
api.before_refresh = ->(token, request) {}
|
|
293
|
+
api.before_revoke = ->(token, request) {}
|
|
294
|
+
api.after_successful_sign_in = ->(resource_owner, token, request) {}
|
|
295
|
+
api.after_successful_sign_up = ->(resource_owner, token, request) {}
|
|
296
|
+
api.after_successful_refresh = ->(resource_owner, token, request) {}
|
|
297
|
+
api.after_successful_revoke = ->(resource_owner, token, request) {}
|
|
122
298
|
end
|
|
123
299
|
end
|
|
124
300
|
```
|
|
125
301
|
|
|
126
|
-
|
|
302
|
+
Settings are read at use time (never cached at boot), so changes take effect immediately — handy in tests. The full reference with types, defaults, and exactly which code consumes each setting is in [docs/configuration.md](docs/configuration.md).
|
|
127
303
|
|
|
128
|
-
|
|
129
|
-
```ruby
|
|
130
|
-
# config/routes.rb
|
|
131
|
-
Rails.application.routes.draw do
|
|
132
|
-
devise_for :customers,
|
|
133
|
-
controllers: { tokens: 'customers/api/tokens' }
|
|
134
|
-
end
|
|
135
|
-
```
|
|
304
|
+
## Security checklist
|
|
136
305
|
|
|
137
|
-
|
|
138
|
-
`devise-api` module works with `:lockable` and `:confirmable` modules. It also works with `:trackable` module.
|
|
306
|
+
Recommended production settings and guardrails:
|
|
139
307
|
|
|
140
|
-
|
|
308
|
+
- ✅ **Send tokens in the `Authorization` header only.** The default `authorization.location = :both` also accepts tokens as query/body params, and URLs leak into server logs, browser history, and `Referer` headers. Set `api.authorization.location = :header` unless you need params support.
|
|
309
|
+
- ✅ **Enable refresh token rotation** (`api.refresh_token.rotation_enabled = true`). Each refresh then revokes the presented refresh token, and replaying a rotated token revokes the entire token family (reuse detection).
|
|
310
|
+
- ✅ **Enable paranoid mode** (`api.paranoid = true`) to prevent account enumeration — unknown emails and wrong passwords return the same generic `invalid_authentication` error.
|
|
311
|
+
- ✅ **Rate limit the token endpoints.** The gem does not throttle `sign_in`/`sign_up`/`refresh`; put [rack-attack](https://github.com/rack/rack-attack) or an equivalent in front of them. Devise `lockable` only slows per-account brute force.
|
|
312
|
+
- ⚠️ **Audit `sign_up.extra_fields`.** Every listed field is mass-assignable at sign-up **and** echoed in every token/info response — never list privileged fields like `:role` or `:admin`.
|
|
313
|
+
- ⚠️ **Keep token values out of logs.** The gem adds `access_token`, `refresh_token`, and `previous_refresh_token` to `filter_parameters` and filters the token model's `#inspect`, but raw SQL logging (e.g. debug log level in production) can still print token values.
|
|
141
314
|
|
|
142
|
-
- [
|
|
315
|
+
The full threat-model review is in [docs/analysis/security-review.md](docs/analysis/security-review.md).
|
|
143
316
|
|
|
144
|
-
|
|
317
|
+
## Devise module compatibility
|
|
145
318
|
|
|
146
|
-
|
|
319
|
+
`devise-api` feature-detects the other modules on your model and adapts:
|
|
147
320
|
|
|
148
|
-
|
|
321
|
+
| Module | Behavior |
|
|
322
|
+
|---|---|
|
|
323
|
+
| `trackable` | `sign_in`/`sign_up` update the tracked fields (sign-in count, IPs, timestamps) |
|
|
324
|
+
| `lockable` | Failed sign-ins increment `failed_attempts`; lock state is reported in the error payload (unless paranoid/quiet); a successful sign-in resets the counter |
|
|
325
|
+
| `confirmable` | Unconfirmed users can sign **up** (they get tokens plus a `confirmable` notice in the response) but cannot sign **in** until confirmed |
|
|
326
|
+
|
|
327
|
+
## Customization
|
|
328
|
+
|
|
329
|
+
### Override the responses
|
|
330
|
+
|
|
331
|
+
Prepend a decorator module to `TokenResponse` or `ErrorResponse`:
|
|
149
332
|
|
|
150
|
-
## Overriding Responses
|
|
151
|
-
You can prepend your decorators to the response classes to override the default responses. For example:
|
|
152
333
|
```ruby
|
|
153
334
|
# app/lib/devise/api/responses/token_response_decorator.rb
|
|
154
335
|
module Devise::Api::Responses::TokenResponseDecorator
|
|
155
336
|
def body
|
|
156
|
-
|
|
337
|
+
default_body.merge({ roles: resource_owner.roles })
|
|
157
338
|
end
|
|
158
339
|
end
|
|
159
340
|
```
|
|
160
341
|
|
|
161
|
-
Then you need to load and prepend your decorator to the response class. For example:
|
|
162
|
-
|
|
163
342
|
```ruby
|
|
164
343
|
# config/initializers/devise.rb
|
|
165
|
-
require 'devise/api/responses/token_response_decorator'
|
|
166
|
-
|
|
167
|
-
Devise.setup do |config|
|
|
168
|
-
end
|
|
344
|
+
require 'devise/api/responses/token_response_decorator'
|
|
169
345
|
|
|
170
346
|
Devise::Api::Responses::TokenResponse.prepend Devise::Api::Responses::TokenResponseDecorator
|
|
171
347
|
```
|
|
172
348
|
|
|
173
|
-
|
|
174
|
-
`devise-api` provides a set of helpers to help you implement authentication in your Rails application. Here's a quick overview of the available helpers:
|
|
349
|
+
### Swap the base classes
|
|
175
350
|
|
|
176
|
-
|
|
177
|
-
```ruby
|
|
178
|
-
# app/controllers/api/v1/orders_controller.rb
|
|
179
|
-
class Api::V1::OrdersController < YourBaseController
|
|
180
|
-
skip_before_action :verify_authenticity_token, raise: false
|
|
181
|
-
before_action :authenticate_devise_api_token!
|
|
351
|
+
`base_token_model` and `base_controller` are stored as class *names* and resolved lazily, so you can subclass without load-order problems:
|
|
182
352
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
353
|
+
```ruby
|
|
354
|
+
# app/models/api_token.rb
|
|
355
|
+
class ApiToken < Devise::Api::Token
|
|
356
|
+
belongs_to :organization, optional: true
|
|
357
|
+
end
|
|
358
|
+
```
|
|
186
359
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
360
|
+
```ruby
|
|
361
|
+
# config/initializers/devise.rb
|
|
362
|
+
Devise.setup do |config|
|
|
363
|
+
config.api.configure do |api|
|
|
364
|
+
api.base_token_model = 'ApiToken'
|
|
365
|
+
api.base_controller = 'Api::BaseController'
|
|
190
366
|
end
|
|
191
367
|
end
|
|
192
368
|
```
|
|
193
369
|
|
|
194
|
-
|
|
195
|
-
`devise-api` provides a set of base services to help you implement authentication in your Rails application. Here's a quick overview of the available services:
|
|
370
|
+
### Hook into the lifecycle
|
|
196
371
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
372
|
+
The `before_*` / `after_successful_*` callbacks (see [Configuration](#configuration)) are handy for audit logging, analytics, or sending welcome emails:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
api.after_successful_sign_up = lambda { |resource_owner, _token, _request|
|
|
376
|
+
WelcomeMailer.with(user: resource_owner).welcome.deliver_later
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`before_*` return values are ignored — raise, or use a `before_action` in a subclassed controller, if you need to halt a request.
|
|
381
|
+
|
|
382
|
+
### Build your own services
|
|
383
|
+
|
|
384
|
+
Every endpoint delegates to a service object built on [dry-monads](https://dry-rb.org/gems/dry-monads), [dry-types](https://dry-rb.org/gems/dry-types), and [dry-initializer](https://dry-rb.org/gems/dry-initializer). Inherit from `Devise::Api::BaseService` to compose your own:
|
|
201
385
|
|
|
202
|
-
You can create a service by inheriting the `Devise::Api::BaseService` class. For example:
|
|
203
386
|
```ruby
|
|
204
387
|
# app/services/devise/api/tokens_service/v2/create.rb
|
|
205
388
|
module Devise::Api::TokensService::V2
|
|
@@ -208,85 +391,60 @@ module Devise::Api::TokensService::V2
|
|
|
208
391
|
option :resource_class, type: Types::Class, reader: true
|
|
209
392
|
|
|
210
393
|
def call
|
|
211
|
-
...
|
|
212
|
-
|
|
394
|
+
# ...
|
|
213
395
|
Success(resource)
|
|
214
396
|
end
|
|
215
397
|
end
|
|
216
398
|
end
|
|
217
399
|
```
|
|
218
400
|
|
|
219
|
-
Then you can call the service in your controller. For example:
|
|
220
401
|
```ruby
|
|
221
402
|
# app/controllers/api/v1/tokens_controller.rb
|
|
222
|
-
|
|
223
|
-
|
|
403
|
+
def create
|
|
404
|
+
result = Devise::Api::TokensService::V2::Create.new(params: params, resource_class: Customer).call
|
|
224
405
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
else
|
|
230
|
-
render json: service.failure, status: :unprocessable_entity
|
|
231
|
-
end
|
|
406
|
+
if result.success?
|
|
407
|
+
render json: result.success, status: :created
|
|
408
|
+
else
|
|
409
|
+
render json: result.failure, status: :unprocessable_entity
|
|
232
410
|
end
|
|
233
411
|
end
|
|
234
412
|
```
|
|
235
413
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
### Sign in
|
|
239
|
-
```curl
|
|
240
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_in' \
|
|
241
|
-
--header 'Content-Type: application/json' \
|
|
242
|
-
--data-raw '{
|
|
243
|
-
"email": "test@development.com",
|
|
244
|
-
"password": "123456"
|
|
245
|
-
}'
|
|
246
|
-
```
|
|
414
|
+
Service contracts (inputs, success/failure values, composition) are documented in [docs/services.md](docs/services.md), and all supported customization points in [docs/extending.md](docs/extending.md).
|
|
247
415
|
|
|
248
|
-
|
|
249
|
-
```curl
|
|
250
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_up' \
|
|
251
|
-
--header 'Content-Type: application/json' \
|
|
252
|
-
--data-raw '{
|
|
253
|
-
"email": "test@development.com",
|
|
254
|
-
"password": "123456"
|
|
255
|
-
}'
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### Refresh token
|
|
259
|
-
```curl
|
|
260
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/refresh' \
|
|
261
|
-
--header 'Authorization: Bearer <refresh_token>'
|
|
262
|
-
```
|
|
416
|
+
## Documentation
|
|
263
417
|
|
|
264
|
-
|
|
265
|
-
```curl
|
|
266
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/revoke' \
|
|
267
|
-
--header 'Authorization: Bearer <access_token>'
|
|
268
|
-
```
|
|
418
|
+
The [`docs/`](docs/README.md) directory is the source of truth for how the gem works internally — written for contributors **and** AI coding agents, and kept in sync with the code by convention:
|
|
269
419
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
420
|
+
| Document | Contents |
|
|
421
|
+
|---|---|
|
|
422
|
+
| [architecture.md](docs/architecture.md) | Component map, boot sequence, request lifecycle (with diagrams) |
|
|
423
|
+
| [api-reference.md](docs/api-reference.md) | Every endpoint, payload, and the full error catalog |
|
|
424
|
+
| [configuration.md](docs/configuration.md) | Every setting: type, default, and where it is consumed |
|
|
425
|
+
| [data-model.md](docs/data-model.md) | `devise_api_tokens` schema, token state machine, refresh chains |
|
|
426
|
+
| [services.md](docs/services.md) | Service-object contracts and composition |
|
|
427
|
+
| [extending.md](docs/extending.md) | Supported customization points |
|
|
428
|
+
| [testing.md](docs/testing.md) / [development.md](docs/development.md) | Test layout, dummy app, CI, release process |
|
|
429
|
+
| [analysis/](docs/analysis/security-review.md) | Security review and vetted known-issues backlog |
|
|
275
430
|
|
|
276
431
|
## Development
|
|
277
432
|
|
|
278
|
-
|
|
433
|
+
```bash
|
|
434
|
+
bin/setup # install dependencies
|
|
435
|
+
bundle exec rake # what CI runs: RSpec + RuboCop
|
|
436
|
+
bundle exec rake rspec # tests only
|
|
437
|
+
bin/console # interactive prompt
|
|
438
|
+
```
|
|
279
439
|
|
|
280
|
-
To install
|
|
440
|
+
Tests run against the dummy Rails app in `spec/dummy`. To install the gem locally run `bundle exec rake install`; to release, bump `version.rb` and run `bundle exec rake release`.
|
|
281
441
|
|
|
282
442
|
## Contributing
|
|
283
443
|
|
|
284
|
-
Bug reports and pull requests are welcome on GitHub
|
|
444
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/nejdetkadir/devise-api). Please read [docs/README.md](docs/README.md) for the ground rules (docs are contractual — behavior changes must update the matching document) and check the [known-issues backlog](docs/analysis/known-issues.md) before "fixing" surprising code.
|
|
445
|
+
|
|
446
|
+
This project is intended to be a safe, welcoming space for collaboration; contributors are expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
285
447
|
|
|
286
448
|
## License
|
|
287
449
|
|
|
288
450
|
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
289
|
-
|
|
290
|
-
## Code of Conduct
|
|
291
|
-
|
|
292
|
-
Everyone interacting in the Devise::Api project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nejdetkadir/devise-api/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -5,6 +5,12 @@ require 'rspec/core/rake_task'
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:rspec)
|
|
7
7
|
|
|
8
|
+
# Full-suite runs enforce the SimpleCov minimum; single-file `rspec` runs do not.
|
|
9
|
+
task :enforce_coverage do
|
|
10
|
+
ENV['ENFORCE_COVERAGE'] = '1'
|
|
11
|
+
end
|
|
12
|
+
task rspec: :enforce_coverage
|
|
13
|
+
|
|
8
14
|
require 'rubocop/rake_task'
|
|
9
15
|
|
|
10
16
|
RuboCop::RakeTask.new
|