Add tests for authentication and security features

- Implement unit tests for password handling in `auth_test.ts`, covering bcrypt and legacy password hashes.
- Create a fake database for user management to facilitate testing of the `AdminHandler`.
- Validate JWT-based identity verification against database records.
- Introduce tests for credential encryption and registry management in `security_test.ts`.
- Ensure registry passwords are securely stored and can be decrypted correctly, including legacy support.
- Add utility functions for password hashing and verification in `auth.ts`.
This commit is contained in:
2026-04-19 01:30:54 +00:00
parent 0c9eb0653d
commit 618d4d674f
34 changed files with 585 additions and 255 deletions
+105
View File
@@ -0,0 +1,105 @@
import { assert, assertEquals, fail } from '@std/assert';
import * as plugins from '../ts/plugins.ts';
import type { IUser as IDatabaseUser } from '../ts/types.ts';
import { AdminHandler } from '../ts/opsserver/handlers/admin.handler.ts';
import {
hashPassword,
isBcryptHash,
needsPasswordUpgrade,
verifyPassword,
} from '../ts/utils/auth.ts';
class FakeDatabase {
constructor(private users: Map<string, IDatabaseUser>) {}
getUserByUsername(username: string): IDatabaseUser | null {
return this.users.get(username) ?? null;
}
updateUserPassword(username: string, passwordHash: string): void {
const user = this.users.get(username);
if (!user) {
return;
}
this.users.set(username, {
...user,
passwordHash,
updatedAt: Date.now(),
});
}
}
async function createAdminHandler(users: IDatabaseUser[]): Promise<AdminHandler> {
const userMap = new Map(users.map((user) => [user.username, user]));
const fakeOpsServer = {
typedrouter: new plugins.typedrequest.TypedRouter(),
oneboxRef: {
database: new FakeDatabase(userMap),
},
};
const adminHandler = new AdminHandler(fakeOpsServer as any);
await adminHandler.initialize();
return adminHandler;
}
Deno.test('password helpers support bcrypt and legacy password hashes', async () => {
const password = 'correct horse battery staple';
const bcryptHash = await hashPassword(password);
assert(isBcryptHash(bcryptHash));
assert(await verifyPassword(password, bcryptHash));
assert(!(await verifyPassword('wrong password', bcryptHash)));
assert(!needsPasswordUpgrade(bcryptHash));
const legacyHash = btoa(password);
assert(await verifyPassword(password, legacyHash));
assert(needsPasswordUpgrade(legacyHash));
});
Deno.test('verified identity is derived from the signed JWT and database, not client fields', async () => {
const adminHandler = await createAdminHandler([
{
id: 1,
username: 'alice',
passwordHash: await hashPassword('password123'),
role: 'user',
createdAt: Date.now(),
updatedAt: Date.now(),
},
]);
const expiresAt = Date.now() + 60_000;
const jwt = await adminHandler.smartjwtInstance.createJWT({
userId: '1',
username: 'alice',
role: 'user',
status: 'loggedIn',
expiresAt,
});
const verifiedIdentity = await adminHandler.getVerifiedIdentity({
jwt,
userId: '999',
username: 'mallory',
role: 'admin',
expiresAt: 0,
});
assertEquals(verifiedIdentity.userId, '1');
assertEquals(verifiedIdentity.username, 'alice');
assertEquals(verifiedIdentity.role, 'user');
assertEquals(verifiedIdentity.expiresAt, expiresAt);
let rejected = false;
try {
await adminHandler.getVerifiedAdminIdentity(verifiedIdentity);
fail('Expected admin-only identity verification to reject non-admin users');
} catch {
rejected = true;
}
assert(rejected);
});
+61
View File
@@ -0,0 +1,61 @@
import { assert, assertEquals } from '@std/assert';
import type { IRegistry } from '../ts/types.ts';
import { credentialEncryption } from '../ts/classes/encryption.ts';
import { OneboxRegistriesManager } from '../ts/classes/registries.ts';
class FakeRegistryDatabase {
private registries = new Map<string, IRegistry>();
getRegistryByURL(url: string): IRegistry | null {
return this.registries.get(url) ?? null;
}
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
const savedRegistry: IRegistry = {
id: this.registries.size + 1,
...registry,
};
this.registries.set(savedRegistry.url, savedRegistry);
return savedRegistry;
}
deleteRegistry(url: string): void {
this.registries.delete(url);
}
getAllRegistries(): IRegistry[] {
return Array.from(this.registries.values());
}
}
Deno.test('credential encryption lazily initializes and roundtrips payloads', async () => {
const encrypted = await credentialEncryption.encrypt({ password: 'super-secret' });
const decrypted = await credentialEncryption.decrypt<{ password: string }>(encrypted);
assert(encrypted.length > 0);
assertEquals(decrypted.password, 'super-secret');
});
Deno.test('registry passwords use encrypted storage with legacy decode fallback', async () => {
const fakeDatabase = new FakeRegistryDatabase();
const registriesManager = new OneboxRegistriesManager({ database: fakeDatabase } as any);
(registriesManager as any).loginToRegistry = async () => {};
const registry = await registriesManager.addRegistry(
'registry.example.com',
'ci-user',
'correct horse battery staple',
);
assert(registry.passwordEncrypted.startsWith('enc:v1:'));
assertEquals(
await (registriesManager as any).decryptPassword(registry.passwordEncrypted),
'correct horse battery staple',
);
assertEquals(
await (registriesManager as any).decryptPassword(btoa('legacy-password')),
'legacy-password',
);
});