Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Force verification even for refreshed clients #44

Merged
merged 25 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b1aaae7
Force verification even for refreshed cients
dbkr Sep 13, 2024
2b10580
Hopefully make matrixchat tests pass?
dbkr Sep 20, 2024
91207a9
Merge branch 'develop' into dbkr/really_force_verification
dbkr Sep 20, 2024
255c7dc
Manual yarn.lock manipulation
dbkr Sep 20, 2024
e986562
Make final test pass(?)
dbkr Sep 23, 2024
2cb9f8f
Merge branch 'develop' into dbkr/really_force_verification
dbkr Sep 23, 2024
80c75d4
another waitFor
dbkr Sep 24, 2024
b07203e
death to flushPromises
dbkr Sep 24, 2024
f8c7db4
Put the logged in dispatch back
dbkr Sep 24, 2024
bc88a9a
Try displaying the screen in onClientStarted instead
dbkr Sep 24, 2024
bd2ca31
Put post login screen back in logged in
dbkr Sep 25, 2024
1471862
Rejig more in the hope it does the right thing
dbkr Sep 25, 2024
e88d1e0
Make hook work before push rules are fetched
dbkr Sep 26, 2024
579d507
Add test for unskippable verification
dbkr Sep 26, 2024
60b8eaf
Add test for use case selection
dbkr Sep 27, 2024
1e2525b
Fix test
dbkr Sep 27, 2024
860dedd
Add playwright test for unskippable verification
dbkr Sep 27, 2024
ee7a3e8
Remove console log
dbkr Sep 30, 2024
c2c3e4a
Add log message to log line
dbkr Oct 2, 2024
71249a3
Add tsdoc
dbkr Oct 2, 2024
1b04bf6
Use useTypedEventEmitter
dbkr Oct 2, 2024
677f616
Remove commented code
dbkr Oct 2, 2024
4a84e3d
Use catch instead of empty then on unawaited promises
dbkr Oct 2, 2024
707c8c7
Merge branch 'develop' into dbkr/really_force_verification
dbkr Oct 2, 2024
3e11360
Add new mock
dbkr Oct 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
"axe-core": "4.10.0",
"babel-jest": "^29.0.0",
"blob-polyfill": "^9.0.0",
"core-js": "^3.38.1",
dbkr marked this conversation as resolved.
Show resolved Hide resolved
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
Expand Down
165 changes: 155 additions & 10 deletions playwright/e2e/login/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,85 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import { Page } from "playwright-core";

import { expect, test } from "../../element-web-test";
import { doTokenRegistration } from "./utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { selectHomeserver } from "../utils";
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";

const username = "user1234";
const password = "p4s5W0rD";

// Pre-generated dummy signing keys to create an account that has signing keys set.
// Note the signatures are specific to the username and must be valid or the HS will reject the keys.
const DEVICE_SIGNING_KEYS_BODY = {
master_key: {
keys: {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg": "6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg",
},
signatures: {
"@user1234:localhost": {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
"mvwqsYiGa2gPH6ueJsiJnceHMrZhf1pqIMGxkvKisN3ucz8sU7LwyzndbYaLkUKEDx1JuOKFfZ9Mb3mqc7PMBQ",
"ed25519:SRHVWTNVBH":
"HVGmVIzsJe3d+Un/6S9tXPsU7YA8HjZPdxogVzdjEFIU8OjLyElccvjupow0rVWgkEqU8sO21LIHw9cWRZEmDw",
},
},
usage: ["master"],
user_id: "@user1234:localhost",
},
self_signing_key: {
keys: {
"ed25519:eqzRly4S1GvTA36v48hOKokHMtYBLm02zXRgPHue5/8": "eqzRly4S1GvTA36v48hOKokHMtYBLm02zXRgPHue5/8",
},
signatures: {
"@user1234:localhost": {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
"M2rt5xs+23egbVUwUcZuU7pMpn0chBNC5rpdyZGayfU3FDlx1DbopbakIcl5v4uOSGMbqUotyzkE6CchB+dgDw",
},
},
usage: ["self_signing"],
user_id: "@user1234:localhost",
},
user_signing_key: {
keys: {
"ed25519:h6C7sonjKSSa/VMvmpmFnwMA02H2rKIMSYZ2ddwgJn4": "h6C7sonjKSSa/VMvmpmFnwMA02H2rKIMSYZ2ddwgJn4",
},
signatures: {
"@user1234:localhost": {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
"5ZMJ7SG2qr76vU2nITKap88AxLZ/RZQmF/mBcAcVZ9Bknvos3WQp8qN9jKuiqOHCq/XpPORA6XBmiDIyPqTFAA",
},
},
usage: ["user_signing"],
user_id: "@user1234:localhost",
},
auth: {
type: "m.login.password",
identifier: { type: "m.id.user", user: "@user1234:localhost" },
password: password,
},
};

async function login(page: Page, homeserver: HomeserverInstance) {
await page.getByRole("link", { name: "Sign in" }).click();
await selectHomeserver(page, homeserver.config.baseUrl);

await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Sign in" }).click();
}

test.describe("Login", () => {
test.describe("Password login", () => {
test.use({ startHomeserverOpts: "consent" });

const username = "user1234";
const password = "p4s5W0rD";
let creds: Credentials;

test.beforeEach(async ({ homeserver }) => {
await homeserver.registerUser(username, password);
creds = await homeserver.registerUser(username, password);
});

test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
Expand Down Expand Up @@ -65,17 +130,97 @@ test.describe("Login", () => {

test("Follows the original link after login", async ({ page, homeserver }) => {
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
await page.getByRole("link", { name: "Sign in" }).click();

await selectHomeserver(page, homeserver.config.baseUrl);

await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Sign in" }).click();
await login(page, homeserver);

await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
});

test.describe("verification after login", () => {
test("Shows verification prompt after login if signing keys are set up, skippable by default", async ({
page,
homeserver,
request,
}) => {
const res = await request.post(
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);

await page.goto("/");
await login(page, homeserver);

await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();

await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
});

test.describe("with force_verification off", () => {
test.use({
config: {
force_verification: false,
},
});

test("Shows skippable verification prompt after login if signing keys are set up", async ({
page,
homeserver,
request,
}) => {
const res = await request.post(
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);

await page.goto("/");
await login(page, homeserver);

await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();

await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
});
});

test.describe("with force_verification on", () => {
test.use({
config: {
force_verification: true,
},
});

test("Shows unskippable verification prompt after login if signing keys are set up", async ({
page,
homeserver,
request,
}) => {
console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY);
const res = await request.post(
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);

await page.goto("/");
await login(page, homeserver);

const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
await expect(h1).toBeVisible();

expect(h1.locator(".mx_CompleteSecurity_skip")).not.toBeVisible();
});
});
});
});

// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
Expand Down
8 changes: 8 additions & 0 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,8 @@ async function doSetLoggedIn(
}
checkSessionLock();

// We are now logged in, so fire this. We have yet to start the client but the
// client_started dispatch is for that.
dis.fire(Action.OnLoggedIn);

const clientPegOpts: MatrixClientPegAssignOpts = {};
Expand All @@ -846,6 +848,12 @@ async function doSetLoggedIn(
// Run the migrations after the MatrixClientPeg has been assigned
SettingsStore.runMigrations(isFreshLogin);

if (isFreshLogin && !credentials.guest) {
// For newly registered users, set a flag so that we force them to verify,
// (we don't want to force users with existing sessions to verify though)
localStorage.setItem("must_verify_device", "true");
}

return client;
}

Expand Down
1 change: 1 addition & 0 deletions src/SdkConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
integrations_rest_url: "https://scalar.vector.im/api",
uisi_autorageshake_app: "element-auto-uisi",
show_labs_settings: false,
force_verification: false,

jitsi: {
preferred_domain: "meet.element.io",
Expand Down
Loading
Loading