@stacksjs/defaults 0.74.53 → 0.74.56
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/ai/skills/stacks-auth/SKILL.md +1 -1
- package/ai/skills/stacks-database/SKILL.md +6 -0
- package/app/Actions/Auth/DisableTwoFactorAction.ts +8 -3
- package/app/Actions/Auth/LoginAction.ts +14 -16
- package/app/Middleware/Csrf.ts +29 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
|
@@ -258,7 +258,7 @@ interface RbacStore { findRoleByName, createRole, deleteRole, getAllRoles, findP
|
|
|
258
258
|
- `SessionAuth.logout(sessionId): void`
|
|
259
259
|
- `SessionAuth.user(sessionId): Promise<UserModel | undefined>`
|
|
260
260
|
- `SessionAuth.check(sessionId): boolean`
|
|
261
|
-
- `SessionAuth.refresh(sessionId, ttlMs?): boolean
|
|
261
|
+
- `SessionAuth.refresh(sessionId, ttlMs?): boolean`, rejects non-positive or non-finite TTLs without changing the session
|
|
262
262
|
|
|
263
263
|
Internal: in-memory Map with 10k session limit, 5-minute eviction interval, timing-safe password comparison with dummy bcrypt hash.
|
|
264
264
|
|
|
@@ -86,6 +86,12 @@ const users = await db.selectFrom('users').where('active', '=', true).get()
|
|
|
86
86
|
|
|
87
87
|
`initializeDbConfig(config)` can be called to update the backing config at runtime.
|
|
88
88
|
|
|
89
|
+
For reads which cannot tolerate replication lag, use `db.primary.selectFrom(...)`.
|
|
90
|
+
It stays on the primary when automatic replica routing is enabled, and uses the
|
|
91
|
+
active transaction connection inside a transaction. It does not mark the request
|
|
92
|
+
as a writer or change routing for unrelated reads. Session authentication uses
|
|
93
|
+
this handle so a revoked session cannot authenticate from a stale replica.
|
|
94
|
+
|
|
89
95
|
## SQL Template Tag (types.ts)
|
|
90
96
|
|
|
91
97
|
```typescript
|
|
@@ -26,12 +26,17 @@ export default new Action({
|
|
|
26
26
|
// rather than trusting the bearer token alone (a stolen/leaked
|
|
27
27
|
// token shouldn't be enough to turn off the second factor it's
|
|
28
28
|
// meant to help guard against).
|
|
29
|
-
|
|
29
|
+
// Bind reconfirmation to the same account and password version while
|
|
30
|
+
// disabling. A concurrent password reset must invalidate this request.
|
|
31
|
+
const confirmed = await Auth.withVerifiedCredentials({ email: user.email, password }, async (verifiedUser) => {
|
|
32
|
+
if (String(verifiedUser.id) !== String(user.id))
|
|
33
|
+
return false
|
|
34
|
+
await disableTwoFactor(user.id as number)
|
|
35
|
+
return true
|
|
36
|
+
})
|
|
30
37
|
if (!confirmed)
|
|
31
38
|
return response.unauthorized('Incorrect password')
|
|
32
39
|
|
|
33
|
-
await disableTwoFactor(user.id as number)
|
|
34
|
-
|
|
35
40
|
return response.json({ enabled: false })
|
|
36
41
|
},
|
|
37
42
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { Auth, authCookie, createTwoFactorChallenge, getTwoFactorState } from '@stacksjs/auth'
|
|
3
|
-
import { User } from '@stacksjs/orm'
|
|
4
3
|
import { response } from '@stacksjs/router'
|
|
5
4
|
import { schema } from '@stacksjs/validation'
|
|
6
5
|
import { PASSWORD_MAX_LENGTH, PASSWORD_PRESENCE_MESSAGE } from '../../password-policy'
|
|
@@ -30,28 +29,27 @@ export default new Action({
|
|
|
30
29
|
const email = request.get('email')
|
|
31
30
|
const password = request.get('password')
|
|
32
31
|
|
|
33
|
-
// Verify
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// Verify once, then keep the observed password version locked while
|
|
33
|
+
// choosing and issuing the challenge or token. A completed password reset
|
|
34
|
+
// cannot be followed by this old in-flight login minting fresh access.
|
|
35
|
+
const decision = await Auth.withVerifiedCredentials({ email, password }, async (authedUser) => {
|
|
36
|
+
const { enabled: twoFactorEnabled } = await getTwoFactorState(authedUser.id as number)
|
|
37
|
+
if (twoFactorEnabled) {
|
|
38
|
+
return { kind: 'challenge' as const, token: await createTwoFactorChallenge(authedUser.id as number) }
|
|
39
|
+
}
|
|
40
|
+
return { kind: 'login' as const, result: await Auth.loginUsingId(authedUser.id as number) }
|
|
41
|
+
})
|
|
42
|
+
if (!decision)
|
|
39
43
|
return response.unauthorized('Incorrect email or password')
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
if (!authedUser)
|
|
43
|
-
return response.unauthorized('Incorrect email or password')
|
|
44
|
-
|
|
45
|
-
const { enabled: twoFactorEnabled } = await getTwoFactorState(authedUser.id as number)
|
|
46
|
-
if (twoFactorEnabled) {
|
|
47
|
-
const challengeToken = await createTwoFactorChallenge(authedUser.id as number)
|
|
45
|
+
if (decision.kind === 'challenge') {
|
|
48
46
|
return response.json({
|
|
49
47
|
requires_two_factor: true,
|
|
50
|
-
challenge_token:
|
|
48
|
+
challenge_token: decision.token,
|
|
51
49
|
})
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
const result =
|
|
52
|
+
const result = decision.result
|
|
55
53
|
if (!result)
|
|
56
54
|
return response.unauthorized('Incorrect email or password')
|
|
57
55
|
|
package/app/Middleware/Csrf.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto'
|
|
2
|
-
import { HttpError } from '@stacksjs/error-handling'
|
|
2
|
+
import { HttpError } from '@stacksjs/error-handling/http-error'
|
|
3
3
|
import type { EnhancedRequest } from '@stacksjs/router'
|
|
4
|
-
import { Middleware } from '@stacksjs/router'
|
|
4
|
+
import { Middleware } from '@stacksjs/router/middleware'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* CSRF Protection Middleware (default-on for unsafe methods)
|
|
@@ -144,7 +144,34 @@ export function createCsrfCookie(req: Request, minted?: string): string {
|
|
|
144
144
|
return `${CSRF_COOKIE_PREFIX}${token}${suffix}`
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Whether a browser could go on to use a CSRF token from this response.
|
|
149
|
+
*
|
|
150
|
+
* A page carries the forms that submit it, and an API answer is what an SPA
|
|
151
|
+
* reads before its next request. A stylesheet, script, font or image is
|
|
152
|
+
* neither, and a cookie on one is worse than useless: a response that sets a
|
|
153
|
+
* cookie is one no shared cache will store, so seeding every static file kept
|
|
154
|
+
* the site's whole asset set out of the CDN (every file came back
|
|
155
|
+
* `cf-cache-status: BYPASS`). A cacheable file carrying a per-visitor token is
|
|
156
|
+
* also the shape of a leak, should any cache in the path store it anyway.
|
|
157
|
+
*
|
|
158
|
+
* No content type at all (a redirect, an empty answer) keeps the old
|
|
159
|
+
* behaviour: it says nothing about what the browser is looking at.
|
|
160
|
+
*/
|
|
161
|
+
export function responseMayUseCsrfToken(response: Response): boolean {
|
|
162
|
+
const type = (response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
|
|
163
|
+
if (!type)
|
|
164
|
+
return true
|
|
165
|
+
return type === 'text/html'
|
|
166
|
+
|| type === 'application/xhtml+xml'
|
|
167
|
+
|| type === 'application/json'
|
|
168
|
+
|| type.endsWith('+json')
|
|
169
|
+
}
|
|
170
|
+
|
|
147
171
|
export function seedCsrfCookieIfMissing(req: Request, response: Response, minted?: string, responseHasNoCookies = false): Response {
|
|
172
|
+
if (!responseMayUseCsrfToken(response))
|
|
173
|
+
return response
|
|
174
|
+
|
|
148
175
|
// A token the router minted before rendering wins over "the header already
|
|
149
176
|
// has one", because it put that value in the header itself - and the page
|
|
150
177
|
// has already embedded it in every form it drew. Generating a second token
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.56",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
58
|
-
"@stacksjs/mobile": "^0.74.
|
|
58
|
+
"@stacksjs/mobile": "^0.74.56",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|