@warlock.js/auth 5.24.0 → 5.26.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/CHANGELOG.md +26 -9
- package/LICENSE +21 -21
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/request-locals.d.mts +1 -1
- package/esm/contracts/types.d.mts +8 -3
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +3 -3
- package/esm/index.mjs +2 -2
- package/esm/middleware/auth.middleware.mjs +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs +5 -0
- package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs +3 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +9 -4
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +45 -17
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/index.d.mts +1 -1
- package/esm/services/index.mjs +1 -1
- package/esm/services/resolve-request-user.d.mts +3 -2
- package/esm/services/resolve-request-user.d.mts.map +1 -1
- package/esm/services/resolve-request-user.mjs +8 -2
- package/esm/services/resolve-request-user.mjs.map +1 -1
- package/esm/session/page-session.d.mts +3 -1
- package/esm/session/page-session.d.mts.map +1 -1
- package/esm/session/page-session.mjs +2 -1
- package/esm/session/page-session.mjs.map +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/llms-full.txt +1946 -1927
- package/package.json +8 -8
- package/skills/auth-basics/SKILL.md +1 -1
- package/skills/customize-token-storage/SKILL.md +160 -160
- package/skills/customize-user-type/SKILL.md +168 -168
- package/skills/handle-login-and-logout/SKILL.md +195 -180
- package/skills/login-with-providers/SKILL.md +283 -283
- package/skills/manage-tokens/SKILL.md +196 -196
- package/skills/overview/SKILL.md +74 -74
- package/skills/protect-routes/SKILL.md +299 -299
- package/skills/register-user/SKILL.md +147 -147
- package/skills/run-auth-commands/SKILL.md +159 -159
- package/skills/throttle-login-attempts/SKILL.md +117 -117
- package/skills/verify-email-and-reset-password/SKILL.md +137 -137
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: manage-tokens
|
|
3
|
-
description: 'Token lifecycle — generateAccessToken, createRefreshToken, createTokenPair, refreshTokens (with rotation + replay detection), revokeAllTokens, revokeTokenFamily, cleanupExpiredTokens, getActiveSessions. Triggers: `createTokenPair`, `refreshTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`, `jwt.generate`, `jwt.verify`, `AccessToken`, `RefreshToken`; "rotate refresh tokens", "detect token replay", "logout from all devices", "list active sessions", "clean up expired tokens"; typical import `import { authService, jwt } from "@warlock.js/auth"`. Skip: login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; CLI cleanup — `@warlock.js/auth/run-auth-commands/SKILL.md`; competing libs `jsonwebtoken`, `jose`, `fast-jwt`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Manage tokens
|
|
7
|
-
|
|
8
|
-
Tokens are persisted Cascade models. Issuing a token writes a row. Verification checks the row exists. Revocation deletes / marks-revoked. This gives you JWT's stateless verification + statelful revocation.
|
|
9
|
-
|
|
10
|
-
## Token shapes
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
type AccessTokenOutput = { token: string; expiresAt: string };
|
|
14
|
-
type RefreshTokenOutput = { token: string; expiresAt: string };
|
|
15
|
-
|
|
16
|
-
type TokenPair = {
|
|
17
|
-
accessToken: AccessTokenOutput;
|
|
18
|
-
refreshToken?: RefreshTokenOutput; // omitted if config.auth.refreshToken.enabled = false
|
|
19
|
-
};
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Issuing tokens
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
import { authService } from "@warlock.js/auth";
|
|
26
|
-
|
|
27
|
-
// Just an access token (rare — usually use createTokenPair)
|
|
28
|
-
const access = await authService.generateAccessToken(user);
|
|
29
|
-
|
|
30
|
-
// Just a refresh token
|
|
31
|
-
const refresh = await authService.createRefreshToken(user, deviceInfo);
|
|
32
|
-
|
|
33
|
-
// Both — the everyday case
|
|
34
|
-
const pair = await authService.createTokenPair(user, deviceInfo);
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
`createTokenPair` is the typical issuance path. It respects `config.auth.refreshToken.enabled` — if disabled, returns only `accessToken`.
|
|
38
|
-
|
|
39
|
-
## Refresh with rotation — `refreshTokens`
|
|
40
|
-
|
|
41
|
-
Refresh and family revocation use durable family revision checks inside a
|
|
42
|
-
serializable Cascade transaction. Run the additive `authMigrations` before
|
|
43
|
-
using the upgraded APIs. Transaction support is required: the service fails
|
|
44
|
-
rather than falling back to uncoordinated writes. MongoDB deployments therefore
|
|
45
|
-
need a transaction-capable topology.
|
|
46
|
-
|
|
47
|
-
An operation that owns its transaction retries serialization conflicts at most
|
|
48
|
-
three times. Inside an application-owned transaction it joins once and leaves
|
|
49
|
-
retry/rollback to the caller. Auth notifications from owned attempts are emitted
|
|
50
|
-
only after successful commit; notifications inside an application-owned outer
|
|
51
|
-
transaction retain their existing timing before that outer commit. Keep external
|
|
52
|
-
side effects outside such a transaction.
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
const next = await authService.refreshTokens(oldRefreshToken, deviceInfo);
|
|
56
|
-
// next: TokenPair | null
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
What happens internally:
|
|
60
|
-
|
|
61
|
-
1. Verify the JWT signature on the old refresh token.
|
|
62
|
-
2. Find the row in `RefreshToken` — must exist + not be revoked.
|
|
63
|
-
3. Look up the user via `config.auth.userType[token.userType]`.
|
|
64
|
-
4. **Rotation** (default — `config.auth.refreshToken.rotation = true`): revoke the old refresh token, create a new pair from the same `family_id`.
|
|
65
|
-
5. **No rotation**: mark the old as "used" but keep it valid.
|
|
66
|
-
|
|
67
|
-
**Replay detection.** If the old refresh token is presented again after rotation (already revoked but still in the DB):
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
// Inside refreshTokens, on a revoked-token presentation:
|
|
71
|
-
await authService.revokeTokenFamily(refreshToken.get("family_id"));
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Every refresh token in the same family is revoked. Pattern: a leaked refresh token is used by both legitimate user and attacker — the second use triggers the revoke, both sides get kicked.
|
|
75
|
-
|
|
76
|
-
## Family — the rotation chain
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
login → creates family X — refresh token A in family X
|
|
80
|
-
refresh (A) → revokes A; creates B in family X
|
|
81
|
-
refresh (B) → revokes B; creates C in family X
|
|
82
|
-
refresh (A again)→ A is revoked → revoke family X entirely
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The family ties together "successive rotations of one session." In 5.19 it is also a durable `AuthTokenFamily` row: family revoke marks that family, revokes its refresh rows, and deletes its associated access rows atomically. Access rows from before family association are conservatively deleted only for the same user and user type; they are never guessed into a family.
|
|
86
|
-
|
|
87
|
-
## Listing active sessions
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
const sessions = await authService.getActiveSessions(user);
|
|
91
|
-
|
|
92
|
-
for (const session of sessions) {
|
|
93
|
-
session.get("device_info"); // { userAgent, ip, deviceId? } if provided at login
|
|
94
|
-
session.get("created_at");
|
|
95
|
-
session.get("expires_at");
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Use this for "active sessions" UIs. Revoke a specific session by calling `.revoke()` on the `RefreshToken` instance.
|
|
100
|
-
|
|
101
|
-
## Removing tokens
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
// Specific access token
|
|
105
|
-
await authService.removeAccessToken(user, accessTokenString);
|
|
106
|
-
|
|
107
|
-
// Specific session/family
|
|
108
|
-
await authService.revokeTokenFamily(familyId);
|
|
109
|
-
|
|
110
|
-
// All access tokens for a user
|
|
111
|
-
await authService.removeAllAccessTokens(user);
|
|
112
|
-
|
|
113
|
-
// Everything — access + refresh + family
|
|
114
|
-
await authService.revokeAllTokens(user);
|
|
115
|
-
|
|
116
|
-
// A specific family
|
|
117
|
-
await authService.revokeTokenFamily(familyId);
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Max refresh tokens per user
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
// In config.auth.refreshToken:
|
|
124
|
-
{
|
|
125
|
-
maxPerUser: 5, // default
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
When issuing a new refresh token, the service counts active tokens for the user and revokes the oldest until count < `maxPerUser`. Pattern: limits how many simultaneous sessions a user can hold; prevents an attacker who got a token from gradually accumulating many.
|
|
130
|
-
|
|
131
|
-
## Expired-token cleanup
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
const cleaned = await authService.cleanupExpiredTokens();
|
|
135
|
-
// Returns: number of expired refresh tokens removed (also purges expired access-token rows
|
|
136
|
-
// and hard-deletes expired or consumed one_time_tokens rows).
|
|
137
|
-
// Fires "token.expired" event per token + "cleanup.completed" with the count.
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Every purge is a hard delete on every driver, including MongoDB. `AccessToken`, `RefreshToken`,
|
|
141
|
-
`OneTimeToken` and `PasskeyCredential` all declare `static deleteStrategy = "permanent"`, so a
|
|
142
|
-
purge never copies credential material into a `*Trash` collection despite MongoDB's driver
|
|
143
|
-
default being `"trash"`.
|
|
144
|
-
|
|
145
|
-
Run this periodically via the scheduler:
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
import { scheduler, job } from "@warlock.js/scheduler";
|
|
149
|
-
import { authService } from "@warlock.js/auth";
|
|
150
|
-
|
|
151
|
-
scheduler.addJob(
|
|
152
|
-
job("auth-cleanup", () => authService.cleanupExpiredTokens())
|
|
153
|
-
.daily()
|
|
154
|
-
.at("03:00"),
|
|
155
|
-
);
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Or use the bundled CLI command — see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md).
|
|
159
|
-
|
|
160
|
-
## JWT helpers
|
|
161
|
-
|
|
162
|
-
For low-level JWT signing/verification (outside the authService flow):
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
import { jwt } from "@warlock.js/auth";
|
|
166
|
-
|
|
167
|
-
const token = await jwt.generate(payload, { expiresIn: 3_600_000 }); // milliseconds
|
|
168
|
-
const decoded = await jwt.verify(token);
|
|
169
|
-
|
|
170
|
-
const refreshToken = await jwt.generateRefreshToken(payload, { expiresIn });
|
|
171
|
-
const decodedRefresh = await jwt.verifyRefreshToken(refreshToken);
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
⚠️ **`jwt.generate` / `jwt.generateRefreshToken` are the low-level escape hatch — `expiresIn` goes straight to `fast-jwt`.** It takes a number of milliseconds or an `ms`-style string, and rejects an unparseable string. What it does **not** reject is `undefined` or `0`: both skip the check and emit a token with **no `exp` claim**. Pass a positive value, and never pass the result of parsing a config string without checking it first.
|
|
175
|
-
|
|
176
|
-
**`jwt.verify` / `jwt.verifyRefreshToken` require an `exp` claim** (since 4.12.0) and reject a token without one. There is no deadline to check on such a token, so verification would otherwise succeed indefinitely. `requiredClaims` is additive — `jwt.verify(token, { requiredClaims: ["iat"] })` requires `iat` **and** `exp`; you cannot opt out of `exp`. If you want a token that effectively never expires, use `expiresIn: NO_EXPIRATION` (`"100y"`), which stamps a real `exp` about a century out.
|
|
177
|
-
|
|
178
|
-
If tokens minted before 4.12.0 may lack an `exp`, see `warlock auth.purge-never-expiring` in [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — they are still in your token tables and no date-based cleanup can reach them.
|
|
179
|
-
|
|
180
|
-
`authService.generateAccessToken` / `createRefreshToken` read `config.auth.accessToken.expiresIn` / `config.auth.refreshToken.expiresIn`, validate them, and throw naming the key if the value is not a positive `ms` duration — before anything is signed, persisted, or capped.
|
|
181
|
-
|
|
182
|
-
The package signs access and refresh tokens with independent secrets — `config.auth.accessToken.secret` and `config.auth.refreshToken.secret`. Setting a distinct `refresh.secret` is recommended: it prevents an access-token compromise from forging refresh tokens (and vice versa). The refresh secret is **optional** — when `config.auth.refreshToken.secret` is unset, refresh tokens fall back to the main `config.auth.accessToken.secret`, so refresh works out of the box without a second secret.
|
|
183
|
-
|
|
184
|
-
## Things NOT to do
|
|
185
|
-
|
|
186
|
-
- Don't use raw JWT libraries directly. The package handles signing, verification, secret loading, and the access/refresh split.
|
|
187
|
-
- Don't disable rotation (`config.auth.refreshToken.rotation = false`) unless you genuinely understand the tradeoff — you lose replay detection.
|
|
188
|
-
- Don't increase `maxPerUser` to a huge number "to be safe." Each active refresh token is a revocation surface; fewer simultaneous tokens means less attack surface.
|
|
189
|
-
- Don't manually delete `AccessToken` rows in a service. The user might be hitting a request mid-revoke and get an inconsistent state. Use the `authService` helpers.
|
|
190
|
-
- Run the additive `authMigrations` on upgrade. Do not alter or backfill token rows yourself; legacy access tokens are handled conservatively on family revoke.
|
|
191
|
-
|
|
192
|
-
## See also
|
|
193
|
-
|
|
194
|
-
- [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — full login/logout flow that uses these primitives
|
|
195
|
-
- [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — the bundled cleanup command
|
|
196
|
-
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — scheduling cleanup
|
|
1
|
+
---
|
|
2
|
+
name: manage-tokens
|
|
3
|
+
description: 'Token lifecycle — generateAccessToken, createRefreshToken, createTokenPair, refreshTokens (with rotation + replay detection), revokeAllTokens, revokeTokenFamily, cleanupExpiredTokens, getActiveSessions. Triggers: `createTokenPair`, `refreshTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`, `jwt.generate`, `jwt.verify`, `AccessToken`, `RefreshToken`; "rotate refresh tokens", "detect token replay", "logout from all devices", "list active sessions", "clean up expired tokens"; typical import `import { authService, jwt } from "@warlock.js/auth"`. Skip: login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; CLI cleanup — `@warlock.js/auth/run-auth-commands/SKILL.md`; competing libs `jsonwebtoken`, `jose`, `fast-jwt`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Manage tokens
|
|
7
|
+
|
|
8
|
+
Tokens are persisted Cascade models. Issuing a token writes a row. Verification checks the row exists. Revocation deletes / marks-revoked. This gives you JWT's stateless verification + statelful revocation.
|
|
9
|
+
|
|
10
|
+
## Token shapes
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
type AccessTokenOutput = { token: string; expiresAt: string };
|
|
14
|
+
type RefreshTokenOutput = { token: string; expiresAt: string };
|
|
15
|
+
|
|
16
|
+
type TokenPair = {
|
|
17
|
+
accessToken: AccessTokenOutput;
|
|
18
|
+
refreshToken?: RefreshTokenOutput; // omitted if config.auth.refreshToken.enabled = false
|
|
19
|
+
};
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Issuing tokens
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { authService } from "@warlock.js/auth";
|
|
26
|
+
|
|
27
|
+
// Just an access token (rare — usually use createTokenPair)
|
|
28
|
+
const access = await authService.generateAccessToken(user);
|
|
29
|
+
|
|
30
|
+
// Just a refresh token
|
|
31
|
+
const refresh = await authService.createRefreshToken(user, deviceInfo);
|
|
32
|
+
|
|
33
|
+
// Both — the everyday case
|
|
34
|
+
const pair = await authService.createTokenPair(user, deviceInfo);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`createTokenPair` is the typical issuance path. It respects `config.auth.refreshToken.enabled` — if disabled, returns only `accessToken`.
|
|
38
|
+
|
|
39
|
+
## Refresh with rotation — `refreshTokens`
|
|
40
|
+
|
|
41
|
+
Refresh and family revocation use durable family revision checks inside a
|
|
42
|
+
serializable Cascade transaction. Run the additive `authMigrations` before
|
|
43
|
+
using the upgraded APIs. Transaction support is required: the service fails
|
|
44
|
+
rather than falling back to uncoordinated writes. MongoDB deployments therefore
|
|
45
|
+
need a transaction-capable topology.
|
|
46
|
+
|
|
47
|
+
An operation that owns its transaction retries serialization conflicts at most
|
|
48
|
+
three times. Inside an application-owned transaction it joins once and leaves
|
|
49
|
+
retry/rollback to the caller. Auth notifications from owned attempts are emitted
|
|
50
|
+
only after successful commit; notifications inside an application-owned outer
|
|
51
|
+
transaction retain their existing timing before that outer commit. Keep external
|
|
52
|
+
side effects outside such a transaction.
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const next = await authService.refreshTokens(oldRefreshToken, deviceInfo);
|
|
56
|
+
// next: TokenPair | null
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
What happens internally:
|
|
60
|
+
|
|
61
|
+
1. Verify the JWT signature on the old refresh token.
|
|
62
|
+
2. Find the row in `RefreshToken` — must exist + not be revoked.
|
|
63
|
+
3. Look up the user via `config.auth.userType[token.userType]`.
|
|
64
|
+
4. **Rotation** (default — `config.auth.refreshToken.rotation = true`): revoke the old refresh token, create a new pair from the same `family_id`.
|
|
65
|
+
5. **No rotation**: mark the old as "used" but keep it valid.
|
|
66
|
+
|
|
67
|
+
**Replay detection.** If the old refresh token is presented again after rotation (already revoked but still in the DB):
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Inside refreshTokens, on a revoked-token presentation:
|
|
71
|
+
await authService.revokeTokenFamily(refreshToken.get("family_id"));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Every refresh token in the same family is revoked. Pattern: a leaked refresh token is used by both legitimate user and attacker — the second use triggers the revoke, both sides get kicked.
|
|
75
|
+
|
|
76
|
+
## Family — the rotation chain
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
login → creates family X — refresh token A in family X
|
|
80
|
+
refresh (A) → revokes A; creates B in family X
|
|
81
|
+
refresh (B) → revokes B; creates C in family X
|
|
82
|
+
refresh (A again)→ A is revoked → revoke family X entirely
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The family ties together "successive rotations of one session." In 5.19 it is also a durable `AuthTokenFamily` row: family revoke marks that family, revokes its refresh rows, and deletes its associated access rows atomically. Access rows from before family association are conservatively deleted only for the same user and user type; they are never guessed into a family.
|
|
86
|
+
|
|
87
|
+
## Listing active sessions
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const sessions = await authService.getActiveSessions(user);
|
|
91
|
+
|
|
92
|
+
for (const session of sessions) {
|
|
93
|
+
session.get("device_info"); // { userAgent, ip, deviceId? } if provided at login
|
|
94
|
+
session.get("created_at");
|
|
95
|
+
session.get("expires_at");
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Use this for "active sessions" UIs. Revoke a specific session by calling `.revoke()` on the `RefreshToken` instance.
|
|
100
|
+
|
|
101
|
+
## Removing tokens
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// Specific access token
|
|
105
|
+
await authService.removeAccessToken(user, accessTokenString);
|
|
106
|
+
|
|
107
|
+
// Specific session/family
|
|
108
|
+
await authService.revokeTokenFamily(familyId);
|
|
109
|
+
|
|
110
|
+
// All access tokens for a user
|
|
111
|
+
await authService.removeAllAccessTokens(user);
|
|
112
|
+
|
|
113
|
+
// Everything — access + refresh + family
|
|
114
|
+
await authService.revokeAllTokens(user);
|
|
115
|
+
|
|
116
|
+
// A specific family
|
|
117
|
+
await authService.revokeTokenFamily(familyId);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Max refresh tokens per user
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
// In config.auth.refreshToken:
|
|
124
|
+
{
|
|
125
|
+
maxPerUser: 5, // default
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
When issuing a new refresh token, the service counts active tokens for the user and revokes the oldest until count < `maxPerUser`. Pattern: limits how many simultaneous sessions a user can hold; prevents an attacker who got a token from gradually accumulating many.
|
|
130
|
+
|
|
131
|
+
## Expired-token cleanup
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const cleaned = await authService.cleanupExpiredTokens();
|
|
135
|
+
// Returns: number of expired refresh tokens removed (also purges expired access-token rows
|
|
136
|
+
// and hard-deletes expired or consumed one_time_tokens rows).
|
|
137
|
+
// Fires "token.expired" event per token + "cleanup.completed" with the count.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Every purge is a hard delete on every driver, including MongoDB. `AccessToken`, `RefreshToken`,
|
|
141
|
+
`OneTimeToken` and `PasskeyCredential` all declare `static deleteStrategy = "permanent"`, so a
|
|
142
|
+
purge never copies credential material into a `*Trash` collection despite MongoDB's driver
|
|
143
|
+
default being `"trash"`.
|
|
144
|
+
|
|
145
|
+
Run this periodically via the scheduler:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { scheduler, job } from "@warlock.js/scheduler";
|
|
149
|
+
import { authService } from "@warlock.js/auth";
|
|
150
|
+
|
|
151
|
+
scheduler.addJob(
|
|
152
|
+
job("auth-cleanup", () => authService.cleanupExpiredTokens())
|
|
153
|
+
.daily()
|
|
154
|
+
.at("03:00"),
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Or use the bundled CLI command — see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md).
|
|
159
|
+
|
|
160
|
+
## JWT helpers
|
|
161
|
+
|
|
162
|
+
For low-level JWT signing/verification (outside the authService flow):
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { jwt } from "@warlock.js/auth";
|
|
166
|
+
|
|
167
|
+
const token = await jwt.generate(payload, { expiresIn: 3_600_000 }); // milliseconds
|
|
168
|
+
const decoded = await jwt.verify(token);
|
|
169
|
+
|
|
170
|
+
const refreshToken = await jwt.generateRefreshToken(payload, { expiresIn });
|
|
171
|
+
const decodedRefresh = await jwt.verifyRefreshToken(refreshToken);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
⚠️ **`jwt.generate` / `jwt.generateRefreshToken` are the low-level escape hatch — `expiresIn` goes straight to `fast-jwt`.** It takes a number of milliseconds or an `ms`-style string, and rejects an unparseable string. What it does **not** reject is `undefined` or `0`: both skip the check and emit a token with **no `exp` claim**. Pass a positive value, and never pass the result of parsing a config string without checking it first.
|
|
175
|
+
|
|
176
|
+
**`jwt.verify` / `jwt.verifyRefreshToken` require an `exp` claim** (since 4.12.0) and reject a token without one. There is no deadline to check on such a token, so verification would otherwise succeed indefinitely. `requiredClaims` is additive — `jwt.verify(token, { requiredClaims: ["iat"] })` requires `iat` **and** `exp`; you cannot opt out of `exp`. If you want a token that effectively never expires, use `expiresIn: NO_EXPIRATION` (`"100y"`), which stamps a real `exp` about a century out.
|
|
177
|
+
|
|
178
|
+
If tokens minted before 4.12.0 may lack an `exp`, see `warlock auth.purge-never-expiring` in [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — they are still in your token tables and no date-based cleanup can reach them.
|
|
179
|
+
|
|
180
|
+
`authService.generateAccessToken` / `createRefreshToken` read `config.auth.accessToken.expiresIn` / `config.auth.refreshToken.expiresIn`, validate them, and throw naming the key if the value is not a positive `ms` duration — before anything is signed, persisted, or capped.
|
|
181
|
+
|
|
182
|
+
The package signs access and refresh tokens with independent secrets — `config.auth.accessToken.secret` and `config.auth.refreshToken.secret`. Setting a distinct `refresh.secret` is recommended: it prevents an access-token compromise from forging refresh tokens (and vice versa). The refresh secret is **optional** — when `config.auth.refreshToken.secret` is unset, refresh tokens fall back to the main `config.auth.accessToken.secret`, so refresh works out of the box without a second secret.
|
|
183
|
+
|
|
184
|
+
## Things NOT to do
|
|
185
|
+
|
|
186
|
+
- Don't use raw JWT libraries directly. The package handles signing, verification, secret loading, and the access/refresh split.
|
|
187
|
+
- Don't disable rotation (`config.auth.refreshToken.rotation = false`) unless you genuinely understand the tradeoff — you lose replay detection.
|
|
188
|
+
- Don't increase `maxPerUser` to a huge number "to be safe." Each active refresh token is a revocation surface; fewer simultaneous tokens means less attack surface.
|
|
189
|
+
- Don't manually delete `AccessToken` rows in a service. The user might be hitting a request mid-revoke and get an inconsistent state. Use the `authService` helpers.
|
|
190
|
+
- Run the additive `authMigrations` on upgrade. Do not alter or backfill token rows yourself; legacy access tokens are handled conservatively on family revoke.
|
|
191
|
+
|
|
192
|
+
## See also
|
|
193
|
+
|
|
194
|
+
- [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — full login/logout flow that uses these primitives
|
|
195
|
+
- [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — the bundled cleanup command
|
|
196
|
+
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — scheduling cleanup
|
package/skills/overview/SKILL.md
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: overview
|
|
3
|
-
description: "@warlock.js/auth overview for orientation and choosing the focused skill for the task."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# `@warlock.js/auth` — overview
|
|
7
|
-
|
|
8
|
-
JWT authentication for Warlock apps. You get a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` that runs login/logout/refresh (with refresh-token rotation and replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, and two bundled CLI commands.
|
|
9
|
-
|
|
10
|
-
Coupled to `@warlock.js/core` — you're inside a Warlock project before this package makes sense.
|
|
11
|
-
|
|
12
|
-
Server-only: `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build refuses app client code that value-imports this package; type-only imports are allowed, and server loaders/controllers/modules are unaffected.
|
|
13
|
-
|
|
14
|
-
## When to reach for it
|
|
15
|
-
|
|
16
|
-
- You're building a Warlock app that needs login, protected routes, and token-based sessions.
|
|
17
|
-
- You need **multiple user types** (admins + regular users, or client/vendor/staff personas) gated separately on the same auth system.
|
|
18
|
-
- You want refresh-token **rotation + replay detection** out of the box rather than hand-rolling token security.
|
|
19
|
-
|
|
20
|
-
Skip if you're not on `@warlock.js/core` (the package depends on it), or if you need session-cookie auth rather than JWTs.
|
|
21
|
-
|
|
22
|
-
## The mental model in one paragraph
|
|
23
|
-
|
|
24
|
-
Your user model extends the `Auth` base model and declares its `userType`. A login flows through `authService.login(Model, credentials, deviceInfo?)`: it verifies the password, issues a persisted access + refresh pair, and fires events. `authMiddleware("user")` is a typed hard gate; `authMiddleware()` resolves the configured default type; the object form selects header/cookie credentials, `optional`, and a page-local redirect. Refresh rotation uses a durable token family: family revocation invalidates its refresh and associated access rows. CLI commands generate the JWT secret and clean up expired tokens.
|
|
25
|
-
|
|
26
|
-
## Skills index
|
|
27
|
-
|
|
28
|
-
Nine task skills. Most apps need `auth-basics` + `protect-routes` + `handle-login-and-logout`.
|
|
29
|
-
|
|
30
|
-
### Foundations
|
|
31
|
-
|
|
32
|
-
#### [`auth-basics`](@warlock.js/auth/auth-basics/SKILL.md)
|
|
33
|
-
Start here. The `Auth` base model, `authMiddleware` gate, `authService` (login/logout/refresh), AccessToken + RefreshToken persistence, multi-user-type support.
|
|
34
|
-
|
|
35
|
-
### The flows
|
|
36
|
-
|
|
37
|
-
#### [`handle-login-and-logout`](@warlock.js/auth/handle-login-and-logout/SKILL.md)
|
|
38
|
-
`authService.login(Model, credentials, deviceInfo?)` — verify password, issue the token pair, fire events. `authService.logout(user, accessToken?, refreshToken?)` — revoke tokens. For your `POST /login` and `POST /logout` controllers.
|
|
39
|
-
|
|
40
|
-
#### [`register-user`](@warlock.js/auth/register-user/SKILL.md)
|
|
41
|
-
Sign up a new user and issue the first token pair — `User.create({ ...password: await hashPassword(plain) })` then `authService.createTokenPair(user)`. For `POST /register`.
|
|
42
|
-
|
|
43
|
-
#### [`protect-routes`](@warlock.js/auth/protect-routes/SKILL.md)
|
|
44
|
-
`authMiddleware` object and legacy overloads: typed/default hard gates, optional resolution, credential source, and page-local redirects. Sets `request.locals.user` + `request.decodedAccessToken` when a user resolves.
|
|
45
|
-
|
|
46
|
-
### Going deeper
|
|
47
|
-
|
|
48
|
-
#### [`manage-tokens`](@warlock.js/auth/manage-tokens/SKILL.md)
|
|
49
|
-
The token lifecycle — `generateAccessToken`, `createRefreshToken`, `createTokenPair`, `refreshTokens` (rotation + replay detection), `revokeAllTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`. For custom login/registration, token revocation, "logout everywhere", and scheduled cleanup.
|
|
50
|
-
|
|
51
|
-
#### [`customize-user-type`](@warlock.js/auth/customize-user-type/SKILL.md)
|
|
52
|
-
Support multiple user types in one system — each `Auth` subclass overrides `userType`, `config.auth.userType.<slug>` maps the slug to a model class, `authMiddleware("admin")` / `authMiddleware(["admin", "staff"])` gates per type.
|
|
53
|
-
|
|
54
|
-
#### [`customize-token-storage`](@warlock.js/auth/customize-token-storage/SKILL.md)
|
|
55
|
-
Override the persisted `AccessToken` / `RefreshToken` models to add columns (multi-tenant `organization_id`), rename, or change storage — extend the model + schema and register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`.
|
|
56
|
-
|
|
57
|
-
#### [`throttle-login-attempts`](@warlock.js/auth/throttle-login-attempts/SKILL.md)
|
|
58
|
-
Brute-force / credential-stuffing protection — `loginThrottleMiddleware()` counts only failed logins, resets on success, locks per-account + per-IP after a threshold, and rejects pre-controller with 429. Cache-backed, fails open on a cache outage.
|
|
59
|
-
|
|
60
|
-
#### [`run-auth-commands`](@warlock.js/auth/run-auth-commands/SKILL.md)
|
|
61
|
-
Two CLI commands — `warlock jwt.generate` (strong JWT secret → `.env`) and `warlock auth.cleanup` (remove expired refresh tokens). Register via `registerJWTSecretGeneratorCommand()` and `registerAuthCleanupCommand()`.
|
|
62
|
-
|
|
63
|
-
## What this package deliberately doesn't do
|
|
64
|
-
|
|
65
|
-
- **Session-cookie auth.** It's JWT/token-based. If you need server-side sessions, this isn't it.
|
|
66
|
-
- **OAuth / social login / SSO.** No provider adapters here — wire those at the controller layer and create the user through this package's models.
|
|
67
|
-
- **Authorization / roles / permissions (RBAC).** It authenticates (who you are) and gates by user *type*, not fine-grained permissions. Build RBAC on top.
|
|
68
|
-
- **Standalone use.** It depends on `@warlock.js/core` for routing, models (Cascade), and config.
|
|
69
|
-
|
|
70
|
-
## See also
|
|
71
|
-
|
|
72
|
-
- [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the framework auth runs inside (routing, middleware, config).
|
|
73
|
-
- [`@warlock.js/cascade/cascade-basics/SKILL.md`](@warlock.js/cascade/cascade-basics/SKILL.md) — the ORM behind the `Auth`, `AccessToken`, and `RefreshToken` models.
|
|
74
|
-
- `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes `.claude/skills/warlock-js-auth-overview/`.
|
|
1
|
+
---
|
|
2
|
+
name: overview
|
|
3
|
+
description: "@warlock.js/auth overview for orientation and choosing the focused skill for the task."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `@warlock.js/auth` — overview
|
|
7
|
+
|
|
8
|
+
JWT authentication for Warlock apps. You get a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` that runs login/logout/refresh (with refresh-token rotation and replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, and two bundled CLI commands.
|
|
9
|
+
|
|
10
|
+
Coupled to `@warlock.js/core` — you're inside a Warlock project before this package makes sense.
|
|
11
|
+
|
|
12
|
+
Server-only: `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build refuses app client code that value-imports this package; type-only imports are allowed, and server loaders/controllers/modules are unaffected.
|
|
13
|
+
|
|
14
|
+
## When to reach for it
|
|
15
|
+
|
|
16
|
+
- You're building a Warlock app that needs login, protected routes, and token-based sessions.
|
|
17
|
+
- You need **multiple user types** (admins + regular users, or client/vendor/staff personas) gated separately on the same auth system.
|
|
18
|
+
- You want refresh-token **rotation + replay detection** out of the box rather than hand-rolling token security.
|
|
19
|
+
|
|
20
|
+
Skip if you're not on `@warlock.js/core` (the package depends on it), or if you need session-cookie auth rather than JWTs.
|
|
21
|
+
|
|
22
|
+
## The mental model in one paragraph
|
|
23
|
+
|
|
24
|
+
Your user model extends the `Auth` base model and declares its `userType`. A login flows through `authService.login(Model, credentials, deviceInfo?)`: it verifies the password, issues a persisted access + refresh pair, and fires events. `authMiddleware("user")` is a typed hard gate; `authMiddleware()` resolves the configured default type; the object form selects header/cookie credentials, `optional`, and a page-local redirect. Refresh rotation uses a durable token family: family revocation invalidates its refresh and associated access rows. CLI commands generate the JWT secret and clean up expired tokens.
|
|
25
|
+
|
|
26
|
+
## Skills index
|
|
27
|
+
|
|
28
|
+
Nine task skills. Most apps need `auth-basics` + `protect-routes` + `handle-login-and-logout`.
|
|
29
|
+
|
|
30
|
+
### Foundations
|
|
31
|
+
|
|
32
|
+
#### [`auth-basics`](@warlock.js/auth/auth-basics/SKILL.md)
|
|
33
|
+
Start here. The `Auth` base model, `authMiddleware` gate, `authService` (login/logout/refresh), AccessToken + RefreshToken persistence, multi-user-type support.
|
|
34
|
+
|
|
35
|
+
### The flows
|
|
36
|
+
|
|
37
|
+
#### [`handle-login-and-logout`](@warlock.js/auth/handle-login-and-logout/SKILL.md)
|
|
38
|
+
`authService.login(Model, credentials, deviceInfo?)` — verify password, issue the token pair, fire events. `authService.logout(user, accessToken?, refreshToken?)` — revoke tokens. For your `POST /login` and `POST /logout` controllers.
|
|
39
|
+
|
|
40
|
+
#### [`register-user`](@warlock.js/auth/register-user/SKILL.md)
|
|
41
|
+
Sign up a new user and issue the first token pair — `User.create({ ...password: await hashPassword(plain) })` then `authService.createTokenPair(user)`. For `POST /register`.
|
|
42
|
+
|
|
43
|
+
#### [`protect-routes`](@warlock.js/auth/protect-routes/SKILL.md)
|
|
44
|
+
`authMiddleware` object and legacy overloads: typed/default hard gates, optional resolution, credential source, and page-local redirects. Sets `request.locals.user` + `request.decodedAccessToken` when a user resolves.
|
|
45
|
+
|
|
46
|
+
### Going deeper
|
|
47
|
+
|
|
48
|
+
#### [`manage-tokens`](@warlock.js/auth/manage-tokens/SKILL.md)
|
|
49
|
+
The token lifecycle — `generateAccessToken`, `createRefreshToken`, `createTokenPair`, `refreshTokens` (rotation + replay detection), `revokeAllTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`. For custom login/registration, token revocation, "logout everywhere", and scheduled cleanup.
|
|
50
|
+
|
|
51
|
+
#### [`customize-user-type`](@warlock.js/auth/customize-user-type/SKILL.md)
|
|
52
|
+
Support multiple user types in one system — each `Auth` subclass overrides `userType`, `config.auth.userType.<slug>` maps the slug to a model class, `authMiddleware("admin")` / `authMiddleware(["admin", "staff"])` gates per type.
|
|
53
|
+
|
|
54
|
+
#### [`customize-token-storage`](@warlock.js/auth/customize-token-storage/SKILL.md)
|
|
55
|
+
Override the persisted `AccessToken` / `RefreshToken` models to add columns (multi-tenant `organization_id`), rename, or change storage — extend the model + schema and register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`.
|
|
56
|
+
|
|
57
|
+
#### [`throttle-login-attempts`](@warlock.js/auth/throttle-login-attempts/SKILL.md)
|
|
58
|
+
Brute-force / credential-stuffing protection — `loginThrottleMiddleware()` counts only failed logins, resets on success, locks per-account + per-IP after a threshold, and rejects pre-controller with 429. Cache-backed, fails open on a cache outage.
|
|
59
|
+
|
|
60
|
+
#### [`run-auth-commands`](@warlock.js/auth/run-auth-commands/SKILL.md)
|
|
61
|
+
Two CLI commands — `warlock jwt.generate` (strong JWT secret → `.env`) and `warlock auth.cleanup` (remove expired refresh tokens). Register via `registerJWTSecretGeneratorCommand()` and `registerAuthCleanupCommand()`.
|
|
62
|
+
|
|
63
|
+
## What this package deliberately doesn't do
|
|
64
|
+
|
|
65
|
+
- **Session-cookie auth.** It's JWT/token-based. If you need server-side sessions, this isn't it.
|
|
66
|
+
- **OAuth / social login / SSO.** No provider adapters here — wire those at the controller layer and create the user through this package's models.
|
|
67
|
+
- **Authorization / roles / permissions (RBAC).** It authenticates (who you are) and gates by user *type*, not fine-grained permissions. Build RBAC on top.
|
|
68
|
+
- **Standalone use.** It depends on `@warlock.js/core` for routing, models (Cascade), and config.
|
|
69
|
+
|
|
70
|
+
## See also
|
|
71
|
+
|
|
72
|
+
- [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the framework auth runs inside (routing, middleware, config).
|
|
73
|
+
- [`@warlock.js/cascade/cascade-basics/SKILL.md`](@warlock.js/cascade/cascade-basics/SKILL.md) — the ORM behind the `Auth`, `AccessToken`, and `RefreshToken` models.
|
|
74
|
+
- `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes `.claude/skills/warlock-js-auth-overview/`.
|