Lower account password minimum to eight characters
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user