element-web/src/PasswordReset.ts

109 lines
3.9 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
Copyright 2015, 2016 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { createClient, IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
import { _t } from "./languageHandler";
/**
* Allows a user to reset their password on a homeserver.
*
* This involves getting an email token from the identity server to "prove" that
* the client owns the given email address, which is then passed to the password
* API on the homeserver in question with the new password.
*/
export default class PasswordReset {
private client: MatrixClient;
private clientSecret: string;
private password = "";
private sessionId = "";
private logoutDevices = false;
private sendAttempt = 0;
/**
* Configure the endpoints for password resetting.
* @param {string} homeserverUrl The URL to the HS which has the account to reset.
* @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
*/
public constructor(homeserverUrl: string, identityUrl: string) {
this.client = createClient({
baseUrl: homeserverUrl,
idBaseUrl: identityUrl,
});
this.clientSecret = this.client.generateClientSecret();
}
/**
* Request a password reset token.
* This will trigger a side-effect of sending an email to the provided email address.
*/
public requestResetToken(emailAddress: string): Promise<IRequestTokenResponse> {
this.sendAttempt++;
return this.client.requestPasswordEmailToken(emailAddress, this.clientSecret, this.sendAttempt).then(
(res) => {
this.sessionId = res.sid;
return res;
},
function (err) {
if (err.errcode === "M_THREEPID_NOT_FOUND") {
err.message = _t("auth|reset_password_email_not_found_title");
} else if (err.httpStatus) {
err.message = err.message + ` (Status ${err.httpStatus})`;
}
throw err;
},
);
}
public setLogoutDevices(logoutDevices: boolean): void {
this.logoutDevices = logoutDevices;
}
public async setNewPassword(password: string): Promise<void> {
this.password = password;
await this.checkEmailLinkClicked();
}
/**
* Checks if the email link has been clicked by attempting to change the password
* for the mxid linked to the email.
* @return {Promise} Resolves if the password was reset. Rejects with an object
* with a "message" property which contains a human-readable message detailing why
* the reset failed, e.g. "There is no mapped matrix user ID for the given email address".
*/
public async checkEmailLinkClicked(): Promise<void> {
const creds = {
sid: this.sessionId,
client_secret: this.clientSecret,
};
try {
await this.client.setPassword(
{
// Note: Though this sounds like a login type for identity servers only, it
// has a dual purpose of being used for homeservers too.
type: "m.login.email.identity",
threepid_creds: creds,
},
this.password,
this.logoutDevices,
);
} catch (err: any) {
if (err.httpStatus === 401) {
err.message = _t("settings|general|add_email_failed_verification");
} else if (err.httpStatus === 404) {
err.message = _t("auth|reset_password_email_not_associated");
} else if (err.httpStatus) {
err.message += ` (Status ${err.httpStatus})`;
}
throw err;
}
}
}