Compare commits

...

6 commits

Author SHA1 Message Date
TrojanerHD
0a270ca31a
fix(frontend): show 2fa code field in login only if 2fa is enabled 2024-02-28 03:07:12 +09:00
TrojanerHD
437d02ef5d
fix(frontend): allow disabling of 2fa when security key is registered 2024-02-28 03:07:11 +09:00
TrojanerHD
2c78c01d57
feat(frontend): enable using security keys without 2fa 2024-02-28 03:06:32 +09:00
TrojanerHD
78af158c30
fix(backend): only check for 2fa if it was enabled
fix(backend): don't reject logins where 2fa is disabled and security keys are available
2024-02-28 03:06:32 +09:00
TrojanerHD
4687b21d79
fix(backend): update security keys available entry 2024-02-28 03:06:32 +09:00
TrojanerHD
7ceaa9c090
feat(backend): remove 2fa requirement for security keys 2024-02-28 03:06:29 +09:00
6 changed files with 25 additions and 62 deletions

View file

@ -47,10 +47,6 @@ export default define(meta, paramDef, async (ps, user) => {
throw new Error("incorrect password"); throw new Error("incorrect password");
} }
if (!profile.twoFactorEnabled) {
throw new Error("2fa not enabled");
}
const clientData = JSON.parse(ps.clientDataJSON); const clientData = JSON.parse(ps.clientDataJSON);
if (clientData.type !== "webauthn.create") { if (clientData.type !== "webauthn.create") {
@ -148,6 +144,8 @@ export default define(meta, paramDef, async (ps, user) => {
}), }),
); );
UserProfiles.update(user.id, { securityKeysAvailable: true });
return { return {
id: credentialIdString, id: credentialIdString,
name: ps.name, name: ps.name,

View file

@ -32,10 +32,6 @@ export default define(meta, paramDef, async (ps, user) => {
throw new Error("incorrect password"); throw new Error("incorrect password");
} }
// if (!profile.twoFactorEnabled) {
// throw new Error("2fa not enabled");
// }
// 32 byte challenge // 32 byte challenge
const entropy = await randomBytes(32); const entropy = await randomBytes(32);
const challenge = entropy const challenge = entropy

View file

@ -47,8 +47,9 @@ export default define(meta, paramDef, async (ps, user) => {
}); });
if (keyCount === 0) { if (keyCount === 0) {
await UserProfiles.update(me.id, { await UserProfiles.update(user.id, {
usePasswordLessLogin: false, usePasswordLessLogin: false,
securityKeysAvailable: false,
}); });
} }

View file

@ -116,7 +116,7 @@ export default async (ctx: Koa.Context) => {
); );
} }
if (!profile.twoFactorEnabled) { if (!profile.twoFactorEnabled && !profile.securityKeysAvailable) {
if (same) { if (same) {
signin(ctx, user); signin(ctx, user);
return; return;
@ -128,7 +128,7 @@ export default async (ctx: Koa.Context) => {
} }
} }
if (token) { if (token && profile.twoFactorEnabled) {
if (!same) { if (!same) {
await fail(403, { await fail(403, {
id: "932c904e-9460-45b7-9ce6-7ed33be7eb2c", id: "932c904e-9460-45b7-9ce6-7ed33be7eb2c",

View file

@ -79,10 +79,10 @@
{{ i18n.ts.retry }} {{ i18n.ts.retry }}
</MkButton> </MkButton>
</div> </div>
<div v-if="user && user.securityKeys" class="or-hr"> <div v-if="user && user.securityKeys && user.twoFactorEnabled" class="or-hr">
<p class="or-msg">{{ i18n.ts.or }}</p> <p class="or-msg">{{ i18n.ts.or }}</p>
</div> </div>
<div class="twofa-group totp-group"> <div v-if="user.twoFactorEnabled" class="twofa-group totp-group">
<p style="margin-bottom: 0"> <p style="margin-bottom: 0">
{{ i18n.ts.twoStepAuthentication }} {{ i18n.ts.twoStepAuthentication }}
</p> </p>
@ -247,25 +247,23 @@ function queryKey() {
function onSubmit() { function onSubmit() {
signing.value = true; signing.value = true;
console.log("submit"); console.log("submit");
if (!totpLogin.value && user.value && user.value.twoFactorEnabled) { if (window.PublicKeyCredential && user.value.securityKeys) {
if (window.PublicKeyCredential && user.value.securityKeys) { os.api("signin", {
os.api("signin", { username: username.value,
username: username.value, password: password.value,
password: password.value, "hcaptcha-response": hCaptchaResponse.value,
"hcaptcha-response": hCaptchaResponse.value, "g-recaptcha-response": reCaptchaResponse.value,
"g-recaptcha-response": reCaptchaResponse.value, })
.then((res) => {
totpLogin.value = true;
signing.value = false;
challengeData.value = res;
return queryKey();
}) })
.then((res) => { .catch(loginFailed);
totpLogin.value = true; } else if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
signing.value = false; totpLogin.value = true;
challengeData.value = res; signing.value = false;
return queryKey();
})
.catch(loginFailed);
} else {
totpLogin.value = true;
signing.value = false;
}
} else { } else {
os.api("signin", { os.api("signin", {
username: username.value, username: username.value,

View file

@ -14,17 +14,7 @@
<template #caption>{{ i18n.ts.totpDescription }}</template> <template #caption>{{ i18n.ts.totpDescription }}</template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s"> <div v-if="$i.twoFactorEnabled" class="_gaps_s">
<div v-text="i18n.ts._2fa.alreadyRegistered" /> <div v-text="i18n.ts._2fa.alreadyRegistered" />
<template v-if="$i.securityKeysList.length > 0"> <MkButton @click="unregisterTOTP"
<MkButton @click="renewTOTP"
><i
:class="icon('ph-shield-check')"
style="margin-inline-end: 0.5rem"
></i
>{{ i18n.ts._2fa.renewTOTP }}</MkButton
>
<MkInfo>{{ i18n.ts._2fa.whyTOTPOnlyRenew }}</MkInfo>
</template>
<MkButton v-else @click="unregisterTOTP"
><i ><i
:class="icon('ph-shield-slash')" :class="icon('ph-shield-slash')"
style="margin-inline-end: 0.5rem" style="margin-inline-end: 0.5rem"
@ -59,13 +49,6 @@
{{ i18n.ts._2fa.securityKeyNotSupported }} {{ i18n.ts._2fa.securityKeyNotSupported }}
</MkInfo> </MkInfo>
<MkInfo
v-else-if="supportsCredentials && !$i.twoFactorEnabled"
warn
>
{{ i18n.ts._2fa.registerTOTPBeforeKey }}
</MkInfo>
<template v-else> <template v-else>
<MkButton primary @click="addSecurityKey" <MkButton primary @click="addSecurityKey"
><i ><i
@ -205,19 +188,6 @@ function unregisterTOTP() {
}); });
} }
function renewTOTP() {
os.confirm({
type: "question",
title: i18n.ts._2fa.renewTOTP,
text: i18n.ts._2fa.renewTOTPConfirm,
okText: i18n.ts._2fa.renewTOTPOk,
cancelText: i18n.ts._2fa.renewTOTPCancel,
}).then(({ canceled }) => {
if (canceled) return;
registerTOTP();
});
}
async function unregisterKey(key) { async function unregisterKey(key) {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: "question", type: "question",