mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-03 20:43:22 +00:00
fix(auth): fail closed on otp-login token reuse check (#4421)
/login/otp and /login/recovery-code marked the otp-login token used through a rate-limit check that admits on a store error, so while the store was unavailable a captured token could complete another sign-in. The check now fails closed. Also drops the check-translations scripts and the AGENTS.md step that runs them; the script they call was removed in #2873.
This commit is contained in:
1 parent
5d907d18c9
commit
11114c80d6
5 files changed
+46
-4
No files matched your search
@@ -201,6 +201,6 @@ Recipes are tested, copy-pasteable patterns for a task, one layer above the per-
|
||||
|
||||
- **Reuse existing UI primitives before writing new ones.** Windows and dialogs are `UIWindow*` functions in [src/gui/src/UI/](src/gui/src/UI/); generic pieces already exist (`UIAlert`, `UIPrompt`, `UIContextMenu`, `UINotification`, `UIPopover`, widgets in [UI/Components/](src/gui/src/UI/Components/)). A new window should read like its neighbors: an async function taking an options object, composing an HTML string, wiring behavior with jQuery, delegating to `UIWindow(...)`.
|
||||
- **Match the visual language.** Use existing CSS classes (`button`, `button-primary`, window chrome, form styles) and copy the layout patterns of neighboring windows; new styles go in [src/gui/src/css/](src/gui/src/css/) following existing conventions. Verify anything positional (menus, overlays, z-index) on both desktop and mobile viewports.
|
||||
- **i18n every user-facing string.** No hardcoded UI text — use `i18n('key')` and add the key to [src/gui/src/i18n/translations/en.js](src/gui/src/i18n/translations/en.js). Run `npm run check-translations` before opening the PR.
|
||||
- **i18n every user-facing string.** No hardcoded UI text — use `i18n('key')` and add the key to [src/gui/src/i18n/translations/en.js](src/gui/src/i18n/translations/en.js).
|
||||
- **Shared logic goes in helpers/services.** Reusable non-UI logic belongs in [src/gui/src/helpers/](src/gui/src/helpers/) or [src/gui/src/services/](src/gui/src/services/), not copy-pasted between windows.
|
||||
- **Tests.** Vitest is wired for the GUI (`src/**/*.test.js`; see [appOrder.test.js](src/gui/src/UI/Dashboard/appOrder.test.js) for the shape). Extract pure logic into functions and test those. Desktop behavior driven through puter.js (`puter.ui.*`) is covered by the Playwright harness in [src/puter-js/tests/e2e/](src/puter-js/tests/e2e/) — add a spec there when you change how the desktop renders SDK-driven UI.
|
||||
@@ -54,7 +54,6 @@
|
||||
"dev": "npm start",
|
||||
"build": "npm run setupExtensions && npm run build:ts && cd src/gui && node ./build.js && cd ../puter-js && npm run build",
|
||||
"build:workerLib": "cd src/puter-js && npm run build && cd ../worker && npm run build",
|
||||
"check-translations": "node tools/check-translations.js",
|
||||
"prepare": "husky",
|
||||
"build:ts": "tsc -p tsconfig.build.json && node ./tools/write-dist-package-json.mjs",
|
||||
"check:puterjs:types": "node tools/checkPuterjsTypes.mjs",
|
||||
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
adminOnlyGate,
|
||||
requireUserActorGate,
|
||||
} from '../../core/http/middleware/gates.js';
|
||||
import {
|
||||
configureRateLimit,
|
||||
listConfiguredRateLimitBackends,
|
||||
} from '../../core/http/middleware/rateLimit.js';
|
||||
import {
|
||||
ROUTES_METADATA_KEY,
|
||||
type CollectedRoute,
|
||||
@@ -1753,6 +1757,40 @@ describe('AuthController.handleLoginOtp + handleLoginRecoveryCode', () => {
|
||||
).rejects.toMatchObject({ statusCode: 400 });
|
||||
});
|
||||
|
||||
it('refuses the sign-in while the used-token store is down', async () => {
|
||||
const { user, secret, otpToken } = await startOtpLogin();
|
||||
const token = await otpToken();
|
||||
|
||||
const { default: backend } = listConfiguredRateLimitBackends();
|
||||
configureRateLimit({
|
||||
default: 'redis',
|
||||
redis: {
|
||||
multi: () => {
|
||||
throw new Error('store down');
|
||||
},
|
||||
},
|
||||
});
|
||||
const logged = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const res = makeRes();
|
||||
try {
|
||||
await expect(
|
||||
controller.handleLoginOtp(
|
||||
makeReq({ token, code: liveTotp(user.username, secret) }),
|
||||
res,
|
||||
),
|
||||
).rejects.toThrow('store down');
|
||||
} finally {
|
||||
logged.mockRestore();
|
||||
configureRateLimit({
|
||||
default: backend,
|
||||
redis: server.clients.redis,
|
||||
kv: server.stores.kv,
|
||||
metering: server.services.metering,
|
||||
});
|
||||
}
|
||||
expect(res.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('a wrong code leaves the token usable for a retry', async () => {
|
||||
const { user, secret, otpToken } = await startOtpLogin();
|
||||
const token = await otpToken();
|
||||
|
||||
@@ -4637,13 +4637,19 @@ export class AuthController extends PuterController {
|
||||
await checkRateLimit(`${scope}:${userUid}`, limit, window);
|
||||
}
|
||||
|
||||
/** An `otp-login` JWT completes one sign-in; a second redemption is refused. */
|
||||
/**
|
||||
* An `otp-login` JWT completes one sign-in; a second redemption is refused.
|
||||
* Fails closed: while the store can't say whether the token was used, the
|
||||
* sign-in is refused rather than letting a captured token replay.
|
||||
*/
|
||||
async #consumeOtpLoginToken(token: string): Promise<void> {
|
||||
const digest = crypto.createHash('sha256').update(token).digest('hex');
|
||||
const firstUse = await checkRateLimit(
|
||||
`login-otp-token-used:${digest}`,
|
||||
1,
|
||||
OTP_LOGIN_TOKEN_USED_WINDOW_MS,
|
||||
undefined,
|
||||
{ failOpen: false },
|
||||
);
|
||||
if (!firstUse) {
|
||||
throw new HttpError(400, 'Invalid token.', {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"test": "vitest run --config vitest.config.js",
|
||||
"start:gui": "nodemon --exec \"node dev-server.js\" ",
|
||||
"build": "node ./build.js",
|
||||
"check-translations": "node tools/check-translations.js",
|
||||
"start-webpack": "webpack --watch --devtool source-map --stats=errors-only"
|
||||
},
|
||||
"workspaces": [
|
||||
|
||||
Reference in new issue
Block a user