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

Allow user to control if they are signed out of all devices when changing password #8259

Merged
merged 24 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9825e44
Allow user to control if they are signed out of all sessions when res…
hughns Apr 8, 2022
ac4b135
Don't sign out all devices when changing password from Settings tab
hughns Apr 8, 2022
e8eed9d
Use camelcase variable name
hughns Apr 8, 2022
49b12b0
Add argument type
hughns Apr 8, 2022
564f0fd
session => device
hughns Apr 8, 2022
5bebf72
Wording changes based on feedback in PR
hughns Apr 8, 2022
f360a64
Merge branch 'hughns/logout-devices-control' of https://github.com/ma…
hughns Apr 8, 2022
13e95bf
Merge branch 'develop' into hughns/logout-devices-control
hughns Apr 13, 2022
68f9551
UI to respect if server has capability to control device logout
hughns Apr 13, 2022
981813d
Revert "Don't sign out all devices when changing password from Settin…
hughns Apr 13, 2022
e6415b2
If homeserver supports it then don't sign out all devices when changi…
hughns Apr 13, 2022
5c48520
Wording revisions
hughns Apr 13, 2022
fd36f66
Whitespace fix
hughns Apr 13, 2022
c4380c9
Remove trailing whitespace from translations
hughns Apr 13, 2022
e8626ed
Wording and whitespace
hughns Apr 13, 2022
bd3a12c
Add proper capability check based on support spec version
hughns Apr 13, 2022
27a7bf1
Merge branch 'develop' into hughns/logout-devices-control
hughns Apr 13, 2022
36c6fde
Update src/components/views/settings/ChangePassword.tsx
hughns Apr 13, 2022
20b3316
Update src/components/structures/auth/ForgotPassword.tsx
hughns Apr 13, 2022
cff80ac
Refactor to use Modal promises
hughns Apr 13, 2022
fe384cd
Take account of whether devices where signed out in password change c…
hughns Apr 19, 2022
f3260a0
Update translations
hughns Apr 19, 2022
853e0e9
Merge branch 'develop' into hughns/logout-devices-control
hughns Apr 21, 2022
9038ea7
Only warn user when changing password in Settings if they have other …
hughns Apr 21, 2022
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
11 changes: 9 additions & 2 deletions src/PasswordReset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default class PasswordReset {
private clientSecret: string;
private password: string;
private sessionId: string;
private logoutDevices: boolean;

/**
* Configure the endpoints for password resetting.
Expand All @@ -50,10 +51,16 @@ export default class PasswordReset {
* sending an email to the provided email address.
* @param {string} emailAddress The email address
* @param {string} newPassword The new password for the account.
* @param {boolean} logoutDevices Should all devices be signed out after the reset? Defaults to `true`.
* @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
*/
public resetPassword(emailAddress: string, newPassword: string): Promise<IRequestTokenResponse> {
public resetPassword(
emailAddress: string,
newPassword: string,
logoutDevices = true,
): Promise<IRequestTokenResponse> {
this.password = newPassword;
this.logoutDevices = logoutDevices;
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, 1).then((res) => {
this.sessionId = res.sid;
return res;
Expand Down Expand Up @@ -90,7 +97,7 @@ export default class PasswordReset {
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
}, this.password);
}, this.password, this.logoutDevices);
} catch (err) {
if (err.httpStatus === 401) {
err.message = _t('Failed to verify email address: make sure you clicked the link in the email');
Expand Down
92 changes: 67 additions & 25 deletions src/components/structures/auth/ForgotPassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import AuthHeader from "../../views/auth/AuthHeader";
import AuthBody from "../../views/auth/AuthBody";
import PassphraseConfirmField from "../../views/auth/PassphraseConfirmField";
import AccessibleButton from '../../views/elements/AccessibleButton';
import StyledCheckbox from '../../views/elements/StyledCheckbox';

enum Phase {
// Show the forgot password inputs
Expand Down Expand Up @@ -72,6 +73,9 @@ interface IState {
serverDeadError: string;

currentHttpRequest?: Promise<any>;

serverSupportsControlOfDevicesLogout: boolean;
logoutDevices: boolean;
}

enum ForgotPasswordField {
Expand All @@ -97,11 +101,14 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
serverIsAlive: true,
serverErrorIsFatal: false,
serverDeadError: "",
serverSupportsControlOfDevicesLogout: false,
logoutDevices: false,
};

public componentDidMount() {
this.reset = null;
this.checkServerLiveliness(this.props.serverConfig);
this.checkServerCapabilities(this.props.serverConfig);
}

// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
Expand All @@ -112,6 +119,9 @@ export default class ForgotPassword extends React.Component<IProps, IState> {

// Do a liveliness check on the new URLs
this.checkServerLiveliness(newProps.serverConfig);

// Do capabilities check on new URLs
this.checkServerCapabilities(newProps.serverConfig);
}

private async checkServerLiveliness(serverConfig): Promise<void> {
Expand All @@ -129,12 +139,21 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
}
}

public submitPasswordReset(email: string, password: string): void {
private checkServerCapabilities(serverConfig: ValidatedServerConfig) {
// TODO: proper capabilities check - this is just a placeholder
const serverSupportsControlOfDevicesLogout = serverConfig.hsUrl === 'https://matrix-client.matrix.org';
this.setState({
logoutDevices: !serverSupportsControlOfDevicesLogout,
serverSupportsControlOfDevicesLogout,
});
}

public submitPasswordReset(email: string, password: string, logoutDevices = true): void {
this.setState({
phase: Phase.SendingEmail,
});
this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl);
this.reset.resetPassword(email, password).then(() => {
this.reset.resetPassword(email, password, logoutDevices).then(() => {
this.setState({
phase: Phase.EmailSent,
});
Expand Down Expand Up @@ -174,24 +193,37 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
return;
}

Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
title: _t('Warning!'),
description:
<div>
{ _t(
"Changing your password will reset any end-to-end encryption keys " +
"on all of your sessions, making encrypted chat history unreadable. Set up " +
"Key Backup or export your room keys from another session before resetting your " +
"password.",
) }
</div>,
button: _t('Continue'),
onFinished: (confirmed) => {
if (confirmed) {
this.submitPasswordReset(this.state.email, this.state.password);
}
},
});
if (this.state.logoutDevices) {
Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, {
title: _t('Warning!'),
description:
<div>
{ !this.state.serverSupportsControlOfDevicesLogout ?
_t(
"Resetting your password on this homeserver will cause all of your devices to be " +
"signed out. This will delete the message encryption keys stored on them, " +
"making encrypted chat history unreadable.",
) :
_t(
"Signing out your devices will delete the message encryption keys stored on them, " +
"making encrypted chat history unreadable.",
)
}
<span> </span>
{ _t("If you want to access your encrypted chat history then set up Key Backup or export " +
"your message keys from one of your other devices before proceeding.",
) }
</div>,
button: _t('Continue'),
onFinished: (confirmed: boolean) => {
if (confirmed) {
this.submitPasswordReset(this.state.email, this.state.password, this.state.logoutDevices);
}
},
});
} else {
this.submitPasswordReset(this.state.email, this.state.password, this.state.logoutDevices);
}
};

private async verifyFieldsBeforeSubmit() {
Expand Down Expand Up @@ -314,6 +346,13 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
autoComplete="new-password"
/>
</div>
{ this.state.serverSupportsControlOfDevicesLogout ?
<div className="mx_AuthBody_fieldRow">
<StyledCheckbox onChange={() => this.setState({ logoutDevices: !this.state.logoutDevices })} checked={this.state.logoutDevices}>
{ _t("Sign out all devices") }
</StyledCheckbox>
</div> : null
}
<span>{ _t(
'A verification email will be sent to your inbox to confirm ' +
'setting your new password.',
Expand Down Expand Up @@ -353,11 +392,14 @@ export default class ForgotPassword extends React.Component<IProps, IState> {
renderDone() {
return <div>
<p>{ _t("Your password has been reset.") }</p>
<p>{ _t(
"You have been logged out of all sessions and will no longer receive " +
"push notifications. To re-enable notifications, sign in again on each " +
"device.",
) }</p>
{ this.state.logoutDevices ?
<p>{ _t(
"You have been logged out of all devices and will no longer receive " +
"push notifications. To re-enable notifications, sign in again on each " +
"device.",
) }</p>
: null
}
<input
className="mx_Login_submit"
type="button"
Expand Down
86 changes: 49 additions & 37 deletions src/components/views/settings/ChangePassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,45 +85,57 @@ export default class ChangePassword extends React.Component<IProps, IState> {
private onChangePassword(oldPassword: string, newPassword: string): void {
const cli = MatrixClientPeg.get();

if (!this.props.confirm) {
this.changePassword(cli, oldPassword, newPassword);
return;
}
// TODO: proper capabilities check - this is just a placeholder
const serverSupportsControlOfDevicesLogout = MatrixClientPeg.getHomeserverName() === 'matrix.org';

Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t(
'Changing password will currently reset any end-to-end encryption keys on all sessions, ' +
'making encrypted chat history unreadable, unless you first export your room keys ' +
'and re-import them afterwards. ' +
'In future this will be improved.',
) }
{ ' ' }
<a href="https://github.com/vector-im/element-web/issues/2671" target="_blank" rel="noreferrer noopener">
https://github.com/vector-im/element-web/issues/2671
</a>
</div>,
button: _t("Continue"),
extraButtons: [
<button
key="exportRoomKeys"
className="mx_Dialog_primary"
onClick={this.onExportE2eKeysClicked}
>
{ _t('Export E2E room keys') }
</button>,
],
onFinished: (confirmed) => {
if (confirmed) {
this.changePassword(cli, oldPassword, newPassword);
}
},
});
if (serverSupportsControlOfDevicesLogout) {
// don't log user out of all devices
this.changePassword(cli, oldPassword, newPassword, /* logoutDevices = */ false);
} else {
hughns marked this conversation as resolved.
Show resolved Hide resolved
if (!this.props.confirm) {
// TODO: should this change to be false rather than undefined? Who uses confirm=false?
this.changePassword(cli, oldPassword, newPassword, /* logoutDevices = */ undefined);
return;
}

// warn about logging out all devices
Modal.createTrackedDialog('Change Password', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t(
'Changing your password on this homeserver will cause all of your other devices to be ' +
'signed out. This will delete the message encryption keys stored on them, and may make ' +
'encrypted chat history unreadable, unless you first export your room keys and ' +
're-import them afterwards. Ask your homeserver admin to upgrade the server to change ' +
'this behaviour.',
) }
</div>,
button: _t("Continue"),
extraButtons: [
<button
key="exportRoomKeys"
className="mx_Dialog_primary"
onClick={this.onExportE2eKeysClicked}
>
{ _t('Export E2E room keys') }
</button>,
],
onFinished: (confirmed) => {
if (confirmed) {
this.changePassword(cli, oldPassword, newPassword, /* logoutDevices = */ undefined);
}
},
});
}
}

private changePassword(cli: MatrixClient, oldPassword: string, newPassword: string): void {
private changePassword(
cli: MatrixClient,
oldPassword: string,
newPassword: string,
logoutDevices: boolean | undefined,
): void {
const authDict = {
type: 'm.login.password',
identifier: {
Expand All @@ -140,7 +152,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
phase: Phase.Uploading,
});

cli.setPassword(authDict, newPassword).then(() => {
cli.setPassword(authDict, newPassword, logoutDevices).then(() => {
if (this.props.shouldAskForEmail) {
return this.optionallySetEmail().then((confirmed) => {
this.props.onFinished({
Expand Down
9 changes: 6 additions & 3 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,7 @@
"Upload new:": "Upload new:",
"No display name": "No display name",
"Warning!": "Warning!",
"Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.",
"Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. Ask your homeserver admin to upgrade the server to change this behaviour.": "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. Ask your homeserver admin to upgrade the server to change this behaviour.",
"Export E2E room keys": "Export E2E room keys",
"New passwords don't match": "New passwords don't match",
"Passwords can't be empty": "Passwords can't be empty",
Expand Down Expand Up @@ -3143,18 +3143,21 @@
"Really reset verification keys?": "Really reset verification keys?",
"Skip verification for now": "Skip verification for now",
"Failed to send email": "Failed to send email",
"Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your sessions, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another session before resetting your password.",
"Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
"Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.",
"If you want to access your encrypted chat history then set up Key Backup or export your message keys from one of your other devices before proceeding.": "If you want to access your encrypted chat history then set up Key Backup or export your message keys from one of your other devices before proceeding.",
hughns marked this conversation as resolved.
Show resolved Hide resolved
"The email address linked to your account must be entered.": "The email address linked to your account must be entered.",
"The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.",
"A new password must be entered.": "A new password must be entered.",
"New passwords must match each other.": "New passwords must match each other.",
"Sign out all devices": "Sign out all devices",
"A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.",
"Send Reset Email": "Send Reset Email",
"Sign in instead": "Sign in instead",
"An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.",
"I have verified my email address": "I have verified my email address",
"Your password has been reset.": "Your password has been reset.",
"You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.",
"You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.",
"Return to login screen": "Return to login screen",
"Set a new password": "Set a new password",
"Invalid homeserver discovery response": "Invalid homeserver discovery response",
Expand Down