fastify-siwe-middleware 1.0.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.
- package/README.md +284 -0
- package/dist/index.cjs +27166 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +27161 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# fastify-siwe-middleware
|
|
2
|
+
|
|
3
|
+
Fastify middleware helpers for SIWE-based auth flows.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install fastify-siwe-middleware
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Public API
|
|
12
|
+
|
|
13
|
+
The package exports three middleware factories from the package root:
|
|
14
|
+
|
|
15
|
+
- `jwtGuard`
|
|
16
|
+
- `onChainGuard`
|
|
17
|
+
- `roleGuard`
|
|
18
|
+
|
|
19
|
+
It also exports the shared auth types:
|
|
20
|
+
|
|
21
|
+
- `AuthUser`
|
|
22
|
+
- `AuthJwtPayload`
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import fastify from "fastify";
|
|
28
|
+
import {
|
|
29
|
+
jwtGuard,
|
|
30
|
+
onChainGuard,
|
|
31
|
+
roleGuard,
|
|
32
|
+
} from "fastify-siwe-middleware";
|
|
33
|
+
|
|
34
|
+
const app = fastify();
|
|
35
|
+
|
|
36
|
+
const authGuard = jwtGuard({
|
|
37
|
+
secret: process.env.JWT_SECRET!,
|
|
38
|
+
isSessionActive: async (sessionId) => {
|
|
39
|
+
return sessionId.length > 0;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
app.get("/private", { preHandler: [authGuard] }, async (req) => {
|
|
44
|
+
return { address: req.user.address };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.get(
|
|
48
|
+
"/token-gated",
|
|
49
|
+
{
|
|
50
|
+
preHandler: [
|
|
51
|
+
authGuard,
|
|
52
|
+
onChainGuard({
|
|
53
|
+
rpcUrl: process.env.RPC_URL!,
|
|
54
|
+
contractAddress: "0x0000000000000000000000000000000000000000",
|
|
55
|
+
minBalance: 1n,
|
|
56
|
+
}),
|
|
57
|
+
roleGuard({
|
|
58
|
+
allowlist: ["0x1234567890abcdef1234567890abcdef12345678"],
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
async () => {
|
|
63
|
+
return { ok: true };
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Served routes
|
|
69
|
+
|
|
70
|
+
When running this application as an auth service, it exposes the routes below.
|
|
71
|
+
|
|
72
|
+
### `GET /docs`
|
|
73
|
+
|
|
74
|
+
- Auth: none
|
|
75
|
+
- Description: Interactive Swagger UI for all API routes.
|
|
76
|
+
|
|
77
|
+
### `GET /docs/json`
|
|
78
|
+
|
|
79
|
+
- Auth: none
|
|
80
|
+
- Description: Generated OpenAPI JSON document used by Swagger UI.
|
|
81
|
+
|
|
82
|
+
### `GET /`
|
|
83
|
+
|
|
84
|
+
- Auth: none
|
|
85
|
+
- Response `200`: `{ "hello": "world" }`
|
|
86
|
+
|
|
87
|
+
### `GET /auth/nonce`
|
|
88
|
+
|
|
89
|
+
- Auth: none
|
|
90
|
+
- Query params:
|
|
91
|
+
- `address` (required): EVM address, pattern `0x[a-fA-F0-9]{40}`
|
|
92
|
+
- Response `200`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{ "nonce": "<hex nonce>" }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Notes:
|
|
99
|
+
- Address is normalized to lowercase before nonce storage.
|
|
100
|
+
- Route-level rate limit uses `NONCE_RATE_LIMIT_MAX` and `NONCE_RATE_LIMIT_WINDOW_MS`.
|
|
101
|
+
|
|
102
|
+
### `POST /auth/verify`
|
|
103
|
+
|
|
104
|
+
- Auth: none
|
|
105
|
+
- Body:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"message": "<SIWE message>",
|
|
110
|
+
"signature": "0x..."
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- Validation:
|
|
115
|
+
- `message`: string, 20-4096 chars
|
|
116
|
+
- `signature`: 132 chars, pattern `^0x[a-fA-F0-9]{130}$`
|
|
117
|
+
- Response `200`:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"accessToken": "<jwt>",
|
|
122
|
+
"refreshToken": "<refresh-token>"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- Common errors:
|
|
127
|
+
- `400 { "error": "Invalid domain" }` when SIWE domain mismatches `ALLOWED_DOMAIN`
|
|
128
|
+
- `400 { "error": "Invalid chain" }` when `CHAIN_ID` is set and does not match
|
|
129
|
+
- `400 { "error": "Invalid nonce" }` when nonce is missing/consumed/mismatched
|
|
130
|
+
- `401 { "error": "Authentication failed" }` when SIWE message/signature verification fails
|
|
131
|
+
- Notes:
|
|
132
|
+
- Route-level rate limit uses `VERIFY_RATE_LIMIT_MAX` and `VERIFY_RATE_LIMIT_WINDOW_MS`.
|
|
133
|
+
|
|
134
|
+
### `POST /auth/refresh`
|
|
135
|
+
|
|
136
|
+
- Auth: none
|
|
137
|
+
- Body:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"refreshToken": "<refresh-token>"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- Validation:
|
|
146
|
+
- `refreshToken`: string, 16-2048 chars
|
|
147
|
+
- Response `200`:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"accessToken": "<jwt>",
|
|
152
|
+
"refreshToken": "<new-refresh-token>"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- Common errors:
|
|
157
|
+
- `401 { "error": "Invalid refresh token" }`
|
|
158
|
+
- `500 { "error": "Unable to refresh token at this time" }`
|
|
159
|
+
- Notes:
|
|
160
|
+
- Route-level rate limit uses `REFRESH_RATE_LIMIT_MAX` and `REFRESH_RATE_LIMIT_WINDOW_MS`.
|
|
161
|
+
|
|
162
|
+
### `GET /auth/me`
|
|
163
|
+
|
|
164
|
+
- Auth: `Authorization: Bearer <accessToken>`
|
|
165
|
+
- Guard behavior:
|
|
166
|
+
- JWT must be valid
|
|
167
|
+
- Session must still be active (not revoked/expired)
|
|
168
|
+
- Response `200`:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"address": "0x...",
|
|
173
|
+
"ensName": "alice.eth",
|
|
174
|
+
"firstSeen": "2026-01-01T00:00:00.000Z",
|
|
175
|
+
"sessionCount": 3
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- Common errors:
|
|
180
|
+
- `401 { "error": "Missing token" }`
|
|
181
|
+
- `401 { "error": "Invalid token" }`
|
|
182
|
+
|
|
183
|
+
### `DELETE /auth/logout`
|
|
184
|
+
|
|
185
|
+
- Auth: `Authorization: Bearer <accessToken>`
|
|
186
|
+
- Guard behavior:
|
|
187
|
+
- JWT must be valid
|
|
188
|
+
- Session must still be active
|
|
189
|
+
- Response `200`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{ "success": true }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Common errors:
|
|
196
|
+
- `401 { "error": "Missing token" }`
|
|
197
|
+
- `401 { "error": "Invalid token" }`
|
|
198
|
+
|
|
199
|
+
### Global behavior
|
|
200
|
+
|
|
201
|
+
- Global rate limit is enabled via:
|
|
202
|
+
- `RATE_LIMIT_MAX`
|
|
203
|
+
- `RATE_LIMIT_WINDOW_MS`
|
|
204
|
+
- Request body size is limited by `BODY_LIMIT_BYTES`.
|
|
205
|
+
|
|
206
|
+
## Self-host
|
|
207
|
+
|
|
208
|
+
This project can be run as a standalone auth service. The application listens on port `3000`, runs migrations on startup, and requires PostgreSQL and Redis.
|
|
209
|
+
|
|
210
|
+
Release builds are published as GitHub Container Registry images on each release. Use the release tag you want to pin to, or `latest` if you intentionally want the most recent release.
|
|
211
|
+
|
|
212
|
+
### Docker image
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
docker pull ghcr.io/ajeanselme/fastify-siwe-middleware:latest
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Required environment variables
|
|
219
|
+
|
|
220
|
+
The service requires these environment variables at startup:
|
|
221
|
+
|
|
222
|
+
- `DB_HOST`
|
|
223
|
+
- `DB_PORT`
|
|
224
|
+
- `DB_USER`
|
|
225
|
+
- `DB_PASSWORD`
|
|
226
|
+
- `DB_DATABASE`
|
|
227
|
+
- `REDIS_HOST`
|
|
228
|
+
- `JWT_SECRET`
|
|
229
|
+
- `ALLOWED_DOMAIN`
|
|
230
|
+
|
|
231
|
+
Optional variables:
|
|
232
|
+
|
|
233
|
+
- `DB_SCHEMA`
|
|
234
|
+
- `REDIS_PORT`
|
|
235
|
+
- `REDIS_PASSWORD`
|
|
236
|
+
- `REDIS_DATABASE`
|
|
237
|
+
- `CHAIN_ID`
|
|
238
|
+
- `RPC_URL`
|
|
239
|
+
- `BODY_LIMIT_BYTES`
|
|
240
|
+
- `RATE_LIMIT_MAX`
|
|
241
|
+
- `RATE_LIMIT_WINDOW_MS`
|
|
242
|
+
- `NONCE_RATE_LIMIT_MAX`
|
|
243
|
+
- `NONCE_RATE_LIMIT_WINDOW_MS`
|
|
244
|
+
- `VERIFY_RATE_LIMIT_MAX`
|
|
245
|
+
- `VERIFY_RATE_LIMIT_WINDOW_MS`
|
|
246
|
+
- `REFRESH_RATE_LIMIT_MAX`
|
|
247
|
+
- `REFRESH_RATE_LIMIT_WINDOW_MS`
|
|
248
|
+
- `REFRESH_TOKEN_TTL_SECONDS`
|
|
249
|
+
|
|
250
|
+
### Example compose setup
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
services:
|
|
254
|
+
app:
|
|
255
|
+
image: ghcr.io/ajeanselme/fastify-siwe-middleware:latest
|
|
256
|
+
ports: ["3000:3000"]
|
|
257
|
+
env_file: .env
|
|
258
|
+
depends_on: [postgres, redis]
|
|
259
|
+
|
|
260
|
+
postgres:
|
|
261
|
+
image: postgres:16-alpine
|
|
262
|
+
environment:
|
|
263
|
+
POSTGRES_DB: siwe_auth
|
|
264
|
+
POSTGRES_USER: siwe
|
|
265
|
+
POSTGRES_PASSWORD: secret
|
|
266
|
+
volumes: [pg_data:/var/lib/postgresql/data]
|
|
267
|
+
|
|
268
|
+
redis:
|
|
269
|
+
image: redis:7-alpine
|
|
270
|
+
command: redis-server --save ""
|
|
271
|
+
|
|
272
|
+
volumes:
|
|
273
|
+
pg_data:
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
If you prefer local builds, the included [docker-compose.yml](docker-compose.yml) still works unchanged.
|
|
277
|
+
|
|
278
|
+
## Build
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
npm run build
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
This emits ESM, CommonJS, and declaration files in `dist/`.
|