Lower account password minimum to eight characters

This commit is contained in:
2026-10-04 09:29:55 -04:00
parent e5c0fda1ed
commit 95cf7dd36f
7 changed files with 18 additions and 17 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ Accounts are disabled rather than deleted so history retains their identity.
## Passwords, sessions and recovery
Passwords are 15–128 characters and stored using salted scrypt hashes. Temporary
Passwords are 8–128 characters and stored using salted scrypt hashes. Temporary
passwords created or reset by administrators must be changed before any fleet
access. Password changes require the current password.
+1 -1
View File
@@ -35,7 +35,7 @@ try {
database.pragma('busy_timeout=5000');
const user = database.prepare("SELECT id FROM auth_users WHERE username=? AND role='admin' AND disabled=0").get(username);
if (!user) throw new Error('An enabled administrator with that username was not found.');
const password = await secret('New administrator password (15–128 characters): ');
const password = await secret('New administrator password (8–128 characters): ');
const confirmation = await secret('Repeat password: ');
if (password !== confirmation) throw new Error('Passwords did not match.');
const passwordHash = await hashPassword(password);
+2 -2
View File
@@ -7,8 +7,8 @@ let active = 0;
/** @param {unknown} password */
export function validatePassword(password) {
if (typeof password !== 'string' || [...password].length < 15 || [...password].length > 128) {
throw new Error('Use a password or passphrase between 15 and 128 characters.');
if (typeof password !== 'string' || [...password].length < 8 || [...password].length > 128) {
throw new Error('Use a password or passphrase between 8 and 128 characters.');
}
return password;
}
+3 -3
View File
@@ -40,9 +40,9 @@
<form onsubmit={changePassword} aria-busy={busy}><fieldset disabled={busy || signingOut}>
<input type="text" class="password-username" value={user.username} autocomplete="username" aria-label="Username" readonly tabindex="-1" />
<label for="current-password">{user.mustChangePassword ? 'Temporary password' : 'Current password'}<input id="current-password" name="currentPassword" type="password" autocomplete="current-password" required maxlength="128" bind:value={currentPassword} /></label>
<label for="new-password">New password<input id="new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="15" maxlength="128" bind:value={newPassword} aria-describedby="new-password-help" /></label>
<p id="new-password-help" class="hint">Use 15–128 characters and a password you do not use elsewhere.</p>
<label for="confirm-password">Confirm new password<input id="confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="15" maxlength="128" bind:value={confirmPassword} /></label>
<label for="new-password">New password<input id="new-password" name="newPassword" type="password" autocomplete="new-password" required minlength="8" maxlength="128" bind:value={newPassword} aria-describedby="new-password-help" /></label>
<p id="new-password-help" class="hint">Use 8–128 characters and a password you do not use elsewhere.</p>
<label for="confirm-password">Confirm new password<input id="confirm-password" name="confirmPassword" type="password" autocomplete="new-password" required minlength="8" maxlength="128" bind:value={confirmPassword} /></label>
<div><Button type="submit" variant="primary" loading={busy} disabled={busy || signingOut}>{user.mustChangePassword ? 'Save password and continue' : 'Change password'}</Button></div>
</fieldset></form>
</section>
+3 -3
View File
@@ -123,8 +123,8 @@
<p class="hint" id="new-username-help">3–64 characters: letters, numbers, periods, underscores, and hyphens. Start with a letter or number.</p>
<label for="new-name">Display name<input id="new-name" name="displayName" required maxlength="100" autocomplete="off" bind:value={displayName} /></label>
<label for="new-role">Role<select id="new-role" name="role" bind:value={role} aria-describedby="new-role-help">{#each roles as item}<option value={item.value}>{item.label}</option>{/each}</select></label><p class="hint" id="new-role-help">{roleDescription}</p>
<label for="temporary-password">Temporary password<input id="temporary-password" name="password" type={showPassword ? 'text' : 'password'} required minlength="15" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="temporary-help" /></label>
<p class="hint" id="temporary-help">15–128 characters. Save it before adding the user; it will not be displayed again.</p>
<label for="temporary-password">Temporary password<input id="temporary-password" name="password" type={showPassword ? 'text' : 'password'} required minlength="8" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="temporary-help" /></label>
<p class="hint" id="temporary-help">8–128 characters. Save it before adding the user; it will not be displayed again.</p>
<label class="checkbox"><input type="checkbox" bind:checked={showPassword} />Show temporary password</label>
<div class="form-actions"><Button disabled={busy} variant="ghost" onclick={() => { createOpen = false; password = ''; }}>Cancel</Button><Button type="submit" variant="primary" loading={busy} disabled={busy}>Add user</Button></div>
</fieldset></form>
@@ -137,7 +137,7 @@
<label for="edit-role">Role<select id="edit-role" name="role" bind:value={role} aria-describedby="edit-role-help">{#each roles as item}<option value={item.value}>{item.label}</option>{/each}</select></label><p class="hint" id="edit-role-help">{roleDescription}</p>
<label class="checkbox"><input type="checkbox" bind:checked={enabled} />Account enabled</label><p class="hint">Disabled users cannot sign in. Running game servers continue operating.</p>
{#if editing?.id === $page.data.user?.id}<p class="hint">Use <a href="/account">My account</a> to change your own password. Changes to your access can sign you out.</p>
{:else}<label for="reset-password">New temporary password <span class="optional">(optional)</span><input id="reset-password" name="password" type={showPassword ? 'text' : 'password'} minlength="15" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="reset-help" /></label><p class="hint" id="reset-help">Leave blank to keep the current password. A reset requires the user to choose a new password at their next sign-in.</p><label class="checkbox"><input type="checkbox" bind:checked={showPassword} />Show temporary password</label>{/if}
{:else}<label for="reset-password">New temporary password <span class="optional">(optional)</span><input id="reset-password" name="password" type={showPassword ? 'text' : 'password'} minlength="8" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="reset-help" /></label><p class="hint" id="reset-help">Leave blank to keep the current password. A reset requires the user to choose a new password at their next sign-in.</p><label class="checkbox"><input type="checkbox" bind:checked={showPassword} />Show temporary password</label>{/if}
<div class="form-actions"><Button disabled={busy} variant="ghost" onclick={() => { editOpen = false; password = ''; }}>Cancel</Button><Button type="submit" variant="primary" loading={busy} disabled={busy}>Save changes</Button></div>
</fieldset></form>
</Dialog>
+3 -3
View File
@@ -27,9 +27,9 @@
<label for="setup-username">Username<input id="setup-username" name="username" required minlength="3" maxlength="64" pattern={'[a-zA-Z0-9][a-zA-Z0-9_.\\-]{2,63}'} autocomplete="username" autocapitalize="none" spellcheck="false" bind:value={username} aria-describedby="username-help" /></label>
<p id="username-help" class="hint">3–64 characters. Start with a letter or number; periods, underscores, and hyphens are allowed.</p>
<label for="setup-name">Display name<input id="setup-name" name="displayName" required maxlength="100" autocomplete="name" bind:value={displayName} /></label>
<label for="setup-password">Password<input id="setup-password" name="password" type="password" required minlength="15" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="password-help" /></label>
<p id="password-help" class="hint">Use 15–128 characters. A long, unique passphrase works well.</p>
<label for="setup-confirm">Confirm password<input id="setup-confirm" name="confirmPassword" type="password" required minlength="15" maxlength="128" autocomplete="new-password" bind:value={confirmPassword} /></label>
<label for="setup-password">Password<input id="setup-password" name="password" type="password" required minlength="8" maxlength="128" autocomplete="new-password" bind:value={password} aria-describedby="password-help" /></label>
<p id="password-help" class="hint">Use 8–128 characters. A long, unique passphrase works well.</p>
<label for="setup-confirm">Confirm password<input id="setup-confirm" name="confirmPassword" type="password" required minlength="8" maxlength="128" autocomplete="new-password" bind:value={confirmPassword} /></label>
<Button type="submit" variant="primary" disabled={busy} loading={busy}>Create administrator</Button>
</fieldset></form>
<a class="login-link" href="/login">Already have an account? Sign in</a>
+5 -4
View File
@@ -7,7 +7,7 @@ vi.setConfig({ testTimeout: 20_000, hookTimeout: 20_000 });
import { newFixture, type Fixture } from '../helpers/fixture.js';
import type { UserRole } from '../../src/lib/auth.js';
const password = 'correct horse battery staple';
const password = 'testpass';
const replacement = 'another long secret passphrase';
const setupToken = 'ec_setup_test_0123456789abcdef0123456789abcdef';
let passwordHash: string;
@@ -106,10 +106,11 @@ describe('password storage and verification', () => {
it('bounds password length by Unicode characters and rejects invalid inputs before hashing', async () => {
const { validatePassword, hashPassword, verifyPassword } = await import('../../src/lib/server/auth/passwords.js');
for (const value of [undefined, null, 123, '', 'short password', 'a'.repeat(129)]) {
await expect(hashPassword(value)).rejects.toThrow('15 and 128');
for (const value of [undefined, null, 123, '', 'a'.repeat(7), '🦊'.repeat(7), 'a'.repeat(129)]) {
await expect(hashPassword(value)).rejects.toThrow('8 and 128');
}
expect(validatePassword('🦊'.repeat(15))).toBe('🦊'.repeat(15));
expect(validatePassword('a'.repeat(8))).toBe('a'.repeat(8));
expect(validatePassword('🦊'.repeat(8))).toBe('🦊'.repeat(8));
expect(validatePassword('a'.repeat(128))).toHaveLength(128);
expect(await verifyPassword('a'.repeat(129), passwordHash)).toBe(false);
});