Files
TREK/server/tests/unit/db/email-self-asserted-migration.test.ts
T
Maurice 3d7e8be9aa fix(oidc): a sign-in links to an account by email only when the address was not set by the account itself
An SSO login that matched no account by its subject took the account with the
same email address and linked the provider identity to it. That address proves
nothing when the account typed it in itself: anyone could sign up with, or
switch their profile to, a colleague's address and receive the colleague's
first SSO login. The provider switch branch (#2110) relinked a row on any email
match without even asking for email_verified, and also for another subject of
the same provider.

users.email_self_asserted remembers an address the account set itself: signup
(not the first account) and an email change in the profile settings set it, an
admin entering a new address clears it, and accounts the provider creates start
without it. Existing accounts keep linking, except those whose address moved
away from the registered one without an admin editing it. A login by email now
needs a verified address and an unmarked account, and is refused for another
subject of the same issuer, with email_link_blocked and a translated message on
the login screen. An email change in the profile settings writes a
user.email_change audit row.

Refs #2110
2026-10-12 17:06:52 +02:00

95 lines
3.6 KiB
TypeScript

/**
* `Migration20200101042800_an_email_the_account_typed_in_itself`: every
* existing account starts out trusted (0), except one whose address provably
* moved away from the one it registered with while no admin ever edited its
* email. A broken audit row is skipped, and a second run changes nothing.
*/
import { Migration20200101042800_an_email_the_account_typed_in_itself } from '../../../src/db/migrations/Migration20200101042800_an_email_the_account_typed_in_itself';
import {
createMigrationOrm,
migrateTo,
pendingNames,
rawExec,
rawQuery,
runMigrationDirect,
} from '../../helpers/migration-step';
import type { MikroORM } from '@mikro-orm/sqlite';
import { describe, expect, it } from 'vitest';
const TARGET = 'Migration20200101042800_an_email_the_account_typed_in_itself';
async function ormBeforeTarget(): Promise<MikroORM> {
const orm = await createMigrationOrm();
const names = await pendingNames(orm);
const idx = names.indexOf(TARGET);
expect(idx).toBeGreaterThan(0);
await migrateTo(orm, names[idx - 1]);
return orm;
}
async function seed(orm: MikroORM): Promise<void> {
// 1 registered and kept the address; 2 registered and changed it itself;
// 3 registered, changed it, and an admin set an email later; 4 was made by
// an admin (no register row); 5 registered in another case only; 6 has a
// broken register row.
await rawExec(
orm,
`INSERT INTO users (id, username, email, password_hash) VALUES
(1, 'kept', 'kept@test', 'x'),
(2, 'moved', 'colleague@test', 'x'),
(3, 'fixed', 'fixed-by-admin@test', 'x'),
(4, 'made', 'made@test', 'x'),
(5, 'cased', 'Cased@Test', 'x'),
(6, 'broken', 'broken-now@test', 'x')`,
);
await rawExec(
orm,
`INSERT INTO audit_log (user_id, action, resource, details) VALUES
(1, 'user.register', NULL, '{"username":"kept","email":"kept@test","role":"user"}'),
(2, 'user.register', NULL, '{"username":"moved","email":"moved@test","role":"user"}'),
(3, 'user.register', NULL, '{"username":"fixed","email":"fixed@test","role":"user"}'),
(4, 'admin.user_update', '3', '{"targetUser":"fixed-self@test","fields":["username","email","role"]}'),
(4, 'admin.user_update', '2', '{"targetUser":"colleague@test","fields":["role"]}'),
(5, 'user.register', NULL, '{"username":"cased","email":"cased@test","role":"user"}'),
(6, 'user.register', NULL, 'not json')`,
);
}
const flags = (orm: MikroORM) =>
rawQuery<{ id: number; email_self_asserted: number }>(orm, 'SELECT id, email_self_asserted FROM users ORDER BY id');
const EXPECTED = [
{ id: 1, email_self_asserted: 0 },
{ id: 2, email_self_asserted: 1 },
{ id: 3, email_self_asserted: 0 },
{ id: 4, email_self_asserted: 0 },
{ id: 5, email_self_asserted: 0 },
{ id: 6, email_self_asserted: 0 },
];
describe('email self-asserted migration', () => {
it('EMAILSELF-MIG-001: only an address the account provably changed itself is marked', async () => {
const orm = await ormBeforeTarget();
try {
await seed(orm);
await migrateTo(orm, TARGET);
expect(await flags(orm)).toEqual(EXPECTED);
} finally {
await orm.close(true);
}
}, 30000);
it('EMAILSELF-MIG-002: running it again changes nothing', async () => {
const orm = await ormBeforeTarget();
try {
await seed(orm);
await migrateTo(orm, TARGET);
await runMigrationDirect(orm, Migration20200101042800_an_email_the_account_typed_in_itself);
expect(await flags(orm)).toEqual(EXPECTED);
} finally {
await orm.close(true);
}
}, 30000);
});