@warlock.js/auth 5.19.1 → 5.21.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 +25 -1
- package/esm/contracts/request-locals.d.mts +5 -0
- package/esm/contracts/request-locals.d.mts.map +1 -1
- package/esm/contracts/types.d.mts +13 -0
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +3 -1
- package/esm/index.mjs +6 -3
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +37 -114
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs +6 -2
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/services/auth-config.mjs +2 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +36 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +67 -0
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/index.d.mts +2 -1
- package/esm/services/index.mjs +1 -0
- package/esm/services/jwt.d.mts.map +1 -1
- package/esm/services/jwt.mjs +4 -1
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/services/resolve-request-user.d.mts +49 -0
- package/esm/services/resolve-request-user.d.mts.map +1 -0
- package/esm/services/resolve-request-user.mjs +131 -0
- package/esm/services/resolve-request-user.mjs.map +1 -0
- package/esm/session/index.d.mts +1 -0
- package/esm/session/index.mjs +3 -0
- package/esm/session/page-session.d.mts +49 -0
- package/esm/session/page-session.d.mts.map +1 -0
- package/esm/session/page-session.mjs +119 -0
- package/esm/session/page-session.mjs.map +1 -0
- package/esm/utils/index.mjs +1 -1
- package/llms-full.txt +1565 -1564
- package/llms.txt +8 -8
- package/package.json +9 -9
- 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 +183 -179
- package/skills/index.md +22 -0
- package/skills/login-with-providers/SKILL.md +283 -283
- package/skills/manage-tokens/SKILL.md +15 -15
- package/skills/protect-routes/SKILL.md +288 -288
- package/skills/register-user/SKILL.md +147 -147
- package/skills/run-auth-commands/SKILL.md +159 -159
- package/skills/throttle-login-attempts/SKILL.md +2 -1
- package/skills/verify-email-and-reset-password/SKILL.md +137 -137
|
@@ -1,179 +1,183 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: handle-login-and-logout
|
|
3
|
-
description:
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Login + logout
|
|
7
|
-
|
|
8
|
-
`authService` exposes the full flow. Pass the model class so the service knows which user-type to look up.
|
|
9
|
-
|
|
10
|
-
```ts title="src/app/users/models/user/user.model.ts"
|
|
11
|
-
import { Auth } from "@warlock.js/auth";
|
|
12
|
-
import { RegisterModel } from "@warlock.js/cascade";
|
|
13
|
-
|
|
14
|
-
@RegisterModel()
|
|
15
|
-
export class User extends Auth {
|
|
16
|
-
public static table = "users";
|
|
17
|
-
|
|
18
|
-
public get userType(): string {
|
|
19
|
-
return "user";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Login — `authService.login(Model, credentials, deviceInfo?)`
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { authService } from "@warlock.js/auth";
|
|
28
|
-
import { type RequestHandler } from "@warlock.js/core";
|
|
29
|
-
import { User } from "app/users/models/user/user.model";
|
|
30
|
-
|
|
31
|
-
export const loginController: RequestHandler = async ({ request, response }) => {
|
|
32
|
-
const result = await authService.login(User, {
|
|
33
|
-
email: request.input("email"),
|
|
34
|
-
password: request.input("password"),
|
|
35
|
-
}, {
|
|
36
|
-
userAgent: request.header("user-agent"),
|
|
37
|
-
ip: request.ip,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (!result) {
|
|
41
|
-
return response.unauthorized({ error: "Invalid credentials" });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return response.success({
|
|
45
|
-
user: result.user,
|
|
46
|
-
tokens: result.tokens,
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
The returned shape:
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
{
|
|
55
|
-
user: T, // your User subclass, hydrated
|
|
56
|
-
tokens: {
|
|
57
|
-
accessToken: { token: string, expiresAt: string },
|
|
58
|
-
refreshToken?: { token: string, expiresAt: string }, // omitted if refresh tokens disabled
|
|
59
|
-
},
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Returns `null` on failure (wrong password, user not found). The service emits `login.attempt` → `login.success` or `login.failed` events as it goes — subscribe via the auth event bus if you need an audit trail.
|
|
64
|
-
|
|
65
|
-
## What `credentials` looks like
|
|
66
|
-
|
|
67
|
-
The shape is **arbitrary** — every key except `password` is used as a `where(...)` filter against the model. The password is verified separately via bcrypt.
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
// Email + password
|
|
71
|
-
authService.login(User, { email: "ada@example.com", password: "..." });
|
|
72
|
-
|
|
73
|
-
// Username + password
|
|
74
|
-
authService.login(User, { username: "ada", password: "..." });
|
|
75
|
-
|
|
76
|
-
// Phone-based OTP (where password is the OTP hash)
|
|
77
|
-
authService.login(User, { phone: "+1...", password: hashedOTP });
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
For lower-level credential verification (just check, don't issue tokens), use `authService.attemptLogin(Model, credentials)` — returns the user or null without creating tokens.
|
|
81
|
-
|
|
82
|
-
## Device info
|
|
83
|
-
|
|
84
|
-
The optional `deviceInfo` carries metadata into the refresh token row:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
authService.login(User, credentials, {
|
|
88
|
-
userAgent: request.header("user-agent"),
|
|
89
|
-
ip: request.ip,
|
|
90
|
-
deviceId: "...", // your client-side device fingerprint
|
|
91
|
-
familyId: "...", // pre-existing family for token rotation, usually omitted
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Useful for "show active sessions" UIs — see `authService.getActiveSessions(user)`.
|
|
96
|
-
|
|
97
|
-
## Logout — `authService.logout(user, accessToken?, refreshToken?)`
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
import { type RequestHandler } from "@warlock.js/core";
|
|
101
|
-
|
|
102
|
-
export const logoutController: RequestHandler = async ({ request, response }) => {
|
|
103
|
-
await authService.logout(
|
|
104
|
-
request.locals.user!,
|
|
105
|
-
request.authorizationValue, // access token from the Authorization header
|
|
106
|
-
request.input("refreshToken"), // refresh token from the request body
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
return response.success({ message: "Logged out" });
|
|
110
|
-
};
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
The contract:
|
|
114
|
-
- **Pass the access token** → that specific access-token row is deleted.
|
|
115
|
-
- **Pass the refresh token** → that specific refresh-token row is revoked.
|
|
116
|
-
- **Omit refresh token** → behavior depends on `config.auth.refreshToken.logoutWithoutToken`:
|
|
117
|
-
- `"revoke-all"` (default) — every refresh token for this user is revoked. Fail-safe.
|
|
118
|
-
- `"error"` — throws. Force the client to send the refresh token.
|
|
119
|
-
|
|
120
|
-
The `revoke-all` default is the right call for most apps. If a client loses track of the refresh token, logout still works and the user has to log in fresh on every device.
|
|
121
|
-
|
|
122
|
-
## Logout-everywhere
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
await authService.revokeAllTokens(user);
|
|
126
|
-
// Revokes every refresh token + deletes every access token for this user.
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Useful for "logout from all devices" buttons. Fires `token.revoked` per token + `logout.all` once.
|
|
130
|
-
|
|
131
|
-
## Refresh tokens — `authService.refreshTokens(refreshTokenString, deviceInfo?)`
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
import { type RequestHandler } from "@warlock.js/core";
|
|
135
|
-
|
|
136
|
-
export const refreshController: RequestHandler = async ({ request, response }) => {
|
|
137
|
-
const tokens = await authService.refreshTokens(
|
|
138
|
-
request.input("refreshToken"),
|
|
139
|
-
{ userAgent: request.header("user-agent"), ip: request.ip },
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
if (!tokens) {
|
|
143
|
-
return response.unauthorized({ error: "Invalid refresh token" });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return response.success({ tokens });
|
|
147
|
-
};
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Returns a new token pair or `null` (token expired, revoked, or replay-detected). With rotation enabled (default), the old refresh token is consumed; the new pair stays in the same "family." Replay → revoke the whole family. See [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md).
|
|
151
|
-
|
|
152
|
-
## Auth events
|
|
153
|
-
|
|
154
|
-
`authEvents` is a type-safe event bus (over `@mongez/events`) that fires on every meaningful auth moment. Subscribe with `on` / `subscribe`, unsubscribe with `off` / `unsubscribeAll`:
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
import { authEvents } from "@warlock.js/auth";
|
|
158
|
-
|
|
159
|
-
authEvents.on("login.success", (user, tokens, deviceInfo) => { /* audit */ });
|
|
160
|
-
authEvents.on("login.failed", (credentials, reason) => { /* alert on brute force */ });
|
|
161
|
-
authEvents.on("logout", (user) => { /* clear server-side session, if any */ });
|
|
162
|
-
authEvents.on("token.refreshed", (user, newPair, oldToken) => { /* track rotation */ });
|
|
163
|
-
authEvents.on("cleanup.completed", (count) => { /* metrics */ });
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Full event list: `login.attempt`, `login.success`, `login.failed`, `logout`, `logout.all`, `logout.failsafe`, `token.created`, `token.refreshed`, `token.revoked`, `token.expired`, `token.familyRevoked`, `session.created`, `session.destroyed`, `cleanup.completed`.
|
|
167
|
-
|
|
168
|
-
## Things NOT to do
|
|
169
|
-
|
|
170
|
-
- Don't `authService.login(User, { password })` without other credentials — the password is the secret; the other fields are the lookup. A login with only a password is a logic bug.
|
|
171
|
-
- Don't return the password hash in the response. `static toJsonColumns` on the User model should explicitly exclude it.
|
|
172
|
-
- Don't store the refresh token in localStorage. Use an httpOnly secure cookie for refresh tokens; the access token can sit in memory.
|
|
173
|
-
- Don't issue a new token pair without revoking the old one when rotation is enabled. `refreshTokens` does this for you — don't bypass it.
|
|
174
|
-
|
|
175
|
-
## See also
|
|
176
|
-
|
|
177
|
-
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token lifecycle, rotation, family revocation
|
|
178
|
-
- [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) — sign-up that issues tokens after creation
|
|
179
|
-
- [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — where the access token gets consumed
|
|
1
|
+
---
|
|
2
|
+
name: handle-login-and-logout
|
|
3
|
+
description: 'Run the full login flow via authService.login(Model, credentials, deviceInfo?) — verify password, create access + refresh token pair, fire events. Logout via authService.logout(user, accessToken?, refreshToken?) revokes tokens. Triggers: `authService.login`, `authService.logout`, `authService.attemptLogin`, `authService.refreshTokens`, `authService.revokeAllTokens`, `authEvents`; "build a login endpoint", "POST /login controller", "logout from all devices", "verify credentials and issue tokens"; typical import `import { authService, authEvents } from "@warlock.js/auth"`. Skip: token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; sign-up — `@warlock.js/auth/register-user/SKILL.md`; competing libs `passport-local`, `next-auth` credentials.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Login + logout
|
|
7
|
+
|
|
8
|
+
`authService` exposes the full flow. Pass the model class so the service knows which user-type to look up.
|
|
9
|
+
|
|
10
|
+
```ts title="src/app/users/models/user/user.model.ts"
|
|
11
|
+
import { Auth } from "@warlock.js/auth";
|
|
12
|
+
import { RegisterModel } from "@warlock.js/cascade";
|
|
13
|
+
|
|
14
|
+
@RegisterModel()
|
|
15
|
+
export class User extends Auth {
|
|
16
|
+
public static table = "users";
|
|
17
|
+
|
|
18
|
+
public get userType(): string {
|
|
19
|
+
return "user";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Login — `authService.login(Model, credentials, deviceInfo?)`
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { authService } from "@warlock.js/auth";
|
|
28
|
+
import { type RequestHandler } from "@warlock.js/core";
|
|
29
|
+
import { User } from "app/users/models/user/user.model";
|
|
30
|
+
|
|
31
|
+
export const loginController: RequestHandler = async ({ request, response }) => {
|
|
32
|
+
const result = await authService.login(User, {
|
|
33
|
+
email: request.input("email"),
|
|
34
|
+
password: request.input("password"),
|
|
35
|
+
}, {
|
|
36
|
+
userAgent: request.header("user-agent"),
|
|
37
|
+
ip: request.ip,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!result) {
|
|
41
|
+
return response.unauthorized({ error: "Invalid credentials" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return response.success({
|
|
45
|
+
user: result.user,
|
|
46
|
+
tokens: result.tokens,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The returned shape:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
{
|
|
55
|
+
user: T, // your User subclass, hydrated
|
|
56
|
+
tokens: {
|
|
57
|
+
accessToken: { token: string, expiresAt: string },
|
|
58
|
+
refreshToken?: { token: string, expiresAt: string }, // omitted if refresh tokens disabled
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Returns `null` on failure (wrong password, user not found). The service emits `login.attempt` → `login.success` or `login.failed` events as it goes — subscribe via the auth event bus if you need an audit trail.
|
|
64
|
+
|
|
65
|
+
## What `credentials` looks like
|
|
66
|
+
|
|
67
|
+
The shape is **arbitrary** — every key except `password` is used as a `where(...)` filter against the model. The password is verified separately via bcrypt.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// Email + password
|
|
71
|
+
authService.login(User, { email: "ada@example.com", password: "..." });
|
|
72
|
+
|
|
73
|
+
// Username + password
|
|
74
|
+
authService.login(User, { username: "ada", password: "..." });
|
|
75
|
+
|
|
76
|
+
// Phone-based OTP (where password is the OTP hash)
|
|
77
|
+
authService.login(User, { phone: "+1...", password: hashedOTP });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For lower-level credential verification (just check, don't issue tokens), use `authService.attemptLogin(Model, credentials)` — returns the user or null without creating tokens.
|
|
81
|
+
|
|
82
|
+
## Device info
|
|
83
|
+
|
|
84
|
+
The optional `deviceInfo` carries metadata into the refresh token row:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
authService.login(User, credentials, {
|
|
88
|
+
userAgent: request.header("user-agent"),
|
|
89
|
+
ip: request.ip,
|
|
90
|
+
deviceId: "...", // your client-side device fingerprint
|
|
91
|
+
familyId: "...", // pre-existing family for token rotation, usually omitted
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Useful for "show active sessions" UIs — see `authService.getActiveSessions(user)`.
|
|
96
|
+
|
|
97
|
+
## Logout — `authService.logout(user, accessToken?, refreshToken?)`
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { type RequestHandler } from "@warlock.js/core";
|
|
101
|
+
|
|
102
|
+
export const logoutController: RequestHandler = async ({ request, response }) => {
|
|
103
|
+
await authService.logout(
|
|
104
|
+
request.locals.user!,
|
|
105
|
+
request.authorizationValue, // access token from the Authorization header
|
|
106
|
+
request.input("refreshToken"), // refresh token from the request body
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return response.success({ message: "Logged out" });
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The contract:
|
|
114
|
+
- **Pass the access token** → that specific access-token row is deleted.
|
|
115
|
+
- **Pass the refresh token** → that specific refresh-token row is revoked.
|
|
116
|
+
- **Omit refresh token** → behavior depends on `config.auth.refreshToken.logoutWithoutToken`:
|
|
117
|
+
- `"revoke-all"` (default) — every refresh token for this user is revoked. Fail-safe.
|
|
118
|
+
- `"error"` — throws. Force the client to send the refresh token.
|
|
119
|
+
|
|
120
|
+
The `revoke-all` default is the right call for most apps. If a client loses track of the refresh token, logout still works and the user has to log in fresh on every device.
|
|
121
|
+
|
|
122
|
+
## Logout-everywhere
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
await authService.revokeAllTokens(user);
|
|
126
|
+
// Revokes every refresh token + deletes every access token for this user.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Useful for "logout from all devices" buttons. Fires `token.revoked` per token + `logout.all` once.
|
|
130
|
+
|
|
131
|
+
## Refresh tokens — `authService.refreshTokens(refreshTokenString, deviceInfo?)`
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { type RequestHandler } from "@warlock.js/core";
|
|
135
|
+
|
|
136
|
+
export const refreshController: RequestHandler = async ({ request, response }) => {
|
|
137
|
+
const tokens = await authService.refreshTokens(
|
|
138
|
+
request.input("refreshToken"),
|
|
139
|
+
{ userAgent: request.header("user-agent"), ip: request.ip },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (!tokens) {
|
|
143
|
+
return response.unauthorized({ error: "Invalid refresh token" });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return response.success({ tokens });
|
|
147
|
+
};
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Returns a new token pair or `null` (token expired, revoked, or replay-detected). With rotation enabled (default), the old refresh token is consumed; the new pair stays in the same "family." Replay → revoke the whole family. See [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md).
|
|
151
|
+
|
|
152
|
+
## Auth events
|
|
153
|
+
|
|
154
|
+
`authEvents` is a type-safe event bus (over `@mongez/events`) that fires on every meaningful auth moment. Subscribe with `on` / `subscribe`, unsubscribe with `off` / `unsubscribeAll`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { authEvents } from "@warlock.js/auth";
|
|
158
|
+
|
|
159
|
+
authEvents.on("login.success", (user, tokens, deviceInfo) => { /* audit */ });
|
|
160
|
+
authEvents.on("login.failed", (credentials, reason) => { /* alert on brute force */ });
|
|
161
|
+
authEvents.on("logout", (user) => { /* clear server-side session, if any */ });
|
|
162
|
+
authEvents.on("token.refreshed", (user, newPair, oldToken) => { /* track rotation */ });
|
|
163
|
+
authEvents.on("cleanup.completed", (count) => { /* metrics */ });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Full event list: `login.attempt`, `login.success`, `login.failed`, `logout`, `logout.all`, `logout.failsafe`, `token.created`, `token.refreshed`, `token.revoked`, `token.expired`, `token.familyRevoked`, `session.created`, `session.destroyed`, `cleanup.completed`.
|
|
167
|
+
|
|
168
|
+
## Things NOT to do
|
|
169
|
+
|
|
170
|
+
- Don't `authService.login(User, { password })` without other credentials — the password is the secret; the other fields are the lookup. A login with only a password is a logic bug.
|
|
171
|
+
- Don't return the password hash in the response. `static toJsonColumns` on the User model should explicitly exclude it.
|
|
172
|
+
- Don't store the refresh token in localStorage. Use an httpOnly secure cookie for refresh tokens; the access token can sit in memory.
|
|
173
|
+
- Don't issue a new token pair without revoking the old one when rotation is enabled. `refreshTokens` does this for you — don't bypass it.
|
|
174
|
+
|
|
175
|
+
## See also
|
|
176
|
+
|
|
177
|
+
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token lifecycle, rotation, family revocation
|
|
178
|
+
- [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) — sign-up that issues tokens after creation
|
|
179
|
+
- [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — where the access token gets consumed
|
|
180
|
+
|
|
181
|
+
## Browser sessions for Web pages
|
|
182
|
+
|
|
183
|
+
For cookie sessions read by `@warlock.js/web` pages, log in with `authService.loginWithSessionCookies(request, response, Model, credentials)`. It requires a same-origin `Origin`/`Referer` even without cookies (login CSRF) and sets the access and refresh cookies (`auth.cookie.name`, `auth.cookie.refreshName`, `Path=/`). Log out with `authService.logout(user, access, refresh)` then `authService.clearSessionCookies(response)`. Resolve pages with `pageSession({ project })` (`web.session`); header beats cookie; `auth.session.maxAge` defaults to `"30d"`. Multi-instance: share the JWT secret and the cache store, keep clocks in sync.
|
package/skills/index.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "JWT authentication for Warlock apps: login/logout, refresh-token rotation, protected routes, multiple user types. Exports `Auth`, `authService`, `authMiddleware`, `authEvents`, `currentUser`, `loginThrottleMiddleware`, `requireVerifiedEmail`, `GoogleProvider`, `AuthErrorCodes`. Use for: \"log a user in and issue tokens\", \"protect this route\", \"add an admin user type\", \"rate-limit login attempts\", \"verify email / reset password\", \"sign in with Google\". Not this package: general route middleware lives in @warlock.js/core; request-body validation is @warlock.js/seal."
|
|
3
|
+
---
|
|
4
|
+
# @warlock.js/auth
|
|
5
|
+
|
|
6
|
+
A user model extends the `Auth` base model and declares a `userType`. `authService.login(Model, credentials)` verifies the password and issues persisted access + refresh tokens; `authMiddleware("user")` gates routes per type. Refresh tokens rotate within a token family, with replay detection. Server-only, and requires `@warlock.js/core`.
|
|
7
|
+
|
|
8
|
+
## The 80% path
|
|
9
|
+
1. Orient with `overview.md`, then `auth-basics.md` for the shape of config and models.
|
|
10
|
+
2. Extend `Auth` for your user model: `customize-user-type.md`, `register-user.md`.
|
|
11
|
+
3. Log in / out: `handle-login-and-logout.md`; token lifecycle: `manage-tokens.md`.
|
|
12
|
+
4. Guard routes: `protect-routes.md`.
|
|
13
|
+
5. Harden: `throttle-login-attempts.md`, `verify-email-and-reset-password.md`.
|
|
14
|
+
6. Social sign-in: `login-with-providers.md`; CLI (JWT secret, token cleanup): `run-auth-commands.md`.
|
|
15
|
+
|
|
16
|
+
## Conventions and pitfalls
|
|
17
|
+
- Tokens are persisted rows, not stateless JWTs alone; revoking a family invalidates its refresh and access rows.
|
|
18
|
+
- `authMiddleware()` with no argument resolves the configured default user type. Be explicit when an app has several.
|
|
19
|
+
- Not for session-cookie auth; this is JWT-based.
|
|
20
|
+
- Do not value-import this package from client code; `@warlock.js/web` builds refuse it (type-only imports are fine).
|
|
21
|
+
- Generate the JWT secret with the bundled command rather than inventing one.
|
|
22
|
+
- Custom token storage is a supported extension point: `customize-token-storage.md`.
|