devise-api 0.1.3 → 0.3.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/.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 +54 -1
- data/CLAUDE.md +49 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +11 -1
- data/README.md +320 -159
- data/Rakefile +6 -0
- data/app/controllers/devise/api/tokens_controller.rb +50 -109
- 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 +8 -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 +15 -7
- data/lib/devise/api/token.rb +34 -2
- data/lib/devise/api/version.rb +1 -1
- metadata +18 -4
- data/sig/devise/api.rbs +0 -6
data/README.md
CHANGED
|
@@ -1,202 +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" }
|
|
62
|
+
|
|
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
|
+
```
|
|
6
71
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
```
|
|
88
|
+
|
|
89
|
+
For the full component map and request lifecycle diagrams, see [docs/architecture.md](docs/architecture.md).
|
|
9
90
|
|
|
10
|
-
|
|
91
|
+
## Requirements
|
|
11
92
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
93
|
+
| Dependency | Version |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Ruby | >= 2.7 |
|
|
96
|
+
| Rails | >= 6.0 |
|
|
97
|
+
| Devise | >= 4.7.2 |
|
|
16
98
|
|
|
17
|
-
|
|
99
|
+
## Quick start
|
|
18
100
|
|
|
19
|
-
|
|
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
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That's it — your existing `devise_for :users` in `config/routes.rb` now draws the token endpoints automatically.
|
|
136
|
+
|
|
137
|
+
**4. Try it**
|
|
138
|
+
|
|
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
|
|
59
221
|
end
|
|
60
222
|
```
|
|
61
223
|
|
|
62
|
-
|
|
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`.
|
|
63
239
|
|
|
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 |
|
|
240
|
+
**Errors** are consistent JSON with a symbolic type and human-readable descriptions (translated via i18n):
|
|
71
241
|
|
|
72
|
-
|
|
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
|
|
273
|
+
api.sign_up.extra_fields = [] # e.g. %i[first_name last_name] — writable at sign-up AND echoed in responses
|
|
274
|
+
|
|
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
|
|
96
278
|
|
|
97
|
-
#
|
|
279
|
+
# Token extraction
|
|
98
280
|
api.authorization.key = 'Authorization'
|
|
99
281
|
api.authorization.scheme = 'Bearer'
|
|
100
|
-
api.authorization.location = :both # :header
|
|
282
|
+
api.authorization.location = :both # :header, :params, or :both (params win)
|
|
101
283
|
api.authorization.params_key = 'access_token'
|
|
102
284
|
|
|
103
|
-
|
|
104
|
-
# Base classes
|
|
285
|
+
# Base classes (string names, constantized lazily — point at your own subclasses)
|
|
105
286
|
api.base_token_model = 'Devise::Api::Token'
|
|
106
287
|
api.base_controller = '::DeviseController'
|
|
107
288
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
api.
|
|
111
|
-
api.
|
|
112
|
-
api.
|
|
113
|
-
api.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
api.before_sign_in = ->(_params, _request, _resource_class) { }
|
|
118
|
-
api.before_sign_up = ->(_params, _request, _resource_class) { }
|
|
119
|
-
api.before_refresh = ->(_params, _request, _resource_class) { }
|
|
120
|
-
api.before_revoke = ->(_params, _request, _resource_class) { }
|
|
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) {}
|
|
121
298
|
end
|
|
122
299
|
end
|
|
123
300
|
```
|
|
124
301
|
|
|
125
|
-
|
|
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).
|
|
126
303
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
304
|
+
## Security checklist
|
|
305
|
+
|
|
306
|
+
Recommended production settings and guardrails:
|
|
307
|
+
|
|
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.
|
|
314
|
+
|
|
315
|
+
The full threat-model review is in [docs/analysis/security-review.md](docs/analysis/security-review.md).
|
|
135
316
|
|
|
136
|
-
##
|
|
137
|
-
`devise-api` module works with `:lockable` and `:confirmable` modules. It also works with `:trackable` module.
|
|
317
|
+
## Devise module compatibility
|
|
138
318
|
|
|
139
|
-
`devise-api`
|
|
319
|
+
`devise-api` feature-detects the other modules on your model and adapts:
|
|
140
320
|
|
|
141
|
-
|
|
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 |
|
|
142
326
|
|
|
143
|
-
|
|
327
|
+
## Customization
|
|
144
328
|
|
|
145
|
-
|
|
329
|
+
### Override the responses
|
|
146
330
|
|
|
147
|
-
|
|
331
|
+
Prepend a decorator module to `TokenResponse` or `ErrorResponse`:
|
|
148
332
|
|
|
149
|
-
## Overriding Responses
|
|
150
|
-
You can prepend your decorators to the response classes to override the default responses. For example:
|
|
151
333
|
```ruby
|
|
152
334
|
# app/lib/devise/api/responses/token_response_decorator.rb
|
|
153
335
|
module Devise::Api::Responses::TokenResponseDecorator
|
|
154
336
|
def body
|
|
155
|
-
|
|
337
|
+
default_body.merge({ roles: resource_owner.roles })
|
|
156
338
|
end
|
|
157
339
|
end
|
|
158
340
|
```
|
|
159
341
|
|
|
160
|
-
Then you need to prepend your decorator to the response class. For example:
|
|
161
|
-
|
|
162
342
|
```ruby
|
|
163
343
|
# config/initializers/devise.rb
|
|
164
|
-
|
|
165
|
-
end
|
|
344
|
+
require 'devise/api/responses/token_response_decorator'
|
|
166
345
|
|
|
167
346
|
Devise::Api::Responses::TokenResponse.prepend Devise::Api::Responses::TokenResponseDecorator
|
|
168
347
|
```
|
|
169
348
|
|
|
170
|
-
|
|
171
|
-
`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
|
|
172
350
|
|
|
173
|
-
|
|
174
|
-
```ruby
|
|
175
|
-
# app/controllers/api/v1/orders_controller.rb
|
|
176
|
-
class Api::V1::OrdersController < YourBaseController
|
|
177
|
-
skip_before_action :verify_authenticity_token, raise: false
|
|
178
|
-
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:
|
|
179
352
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
353
|
+
```ruby
|
|
354
|
+
# app/models/api_token.rb
|
|
355
|
+
class ApiToken < Devise::Api::Token
|
|
356
|
+
belongs_to :organization, optional: true
|
|
357
|
+
end
|
|
358
|
+
```
|
|
183
359
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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'
|
|
187
366
|
end
|
|
188
367
|
end
|
|
189
368
|
```
|
|
190
369
|
|
|
191
|
-
|
|
192
|
-
`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
|
|
193
371
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
198
385
|
|
|
199
|
-
You can create a service by inheriting the `Devise::Api::BaseService` class. For example:
|
|
200
386
|
```ruby
|
|
201
387
|
# app/services/devise/api/tokens_service/v2/create.rb
|
|
202
388
|
module Devise::Api::TokensService::V2
|
|
@@ -205,85 +391,60 @@ module Devise::Api::TokensService::V2
|
|
|
205
391
|
option :resource_class, type: Types::Class, reader: true
|
|
206
392
|
|
|
207
393
|
def call
|
|
208
|
-
...
|
|
209
|
-
|
|
394
|
+
# ...
|
|
210
395
|
Success(resource)
|
|
211
396
|
end
|
|
212
397
|
end
|
|
213
398
|
end
|
|
214
399
|
```
|
|
215
400
|
|
|
216
|
-
Then you can call the service in your controller. For example:
|
|
217
401
|
```ruby
|
|
218
402
|
# app/controllers/api/v1/tokens_controller.rb
|
|
219
|
-
|
|
220
|
-
|
|
403
|
+
def create
|
|
404
|
+
result = Devise::Api::TokensService::V2::Create.new(params: params, resource_class: Customer).call
|
|
221
405
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
else
|
|
227
|
-
render json: service.failure, status: :unprocessable_entity
|
|
228
|
-
end
|
|
406
|
+
if result.success?
|
|
407
|
+
render json: result.success, status: :created
|
|
408
|
+
else
|
|
409
|
+
render json: result.failure, status: :unprocessable_entity
|
|
229
410
|
end
|
|
230
411
|
end
|
|
231
412
|
```
|
|
232
413
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
### Sign in
|
|
236
|
-
```curl
|
|
237
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_in' \
|
|
238
|
-
--header 'Content-Type: application/json' \
|
|
239
|
-
--data-raw '{
|
|
240
|
-
"email": "test@development.com",
|
|
241
|
-
"password": "123456"
|
|
242
|
-
}'
|
|
243
|
-
```
|
|
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).
|
|
244
415
|
|
|
245
|
-
|
|
246
|
-
```curl
|
|
247
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_up' \
|
|
248
|
-
--header 'Content-Type: application/json' \
|
|
249
|
-
--data-raw '{
|
|
250
|
-
"email": "test@development.com",
|
|
251
|
-
"password": "123456"
|
|
252
|
-
}'
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
### Refresh token
|
|
256
|
-
```curl
|
|
257
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/refresh' \
|
|
258
|
-
--header 'Authorization: Bearer <refresh_token>'
|
|
259
|
-
```
|
|
416
|
+
## Documentation
|
|
260
417
|
|
|
261
|
-
|
|
262
|
-
```curl
|
|
263
|
-
curl --location --request POST 'http://127.0.0.1:3000/users/tokens/revoke' \
|
|
264
|
-
--header 'Authorization: Bearer <access_token>'
|
|
265
|
-
```
|
|
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:
|
|
266
419
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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 |
|
|
272
430
|
|
|
273
431
|
## Development
|
|
274
432
|
|
|
275
|
-
|
|
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
|
+
```
|
|
276
439
|
|
|
277
|
-
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`.
|
|
278
441
|
|
|
279
442
|
## Contributing
|
|
280
443
|
|
|
281
|
-
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).
|
|
282
447
|
|
|
283
448
|
## License
|
|
284
449
|
|
|
285
450
|
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
286
|
-
|
|
287
|
-
## Code of Conduct
|
|
288
|
-
|
|
289
|
-
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
|