Skip to content

Commit

Permalink
feat(security): add revoke all tokens button
Browse files Browse the repository at this point in the history
With this, user can sign out all sessions if they think their account has bee compromised.
  • Loading branch information
Miodec committed Aug 31, 2023
1 parent 0de54f8 commit 87e882b
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 1 deletion.
8 changes: 8 additions & 0 deletions backend/src/api/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
10 changes: 10 additions & 0 deletions backend/src/api/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
7 changes: 7 additions & 0 deletions backend/src/middlewares/rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/ts/ape/endpoints/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,8 @@ export default class Users {
payload: { hourOffset },
});
}

async revokeAllTokens(): Ape.EndpointResponse {
return await this.httpClient.post(`${BASE_PATH}/revokeAllTokens`);
}
}
75 changes: 75 additions & 0 deletions frontend/src/ts/popups/simple-popups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 });
Expand Down
23 changes: 22 additions & 1 deletion frontend/static/html/pages/settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 87e882b

Please sign in to comment.