@lastshotlabs/bunshot 0.0.21 → 0.0.27
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 +3035 -1249
- package/dist/adapters/localStorage.d.ts +6 -0
- package/dist/adapters/localStorage.js +59 -0
- package/dist/adapters/memoryAuth.d.ts +13 -0
- package/dist/adapters/memoryAuth.js +261 -2
- package/dist/adapters/memoryStorage.d.ts +3 -0
- package/dist/adapters/memoryStorage.js +44 -0
- package/dist/adapters/mongoAuth.js +217 -1
- package/dist/adapters/s3Storage.d.ts +14 -0
- package/dist/adapters/s3Storage.js +126 -0
- package/dist/adapters/sqliteAuth.d.ts +30 -0
- package/dist/adapters/sqliteAuth.js +352 -2
- package/dist/app.d.ts +203 -3
- package/dist/app.js +352 -48
- package/dist/cli.js +118 -38
- package/dist/index.d.ts +69 -8
- package/dist/index.js +46 -5
- package/dist/lib/HttpError.d.ts +7 -1
- package/dist/lib/HttpError.js +10 -1
- package/dist/lib/appConfig.d.ts +157 -0
- package/dist/lib/appConfig.js +54 -0
- package/dist/lib/auditLog.d.ts +58 -0
- package/dist/lib/auditLog.js +218 -0
- package/dist/lib/authAdapter.d.ts +140 -1
- package/dist/lib/authRateLimit.js +36 -0
- package/dist/lib/breachedPassword.d.ts +13 -0
- package/dist/lib/breachedPassword.js +48 -0
- package/dist/lib/captcha.d.ts +25 -0
- package/dist/lib/captcha.js +37 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +24 -1
- package/dist/lib/context.js +17 -3
- package/dist/lib/createRoute.d.ts +28 -2
- package/dist/lib/createRoute.js +54 -3
- package/dist/lib/credentialStuffing.d.ts +31 -0
- package/dist/lib/credentialStuffing.js +77 -0
- package/dist/lib/deletionCancelToken.d.ts +12 -0
- package/dist/lib/deletionCancelToken.js +88 -0
- package/dist/lib/emailVerification.d.ts +6 -0
- package/dist/lib/emailVerification.js +46 -3
- package/dist/lib/groups.d.ts +113 -0
- package/dist/lib/groups.js +133 -0
- package/dist/lib/idempotency.d.ts +22 -0
- package/dist/lib/idempotency.js +182 -0
- package/dist/lib/jwks.d.ts +25 -0
- package/dist/lib/jwks.js +51 -0
- package/dist/lib/jwt.d.ts +15 -2
- package/dist/lib/jwt.js +92 -5
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +6 -0
- package/dist/lib/m2m.d.ts +29 -0
- package/dist/lib/m2m.js +48 -0
- package/dist/lib/metrics.d.ts +14 -0
- package/dist/lib/metrics.js +158 -0
- package/dist/lib/mfaChallenge.d.ts +14 -1
- package/dist/lib/mfaChallenge.js +111 -6
- package/dist/lib/mongo.js +1 -1
- package/dist/lib/oauthCode.js +23 -18
- package/dist/lib/pagination.d.ts +119 -0
- package/dist/lib/pagination.js +166 -0
- package/dist/lib/resetPassword.js +3 -1
- package/dist/lib/saml.d.ts +25 -0
- package/dist/lib/saml.js +64 -0
- package/dist/lib/scim.d.ts +44 -0
- package/dist/lib/scim.js +54 -0
- package/dist/lib/securityEvents.d.ts +28 -0
- package/dist/lib/securityEvents.js +26 -0
- package/dist/lib/session.d.ts +14 -0
- package/dist/lib/session.js +121 -5
- package/dist/lib/signing.d.ts +52 -0
- package/dist/lib/signing.js +183 -0
- package/dist/lib/storageAdapter.d.ts +30 -0
- package/dist/lib/storageAdapter.js +1 -0
- package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
- package/dist/lib/stripUnreferencedSchemas.js +79 -0
- package/dist/lib/suspension.d.ts +13 -0
- package/dist/lib/suspension.js +23 -0
- package/dist/lib/tenant.js +2 -2
- package/dist/lib/upload.d.ts +39 -0
- package/dist/lib/upload.js +112 -0
- package/dist/lib/uploadRegistry.d.ts +18 -0
- package/dist/lib/uploadRegistry.js +83 -0
- package/dist/lib/validate.js +2 -2
- package/dist/lib/ws.d.ts +1 -0
- package/dist/lib/ws.js +28 -0
- package/dist/lib/wsHeartbeat.d.ts +12 -0
- package/dist/lib/wsHeartbeat.js +57 -0
- package/dist/lib/wsMessages.d.ts +40 -0
- package/dist/lib/wsMessages.js +330 -0
- package/dist/lib/wsPresence.d.ts +25 -0
- package/dist/lib/wsPresence.js +99 -0
- package/dist/middleware/auditLog.d.ts +22 -0
- package/dist/middleware/auditLog.js +39 -0
- package/dist/middleware/bearerAuth.js +1 -1
- package/dist/middleware/cacheResponse.js +5 -1
- package/dist/middleware/captcha.d.ts +10 -0
- package/dist/middleware/captcha.js +36 -0
- package/dist/middleware/csrf.js +18 -4
- package/dist/middleware/errorHandler.js +4 -1
- package/dist/middleware/identify.js +89 -14
- package/dist/middleware/metrics.d.ts +9 -0
- package/dist/middleware/metrics.js +26 -0
- package/dist/middleware/requestId.d.ts +3 -0
- package/dist/middleware/requestId.js +7 -0
- package/dist/middleware/requestLogger.d.ts +38 -0
- package/dist/middleware/requestLogger.js +68 -0
- package/dist/middleware/requestSigning.d.ts +20 -0
- package/dist/middleware/requestSigning.js +100 -0
- package/dist/middleware/requireMfaSetup.d.ts +16 -0
- package/dist/middleware/requireMfaSetup.js +37 -0
- package/dist/middleware/requireRole.d.ts +9 -3
- package/dist/middleware/requireRole.js +23 -36
- package/dist/middleware/requireScope.d.ts +10 -0
- package/dist/middleware/requireScope.js +25 -0
- package/dist/middleware/requireStepUp.d.ts +18 -0
- package/dist/middleware/requireStepUp.js +29 -0
- package/dist/middleware/scimAuth.d.ts +8 -0
- package/dist/middleware/scimAuth.js +29 -0
- package/dist/middleware/upload.d.ts +5 -0
- package/dist/middleware/upload.js +27 -0
- package/dist/middleware/webhookAuth.d.ts +30 -0
- package/dist/middleware/webhookAuth.js +58 -0
- package/dist/models/AuditLog.d.ts +30 -0
- package/dist/models/AuditLog.js +39 -0
- package/dist/models/AuthUser.d.ts +7 -0
- package/dist/models/AuthUser.js +7 -0
- package/dist/models/Group.d.ts +21 -0
- package/dist/models/Group.js +28 -0
- package/dist/models/GroupMembership.d.ts +21 -0
- package/dist/models/GroupMembership.js +25 -0
- package/dist/models/M2MClient.d.ts +18 -0
- package/dist/models/M2MClient.js +18 -0
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +238 -21
- package/dist/routes/groups.d.ts +21 -0
- package/dist/routes/groups.js +346 -0
- package/dist/routes/jobs.js +66 -46
- package/dist/routes/m2m.d.ts +2 -0
- package/dist/routes/m2m.js +72 -0
- package/dist/routes/metrics.d.ts +8 -0
- package/dist/routes/metrics.js +55 -0
- package/dist/routes/mfa.js +13 -1
- package/dist/routes/oauth.js +6 -0
- package/dist/routes/oidc.d.ts +2 -0
- package/dist/routes/oidc.js +29 -0
- package/dist/routes/passkey.d.ts +1 -0
- package/dist/routes/passkey.js +157 -0
- package/dist/routes/saml.d.ts +2 -0
- package/dist/routes/saml.js +86 -0
- package/dist/routes/scim.d.ts +2 -0
- package/dist/routes/scim.js +255 -0
- package/dist/routes/uploads.d.ts +14 -0
- package/dist/routes/uploads.js +227 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +46 -3
- package/dist/services/auth.d.ts +2 -0
- package/dist/services/auth.js +101 -22
- package/dist/services/mfa.js +2 -2
- package/dist/ws/index.js +5 -1
- package/docs/sections/auth-flow/full.md +203 -47
- package/docs/sections/auth-flow/overview.md +2 -2
- package/docs/sections/auth-security-examples/full.md +388 -0
- package/docs/sections/authentication/full.md +130 -0
- package/docs/sections/authentication/overview.md +5 -0
- package/docs/sections/cli/full.md +13 -1
- package/docs/sections/configuration/full.md +17 -0
- package/docs/sections/configuration/overview.md +1 -0
- package/docs/sections/exports/full.md +34 -3
- package/docs/sections/logging/full.md +83 -0
- package/docs/sections/metrics/full.md +131 -0
- package/docs/sections/oauth/full.md +189 -189
- package/docs/sections/oauth/overview.md +1 -1
- package/docs/sections/pagination/full.md +93 -0
- package/docs/sections/passkey-login/full.md +90 -0
- package/docs/sections/passkey-login/overview.md +1 -0
- package/docs/sections/roles/full.md +224 -135
- package/docs/sections/roles/overview.md +3 -1
- package/docs/sections/signing/full.md +203 -0
- package/docs/sections/uploads/full.md +208 -0
- package/docs/sections/versioning/full.md +85 -0
- package/docs/sections/webhook-auth/full.md +100 -0
- package/docs/sections/websocket/full.md +95 -0
- package/docs/sections/websocket-rooms/full.md +6 -1
- package/package.json +18 -5
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
### Auth Flow
|
|
2
2
|
|
|
3
3
|
Sessions are backed by Redis by default. Each login creates an independent session keyed by a UUID (`session:{appName}:{sessionId}`), so multiple devices / tabs can be logged in simultaneously. Set `db.sessions: "mongo"` to store them in MongoDB instead — useful when running without Redis. See [Running without Redis](#running-without-redis).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
#### Browser clients
|
|
6
6
|
1. `POST /auth/login` → JWT set as HttpOnly cookie automatically
|
|
7
7
|
2. All subsequent requests send the cookie — no extra code needed
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
#### API / non-browser clients
|
|
10
10
|
1. `POST /auth/login` → read `token` from response body
|
|
11
11
|
2. Send `x-user-token: <token>` header on every request
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
#### Current user
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
GET /auth/me → { userId, email?, emailVerified?, googleLinked? }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires an active session (cookie or `x-user-token` header). Returns the authenticated user's profile — `email`, `emailVerified`, and `googleLinked` are populated when the auth adapter implements `getUser`.
|
|
20
|
+
|
|
21
|
+
#### Session management
|
|
14
22
|
|
|
15
23
|
Each login creates an independent session so multiple devices stay logged in simultaneously. The framework enforces a configurable cap (default: 6) — the oldest session is evicted when the limit is exceeded.
|
|
16
24
|
|
|
@@ -24,11 +32,11 @@ Session metadata (IP address, user-agent, timestamps) is persisted even after a
|
|
|
24
32
|
|
|
25
33
|
Set `sessionPolicy.includeInactiveSessions: true` to surface expired/deleted sessions in `GET /auth/sessions` with `isActive: false` — useful for a full device-history UI similar to Google or Meta's account security page.
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
##### Sliding sessions
|
|
28
36
|
|
|
29
37
|
Set `sessionPolicy.trackLastActive: true` to update `lastActiveAt` on every authenticated request. This adds one DB write per request but enables a sliding-session experience — sessions that are actively used stay fresh. Pair with refresh tokens (below) for true sliding behavior: short-lived access tokens (15 min) keep authorization tight, while a long-lived refresh token (30 days) lets the client silently renew without re-entering credentials.
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
#### Refresh Tokens
|
|
32
40
|
|
|
33
41
|
When configured, login and register return short-lived access tokens (default 15 min) alongside long-lived refresh tokens (default 30 days). The client uses `POST /auth/refresh` to obtain a new access token when the current one expires.
|
|
34
42
|
|
|
@@ -46,7 +54,7 @@ await createServer({
|
|
|
46
54
|
|
|
47
55
|
**When not configured**, the existing 7-day JWT behavior is unchanged — fully backward compatible.
|
|
48
56
|
|
|
49
|
-
|
|
57
|
+
##### Endpoints
|
|
50
58
|
|
|
51
59
|
| Endpoint | Purpose |
|
|
52
60
|
|---|---|
|
|
@@ -54,15 +62,15 @@ await createServer({
|
|
|
54
62
|
| `POST /auth/register` | Returns `token` + `refreshToken` |
|
|
55
63
|
| `POST /auth/refresh` | Rotates refresh token, returns new `token` + `refreshToken` |
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
##### Rotation with grace window
|
|
58
66
|
|
|
59
67
|
On each refresh, the server generates a new refresh token but keeps the old one valid for `rotationGraceSeconds` (default 30s). If the client's network drops mid-refresh, it can safely retry with the old token. If the old token is reused *after* the grace window, the entire session is invalidated — this is token-family theft detection.
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
##### Cookie behavior
|
|
62
70
|
|
|
63
|
-
The refresh token is set as an `HttpOnly` cookie (`refresh_token`) alongside the existing session cookie. For non-browser clients, it
|
|
71
|
+
The refresh token is set as an `HttpOnly` cookie (`refresh_token`) alongside the existing session cookie. For non-browser clients, it is accepted via the `x-refresh-token` header or in the request body. **The refresh token is not returned in the JSON response body** — it is only delivered via the `HttpOnly` cookie to prevent accidental exposure in logs or client-side code.
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
#### MFA / TOTP
|
|
66
74
|
|
|
67
75
|
Enable multi-factor authentication with TOTP (Google Authenticator, Authy, etc.):
|
|
68
76
|
|
|
@@ -87,7 +95,7 @@ Requires `otpauth` peer dependency:
|
|
|
87
95
|
bun add otpauth
|
|
88
96
|
```
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
##### Endpoints
|
|
91
99
|
|
|
92
100
|
| Endpoint | Auth | Purpose |
|
|
93
101
|
|---|---|---|
|
|
@@ -98,7 +106,7 @@ bun add otpauth
|
|
|
98
106
|
| `POST /auth/mfa/recovery-codes` | userAuth | Regenerate codes (requires TOTP code) |
|
|
99
107
|
| `GET /auth/mfa/methods` | userAuth | Get enabled MFA methods |
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
##### Login flow with MFA enabled
|
|
102
110
|
|
|
103
111
|
1. `POST /auth/login` with credentials → password OK + MFA enabled → `{ mfaRequired: true, mfaToken: "...", mfaMethods: ["totp"] }` (no session created)
|
|
104
112
|
2. `POST /auth/mfa/verify` with `{ mfaToken, code }` → verifies TOTP or recovery code → creates session → returns normal token response
|
|
@@ -109,7 +117,45 @@ The verify endpoint accepts an optional `method` field (`"totp"` or `"emailOtp"`
|
|
|
109
117
|
|
|
110
118
|
**Recovery codes**: 10 random 8-character alphanumeric codes, stored as SHA-256 hashes. Each code can only be used once. Enabling a second MFA method regenerates recovery codes — save the new set.
|
|
111
119
|
|
|
112
|
-
|
|
120
|
+
##### Enforcing MFA for all users
|
|
121
|
+
|
|
122
|
+
By default MFA is opt-in — individual users choose whether to enable it. Set `required: true` to enforce MFA at the app level:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
await createServer({
|
|
126
|
+
auth: {
|
|
127
|
+
mfa: {
|
|
128
|
+
issuer: "My App",
|
|
129
|
+
required: true, // all authenticated users must complete MFA setup
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When `required` is `true`:
|
|
136
|
+
|
|
137
|
+
- Authenticated users who have **not** completed MFA setup receive a `403` response on all non-auth endpoints:
|
|
138
|
+
```json
|
|
139
|
+
{ "error": "MFA setup required", "code": "MFA_SETUP_REQUIRED" }
|
|
140
|
+
```
|
|
141
|
+
- **Exempt paths** remain accessible so users can complete setup: all `/auth/*` routes (login, logout, register, MFA setup, OAuth, sessions), `/health`, `/docs`, `/openapi.json`, and the root `/`.
|
|
142
|
+
- **Unauthenticated requests** pass through normally — the middleware only gates users who are logged in but lack MFA.
|
|
143
|
+
- **OAuth users** must also set up MFA — OAuth login creates a session, but the user is still blocked from service endpoints until MFA is configured.
|
|
144
|
+
- **Disabling MFA** (via `DELETE /auth/mfa`) when `required: true` immediately blocks the user from service endpoints until they re-enable it.
|
|
145
|
+
|
|
146
|
+
**Client-side handling:** Check for the `MFA_SETUP_REQUIRED` code in 403 responses and redirect users to an MFA setup page that calls `POST /auth/mfa/setup`.
|
|
147
|
+
|
|
148
|
+
**Per-route usage:** The `requireMfaSetup` middleware is also exported for apps that want manual, per-route enforcement instead of global:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { userAuth, requireMfaSetup } from "@lastshotlabs/bunshot";
|
|
152
|
+
|
|
153
|
+
router.get("/dashboard", userAuth, requireMfaSetup, (c) => {
|
|
154
|
+
return c.json({ message: "Welcome" });
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Email OTP
|
|
113
159
|
|
|
114
160
|
An alternative to TOTP that sends a one-time code to the user's email. Users can enable TOTP, email OTP, or both.
|
|
115
161
|
|
|
@@ -129,7 +175,7 @@ await createServer({
|
|
|
129
175
|
});
|
|
130
176
|
```
|
|
131
177
|
|
|
132
|
-
|
|
178
|
+
##### Endpoints
|
|
133
179
|
|
|
134
180
|
| Endpoint | Auth | Purpose |
|
|
135
181
|
|---|---|---|
|
|
@@ -138,26 +184,26 @@ await createServer({
|
|
|
138
184
|
| `DELETE /auth/mfa/email-otp` | userAuth | Disable email OTP |
|
|
139
185
|
| `POST /auth/mfa/resend` | none (uses mfaToken) | Resend email OTP code (max 3 per challenge) |
|
|
140
186
|
|
|
141
|
-
|
|
187
|
+
##### Setup flow
|
|
142
188
|
|
|
143
189
|
1. `POST /auth/mfa/email-otp/enable` → sends code to email → returns `{ setupToken }`
|
|
144
190
|
2. `POST /auth/mfa/email-otp/verify-setup` with `{ setupToken, code }` → enables email OTP → returns recovery codes
|
|
145
191
|
|
|
146
192
|
This two-step flow ensures the `onSend` callback actually delivers emails before MFA is activated, preventing lockout from misconfigured email providers.
|
|
147
193
|
|
|
148
|
-
|
|
194
|
+
##### Login flow with email OTP
|
|
149
195
|
|
|
150
196
|
1. `POST /auth/login` → `{ mfaRequired: true, mfaToken, mfaMethods: ["emailOtp"] }` — code is auto-sent to user's email
|
|
151
197
|
2. `POST /auth/mfa/verify` with `{ mfaToken, code }` → creates session
|
|
152
198
|
3. If the code didn't arrive: `POST /auth/mfa/resend` with `{ mfaToken }` (max 3 resends, capped at 3x challenge TTL)
|
|
153
199
|
|
|
154
|
-
|
|
200
|
+
##### Disabling email OTP
|
|
155
201
|
|
|
156
202
|
- If TOTP is also enabled: requires a TOTP code in the `code` field
|
|
157
203
|
- If email OTP is the only method: requires the account password in the `password` field
|
|
158
204
|
- Disabling the last MFA method turns off MFA entirely
|
|
159
205
|
|
|
160
|
-
|
|
206
|
+
#### WebAuthn / Security Keys
|
|
161
207
|
|
|
162
208
|
Hardware security keys (YubiKey, etc.) and platform authenticators (Touch ID, Windows Hello) via the WebAuthn/FIDO2 standard. Users can register multiple keys and use them as an MFA method alongside TOTP and email OTP.
|
|
163
209
|
|
|
@@ -186,7 +232,7 @@ bun add @simplewebauthn/server
|
|
|
186
232
|
|
|
187
233
|
If `mfa.webauthn` is configured but the dependency is missing, the server fails fast at startup with a clear error message.
|
|
188
234
|
|
|
189
|
-
|
|
235
|
+
##### Endpoints
|
|
190
236
|
|
|
191
237
|
| Endpoint | Auth | Purpose |
|
|
192
238
|
|---|---|---|
|
|
@@ -196,13 +242,15 @@ If `mfa.webauthn` is configured but the dependency is missing, the server fails
|
|
|
196
242
|
| `DELETE /auth/mfa/webauthn/credentials/:credentialId` | userAuth | Remove a single key |
|
|
197
243
|
| `DELETE /auth/mfa/webauthn` | userAuth | Disable WebAuthn entirely |
|
|
198
244
|
|
|
199
|
-
|
|
245
|
+
##### Registration flow
|
|
200
246
|
|
|
201
247
|
1. `POST /auth/mfa/webauthn/register-options` → returns `{ options, registrationToken }`
|
|
202
248
|
2. Client passes `options` to `navigator.credentials.create()` — browser prompts user to tap/scan key
|
|
203
249
|
3. `POST /auth/mfa/webauthn/register` with `{ registrationToken, attestationResponse, name? }` → stores credential → returns recovery codes
|
|
204
250
|
|
|
205
|
-
|
|
251
|
+
> Credentials are registered with `residentKey: "required"` and `userVerification: "required"`, making them usable as discoverable passkeys. This enables passwordless login via `allowPasswordlessLogin` without requiring users to re-register.
|
|
252
|
+
|
|
253
|
+
##### Login flow with WebAuthn
|
|
206
254
|
|
|
207
255
|
1. `POST /auth/login` → `{ mfaRequired: true, mfaToken, mfaMethods: ["webauthn"], webauthnOptions: {...} }`
|
|
208
256
|
2. Client passes `webauthnOptions` to `navigator.credentials.get()` — browser prompts for key
|
|
@@ -210,17 +258,87 @@ If `mfa.webauthn` is configured but the dependency is missing, the server fails
|
|
|
210
258
|
|
|
211
259
|
The `webauthnOptions` object follows the WebAuthn spec — pass it directly to `navigator.credentials.get()`. The `webauthnResponse` is the full result from the browser API.
|
|
212
260
|
|
|
213
|
-
|
|
261
|
+
##### Credential removal
|
|
214
262
|
|
|
215
263
|
- Removing a spare key (other keys or MFA methods still active): no extra verification needed
|
|
216
264
|
- Removing the last credential of the last MFA method: requires TOTP code or password
|
|
217
265
|
- `DELETE /auth/mfa/webauthn` (disable all): always requires verification
|
|
218
266
|
|
|
219
|
-
|
|
267
|
+
##### Sign count validation
|
|
220
268
|
|
|
221
269
|
WebAuthn authenticators increment a sign count on each use to detect cloned keys. By default, a backward count logs a warning but allows authentication. Set `strictSignCount: true` to reject authentication when the count goes backward.
|
|
222
270
|
|
|
223
|
-
|
|
271
|
+
#### Email Verification
|
|
272
|
+
|
|
273
|
+
Opt-in via `auth.emailVerification`. Requires `primaryField: "email"` (default).
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
await createServer({
|
|
277
|
+
auth: {
|
|
278
|
+
emailVerification: {
|
|
279
|
+
onSend: async (email, token) => {
|
|
280
|
+
// Send via any provider (Resend, SendGrid, etc.)
|
|
281
|
+
await sendEmail(email, `Verify your email: https://myapp.com/verify?token=${token}`);
|
|
282
|
+
},
|
|
283
|
+
required: true, // Block login until verified (default: false)
|
|
284
|
+
tokenExpiry: 86400, // Seconds — default: 86400 (24 hours)
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
When configured, two routes are mounted:
|
|
291
|
+
|
|
292
|
+
| Endpoint | Auth | Purpose |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `POST /auth/verify-email` | none | Consume verification token |
|
|
295
|
+
| `POST /auth/resend-verification` | none (uses credentials) | Resend verification email |
|
|
296
|
+
|
|
297
|
+
A verification token is sent automatically on registration via `emailVerification.onSend`. The token is SHA-256 hashed before storage. Rate-limited by IP (verify) and identifier (resend).
|
|
298
|
+
|
|
299
|
+
When `required: true`, unverified users receive `403 { error: "Email not verified" }` on `/auth/login`.
|
|
300
|
+
|
|
301
|
+
#### Password Reset
|
|
302
|
+
|
|
303
|
+
Opt-in via `auth.passwordReset`. Requires `primaryField: "email"` (default) and a `setPassword` implementation on your auth adapter.
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
await createServer({
|
|
307
|
+
auth: {
|
|
308
|
+
passwordReset: {
|
|
309
|
+
onSend: async (email, token) => {
|
|
310
|
+
await sendEmail(email, `Reset your password: https://myapp.com/reset?token=${token}`);
|
|
311
|
+
},
|
|
312
|
+
tokenExpiry: 3600, // Seconds — default: 3600 (1 hour)
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
When configured, two routes are mounted:
|
|
319
|
+
|
|
320
|
+
| Endpoint | Auth | Purpose |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| `POST /auth/forgot-password` | none | Request reset email (always returns 200 — never reveals whether address is registered) |
|
|
323
|
+
| `POST /auth/reset-password` | none | Consume token, set new password, revoke all sessions |
|
|
324
|
+
|
|
325
|
+
The forgot-password endpoint uses a fire-and-forget pattern and is rate-limited by **both IP and email address** to prevent distributed email-bombing. Tokens are SHA-256 hashed before storage and single-use. After a successful reset, all active sessions are revoked.
|
|
326
|
+
|
|
327
|
+
#### Primary Field
|
|
328
|
+
|
|
329
|
+
By default, users register and log in with their email address. Change this via `auth.primaryField`:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
await createServer({
|
|
333
|
+
auth: {
|
|
334
|
+
primaryField: "username", // "email" (default) | "username" | "phone"
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
When set to `"username"` or `"phone"`, the registration/login body uses that field name instead of `email`. Email verification and password reset are only available when `primaryField` is `"email"`. Implement `adapter.findByIdentifier` on your custom adapter for non-email primary fields — it falls back to `findByEmail` when absent.
|
|
340
|
+
|
|
341
|
+
#### Account Deletion
|
|
224
342
|
|
|
225
343
|
Enable `DELETE /auth/me` for user-initiated account deletion:
|
|
226
344
|
|
|
@@ -245,18 +363,47 @@ await createServer({
|
|
|
245
363
|
});
|
|
246
364
|
```
|
|
247
365
|
|
|
248
|
-
|
|
366
|
+
##### Behavior
|
|
249
367
|
|
|
250
368
|
- Requires `userAuth` middleware (user must be logged in)
|
|
251
369
|
- Body: `{ password?: string }` — required for credential accounts, skipped for OAuth-only
|
|
252
370
|
- Revokes all sessions, deletes tokens, calls `adapter.deleteUser(userId)`
|
|
253
371
|
- Rate limited (3/hour by userId)
|
|
254
372
|
|
|
255
|
-
|
|
373
|
+
##### Queued deletion
|
|
256
374
|
|
|
257
|
-
When `queued: true`, deletion is enqueued as a BullMQ job instead of running synchronously. The endpoint returns `202 Accepted` immediately.
|
|
375
|
+
When `queued: true`, deletion is enqueued as a BullMQ job instead of running synchronously. The endpoint returns `202 Accepted` immediately and the user's sessions are revoked at that point. The actual user data deletion executes after the delay set by `gracePeriod`.
|
|
258
376
|
|
|
259
|
-
|
|
377
|
+
The framework starts a built-in BullMQ worker automatically (in-process, alongside the API server) to process deletion jobs. Requires BullMQ and Redis:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
bun add bullmq
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
With `gracePeriod > 0`, the `POST /auth/cancel-deletion` route is mounted. Provide `onDeletionScheduled` to send the cancel token to the user:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
auth: {
|
|
387
|
+
accountDeletion: {
|
|
388
|
+
queued: true,
|
|
389
|
+
gracePeriod: 7 * 24 * 60 * 60, // 7 days in seconds
|
|
390
|
+
onDeletionScheduled: async (userId, email, cancelToken) => {
|
|
391
|
+
await sendEmail(email, `Cancel deletion: https://myapp.com/cancel?token=${cancelToken}`);
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The cancel endpoint accepts the token and removes the pending BullMQ job:
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
POST /auth/cancel-deletion → { message: "Account deletion cancelled" }
|
|
401
|
+
Body: { token: string }
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The cancel token expires when the grace period elapses and can only be used once.
|
|
405
|
+
|
|
406
|
+
#### Password Policy
|
|
260
407
|
|
|
261
408
|
Configure password complexity requirements via `auth.passwordPolicy`. The policy applies to registration and password reset — login uses `min(1)` intentionally to avoid locking out users registered under older/weaker policies.
|
|
262
409
|
|
|
@@ -275,7 +422,7 @@ await createServer({
|
|
|
275
422
|
|
|
276
423
|
When not configured, the default policy requires 8+ characters with at least one letter and one digit.
|
|
277
424
|
|
|
278
|
-
|
|
425
|
+
#### Protecting routes
|
|
279
426
|
|
|
280
427
|
```ts
|
|
281
428
|
import { userAuth, requireRole, requireVerifiedEmail } from "@lastshotlabs/bunshot";
|
|
@@ -286,7 +433,7 @@ router.use("/content", userAuth, requireRole("admin", "editor")); // allow eithe
|
|
|
286
433
|
router.use("/dashboard", userAuth, requireVerifiedEmail); // returns 403 if email not verified
|
|
287
434
|
```
|
|
288
435
|
|
|
289
|
-
|
|
436
|
+
#### Custom auth adapter
|
|
290
437
|
|
|
291
438
|
By default, `/auth/*` routes store users in MongoDB via `mongoAuthAdapter`. Pass `auth: { adapter: myAdapter }` to `createServer` to use any other store — Postgres, SQLite, an external service, etc. Alternatively, use `db.auth` to select a built-in adapter (`"mongo"` | `"sqlite"` | `"memory"`).
|
|
292
439
|
|
|
@@ -367,7 +514,7 @@ The adapter is responsible for:
|
|
|
367
514
|
|
|
368
515
|
Everything else (password hashing, JWT signing, Redis sessions) is handled by the package.
|
|
369
516
|
|
|
370
|
-
|
|
517
|
+
#### Auth Rate Limiting
|
|
371
518
|
|
|
372
519
|
All built-in auth endpoints are rate-limited out of the box with sensible defaults. No configuration needed — just be aware of the behavior:
|
|
373
520
|
|
|
@@ -377,7 +524,7 @@ All built-in auth endpoints are rate-limited out of the box with sensible defaul
|
|
|
377
524
|
| `POST /auth/register` | IP address | Every attempt | 5 / hour |
|
|
378
525
|
| `POST /auth/verify-email` | IP address | Every attempt | 10 / 15 min |
|
|
379
526
|
| `POST /auth/resend-verification` | Identifier (email/username/phone) | Every attempt | 3 / hour |
|
|
380
|
-
| `POST /auth/forgot-password` | IP address | Every attempt | 5 / 15 min |
|
|
527
|
+
| `POST /auth/forgot-password` | IP address **and** email address | Every attempt | 5 / 15 min (shared window per key) |
|
|
381
528
|
| `POST /auth/reset-password` | IP address | Every attempt | 10 / 15 min |
|
|
382
529
|
| `POST /auth/refresh` | IP address | Every attempt | 30 / min |
|
|
383
530
|
| `POST /auth/mfa/verify` | IP address | Every attempt | 10 / 15 min |
|
|
@@ -385,7 +532,7 @@ All built-in auth endpoints are rate-limited out of the box with sensible defaul
|
|
|
385
532
|
|
|
386
533
|
Login is keyed by the **identifier being targeted** — an attacker rotating IPs to brute-force `alice@example.com` is blocked regardless of source IP. A successful login resets the counter so legitimate users aren't locked out.
|
|
387
534
|
|
|
388
|
-
|
|
535
|
+
##### Tuning limits
|
|
389
536
|
|
|
390
537
|
```ts
|
|
391
538
|
await createServer({
|
|
@@ -401,7 +548,7 @@ await createServer({
|
|
|
401
548
|
});
|
|
402
549
|
```
|
|
403
550
|
|
|
404
|
-
|
|
551
|
+
##### Manually clearing a limit (admin unlock)
|
|
405
552
|
|
|
406
553
|
If a legitimate user gets locked out, call `bustAuthLimit` with the same key format the limiter uses:
|
|
407
554
|
|
|
@@ -416,9 +563,9 @@ router.post("/admin/unblock-login", userAuth, requireRole("admin"), async (c) =>
|
|
|
416
563
|
});
|
|
417
564
|
```
|
|
418
565
|
|
|
419
|
-
Key formats: `login:{identifier}`, `register:{ip}`, `verify:{ip}`, `resend:{userId}`.
|
|
566
|
+
Key formats: `login:{identifier}`, `register:{ip}`, `verify:{ip}`, `resend:{identifier}`, `forgot:ip:{ip}`, `forgot:email:{email}`, `reset:{ip}`, `refresh:ip:{ip}`, `deleteaccount:{userId}`.
|
|
420
567
|
|
|
421
|
-
|
|
568
|
+
##### Using the rate limiter in your own routes
|
|
422
569
|
|
|
423
570
|
`trackAttempt` and `isLimited` are exported so you can apply the same Redis-backed rate limiting to any route in your app. They use the same store configured via `auth.rateLimit.store`.
|
|
424
571
|
|
|
@@ -452,17 +599,17 @@ if (await isLimited(key, opts)) {
|
|
|
452
599
|
|
|
453
600
|
Keys are automatically namespaced to the app (e.g. `rl:MyApp:submit:1.2.3.4`) when the Redis store is active, so they won't collide on a shared Redis instance.
|
|
454
601
|
|
|
455
|
-
|
|
602
|
+
##### Store
|
|
456
603
|
|
|
457
604
|
The rate limit store defaults to `"redis"` when Redis is enabled (recommended for multi-instance deployments — limits are shared across all servers). Falls back to `"memory"` automatically when Redis is disabled. In-memory limits don't persist across restarts.
|
|
458
605
|
|
|
459
606
|
---
|
|
460
607
|
|
|
461
|
-
|
|
608
|
+
#### Bot Protection
|
|
462
609
|
|
|
463
610
|
The built-in IP rate limiter is ineffective against bots that rotate IPs. The `botProtection` config adds two IP-rotation-resistant layers that run before the IP rate limit check.
|
|
464
611
|
|
|
465
|
-
|
|
612
|
+
##### Fingerprint rate limiting
|
|
466
613
|
|
|
467
614
|
When `fingerprintRateLimit: true`, every request is also rate-limited by an HTTP fingerprint — a 12-char hash derived from `User-Agent`, `Accept-*`, `Connection`, and the presence/absence of browser-only headers (`sec-fetch-*`, `sec-ch-ua-*`, `origin`, `referer`, etc.).
|
|
468
615
|
|
|
@@ -481,7 +628,7 @@ await createServer({
|
|
|
481
628
|
|
|
482
629
|
The fingerprint bucket uses the same window and max as `security.rateLimit`, and is stored in the same backend as `auth.rateLimit.store` (Redis by default, shared across all instances).
|
|
483
630
|
|
|
484
|
-
|
|
631
|
+
##### IP / CIDR blocklist
|
|
485
632
|
|
|
486
633
|
Block known datacenter ranges, proxy providers, or individual IPs outright. Matched requests receive a 403 before any other processing — no session lookup, no rate-limit increment.
|
|
487
634
|
|
|
@@ -501,7 +648,7 @@ await createServer({
|
|
|
501
648
|
|
|
502
649
|
Both options can be combined. The middleware order is: blocklist → IP rate limit → fingerprint rate limit.
|
|
503
650
|
|
|
504
|
-
|
|
651
|
+
##### Apply `botProtection` to individual routes
|
|
505
652
|
|
|
506
653
|
`botProtection` is also exported for per-route use:
|
|
507
654
|
|
|
@@ -513,7 +660,7 @@ router.use("/api/submit", botProtection({ blockList: ["198.51.100.0/24"] }));
|
|
|
513
660
|
|
|
514
661
|
---
|
|
515
662
|
|
|
516
|
-
|
|
663
|
+
#### Trusted Proxy
|
|
517
664
|
|
|
518
665
|
By default, Bunshot uses the socket-level IP address for all rate limiting and session metadata — the `X-Forwarded-For` header is **ignored entirely**. This prevents attackers from spoofing IPs to bypass rate limits.
|
|
519
666
|
|
|
@@ -542,7 +689,7 @@ router.post("/api/action", async (c) => {
|
|
|
542
689
|
});
|
|
543
690
|
```
|
|
544
691
|
|
|
545
|
-
|
|
692
|
+
#### JWT Secret Validation
|
|
546
693
|
|
|
547
694
|
JWT secrets are validated on first use. The framework throws a clear error if:
|
|
548
695
|
- The environment variable (`JWT_SECRET_DEV` or `JWT_SECRET_PROD`) is missing
|
|
@@ -556,21 +703,30 @@ node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
|
|
|
556
703
|
|
|
557
704
|
---
|
|
558
705
|
|
|
559
|
-
|
|
706
|
+
#### Setting a password after social login
|
|
560
707
|
|
|
561
708
|
If a user signed up via Google or Apple and later wants to add a password, send an authenticated request to `POST /auth/set-password`:
|
|
562
709
|
|
|
563
710
|
```ts
|
|
564
|
-
// Client (logged-in user)
|
|
711
|
+
// Client (logged-in user) — first time setting a password (no currentPassword needed)
|
|
565
712
|
await fetch("/auth/set-password", {
|
|
566
713
|
method: "POST",
|
|
567
714
|
headers: { "Content-Type": "application/json", "x-user-token": token },
|
|
568
715
|
body: JSON.stringify({ password: "mynewpassword" }),
|
|
569
716
|
});
|
|
717
|
+
|
|
718
|
+
// Changing an existing password — currentPassword is required
|
|
719
|
+
await fetch("/auth/set-password", {
|
|
720
|
+
method: "POST",
|
|
721
|
+
headers: { "Content-Type": "application/json", "x-user-token": token },
|
|
722
|
+
body: JSON.stringify({ password: "mynewpassword", currentPassword: "oldpassword" }),
|
|
723
|
+
});
|
|
570
724
|
```
|
|
571
725
|
|
|
572
726
|
The built-in route hashes the password and calls `adapter.setPassword(userId, hash)`. If your adapter does not implement `setPassword`, the route returns `501 Not Implemented`.
|
|
573
727
|
|
|
728
|
+
**If the account already has a password set**, `currentPassword` must be provided. Missing it returns `400 { error: "Current password is required to change an existing password." }`. An incorrect `currentPassword` returns `401 { error: "Current password is incorrect." }`.
|
|
729
|
+
|
|
574
730
|
To support it with a custom adapter:
|
|
575
731
|
|
|
576
732
|
```ts
|
|
@@ -583,7 +739,7 @@ const myAdapter: AuthAdapter = {
|
|
|
583
739
|
};
|
|
584
740
|
```
|
|
585
741
|
|
|
586
|
-
|
|
742
|
+
#### CSRF Protection
|
|
587
743
|
|
|
588
744
|
Opt-in via `security.csrf` — protects cookie-authenticated browser clients against cross-site request forgery attacks. Mobile apps and SPAs using header-based auth (`x-user-token`) are not affected and do not need CSRF.
|
|
589
745
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
### Auth Flow
|
|
2
2
|
|
|
3
3
|
Sessions are backed by Redis (default), MongoDB, SQLite, or memory. Each login creates an independent session keyed by UUID, so multiple devices stay logged in simultaneously.
|
|
4
4
|
|
|
5
5
|
- **Browser clients**: `POST /auth/login` sets an HttpOnly cookie automatically
|
|
6
6
|
- **API clients**: Read `token` from the response body, send `x-user-token: <token>` header
|
|
7
7
|
|
|
8
|
-
Features include session management (list/revoke), refresh tokens (short-lived access + long-lived refresh with rotation), MFA (TOTP via Google Authenticator, email OTP, recovery codes), account deletion (immediate or queued with grace period), custom auth adapters, rate limiting on all auth endpoints, bot protection (fingerprint rate limiting + CIDR blocklist),
|
|
8
|
+
Features include session management (list/revoke), current user endpoint (`GET /auth/me`), email verification (opt-in, with `required` gate), password reset (fire-and-forget, dual-keyed rate limiting), refresh tokens (short-lived access + long-lived refresh with rotation), MFA (TOTP via Google Authenticator, email OTP, WebAuthn, recovery codes), account deletion (immediate or queued BullMQ with grace period and cancel endpoint), custom auth adapters, rate limiting on all auth endpoints, bot protection (fingerprint rate limiting + CIDR blocklist), configurable primary field (email/username/phone), and CSRF protection.
|
|
9
9
|
|
|
10
10
|
Protect routes with `userAuth`, `requireRole("admin")`, and `requireVerifiedEmail` middleware.
|