feat(security): add revoke all tokens button

With this, user can sign out all sessions if they think their account has bee compromised.
This commit is contained in:
Miodec 2023-08-31 13:19:58 +02:00
parent 0de54f889b
commit 87e882b94d
6 changed files with 126 additions and 1 deletions

View file

@ -877,3 +877,11 @@ export async function toggleBan(
banned: !user.banned,
});
}
export async function revokeAllTokens(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
await FirebaseAdmin().auth().revokeRefreshTokens(uid);
return new MonkeyResponse("All tokens revoked");
}

View file

@ -650,4 +650,14 @@ router.post(
asyncHandler(UserController.sendForgotPasswordEmail)
);
router.post(
"/revokeAllTokens",
RateLimit.userRevokeAllTokens,
authenticateRequest({
requireFreshToken: true,
noCache: true,
}),
asyncHandler(UserController.revokeAllTokens)
);
export default router;

View file

@ -483,6 +483,13 @@ export const userForgotPasswordEmail = rateLimit({
handler: customHandler,
});
export const userRevokeAllTokens = rateLimit({
windowMs: ONE_HOUR_MS,
max: 10 * REQUEST_MULTIPLIER,
keyGenerator: getKeyWithUid,
handler: customHandler,
});
export const userProfileGet = rateLimit({
windowMs: ONE_HOUR_MS,
max: 100 * REQUEST_MULTIPLIER,

View file

@ -264,4 +264,8 @@ export default class Users {
payload: { hourOffset },
});
}
async revokeAllTokens(): Ape.EndpointResponse {
return await this.httpClient.post(`${BASE_PATH}/revokeAllTokens`);
}
}

View file

@ -991,6 +991,73 @@ list["resetSettings"] = new SimplePopup(
}
);
list["revokeAllTokens"] = new SimplePopup(
"revokeAllTokens",
"text",
"Revoke All Tokens",
[
{
placeholder: "Password",
type: "password",
initVal: "",
},
],
"Are you sure you want to this? This will log you out of all devices.",
"revoke all",
async (_thisPopup, password) => {
try {
const user = Auth?.currentUser;
const snapshot = DB.getSnapshot();
if (!user || !snapshot) return;
if (user.providerData.find((p) => p?.providerId === "password")) {
const credential = EmailAuthProvider.credential(
user.email as string,
password
);
await reauthenticateWithCredential(user, credential);
} else {
await reauthenticateWithPopup(user, AccountController.gmailProvider);
}
Loader.show();
const response = await Ape.users.revokeAllTokens();
Loader.hide();
if (response.status !== 200) {
return Notifications.add(
"Failed to revoke all tokens: " + response.message,
-1
);
}
Notifications.add("All tokens revoked", 1);
setTimeout(() => {
location.reload();
}, 1000);
} catch (e) {
Loader.hide();
const typedError = e as FirebaseError;
if (typedError.code === "auth/wrong-password") {
Notifications.add("Incorrect password", -1);
} else {
Notifications.add("Something went wrong: " + e, -1);
}
}
},
(thisPopup) => {
const user = Auth?.currentUser;
const snapshot = DB.getSnapshot();
if (!user || !snapshot) return;
if (!user.providerData.find((p) => p?.providerId === "password")) {
thisPopup.inputs[0].hidden = true;
thisPopup.buttonText = "reauthenticate to revoke all tokens";
}
},
(_thisPopup) => {
//
}
);
list["unlinkDiscord"] = new SimplePopup(
"unlinkDiscord",
"text",
@ -1385,6 +1452,14 @@ $("#resetSettingsButton").on("click", () => {
list["resetSettings"].show();
});
$("#revokeAllTokens").on("click", () => {
if (!ConnectionState.get()) {
Notifications.add("You are offline", 0, { duration: 2 });
return;
}
list["revokeAllTokens"].show();
});
$(".pageSettings #resetPersonalBestsButton").on("click", () => {
if (!ConnectionState.get()) {
Notifications.add("You are offline", 0, { duration: 2 });

View file

@ -130,7 +130,6 @@
<div class="button addPresetButton"><i class="fas fa-plus"></i></div>
</div>
</div>
<div class="sectionSpacer"></div>
<div class="section">
<div class="groupTitle">
<i class="fas fa-user"></i>
@ -2869,6 +2868,28 @@
</div>
</div>
</div>
<div class="section revokeAllTokens">
<div class="groupTitle">
<i class="fas fa-user-slash"></i>
<span>revoke all tokens</span>
</div>
<div class="text">
Revokes all tokens connected to your account. Do this if you think
someone else has access to your account.
<br />
<span class="red">This will log you out of all devices.</span>
</div>
<div class="buttons">
<div
class="button danger"
id="revokeAllTokens"
tabindex="0"
onclick="this.blur();"
>
revoke all tokens
</div>
</div>
</div>
<div class="section resetSettings">
<div class="groupTitle">
<i class="fas fa-redo-alt"></i>