few bugfixes, fido/u2f still WIP

This commit is contained in:
2024-04-07 18:29:33 +02:00
parent 0974ac9260
commit 6c551b08d8
19 changed files with 164 additions and 67 deletions

View File

@@ -6,7 +6,7 @@
"react": "^18.2.0"
},
"scripts": {
"dev": "react-app-rewired start"
"dev": "HTTPS=true react-app-rewired start"
},
"author": "",
"license": "ISC",

View File

@@ -28,6 +28,14 @@ export default function App() {
});
}, [api]);
const onLogout = useCallback(() => {
api.logout().then(data => {
if (!data.success) {
setError("Error logging out: " + data.msg);
}
});
}, [api]);
const onInit = useCallback((force = false) => {
if (loaded && !force) {
return;
@@ -97,8 +105,8 @@ export default function App() {
} else {
return <b>{L("general.loading")} <Icon icon={"spinner"}/></b>
}
} else if (!user || !api.loggedIn) {
return <LoginForm api={api} info={info} onLogin={fetchUser} />
} else if (!user || !api.loggedIn || (api.user.twoFactorToken?.confirmed && !api.user.twoFactorToken.authenticated)) {
return <LoginForm api={api} info={info} onLogin={fetchUser} onLogout={onLogout} />
} else {
return <AdminDashboard api={api} info={info} />
}

View File

@@ -1,17 +1,76 @@
import {Box, Paper} from "@mui/material";
import {Box, CircularProgress, Paper} from "@mui/material";
import {LocaleContext} from "shared/locale";
import {useCallback, useContext} from "react";
import {decodeText, encodeText} from "shared/util";
export default function MfaFido(props) {
const {api, showDialog, setDialogData, ...other} = props;
const {api, showDialog, setDialogData, set2FA, ...other} = props;
const {translate: L} = useContext(LocaleContext);
const openDialog = useCallback(() => {
if (api.hasPermission("tfa/registerKey")) {
if (!api.hasPermission("tfa/registerKey")) {
return;
}
}, [api, showDialog]);
if (typeof navigator.credentials !== 'object' || typeof navigator.credentials.create !== 'function') {
showDialog(L("Key-based Two-Factor-Authentication (2FA) is not supported on this device."), L("Not supported"));
}
api.register2FA().then(res => {
if (!res.success) {
showDialog(res.msg, L("Error registering 2FA-Device"));
return;
}
setDialogData({
show: true,
title: L("Register a 2FA-Device"),
message: L("You may need to interact with your Device, e.g. typing in your PIN or touching to confirm the registration."),
inputs: [
{ type: "custom", key: "progress", element: CircularProgress }
],
options: [L("general.cancel")],
})
navigator.credentials.create({
publicKey: {
challenge: encodeText(window.atob(res.data.challenge)),
rp: res.data.relyingParty,
user: {
id: encodeText(res.data.id),
name: api.user.name,
displayName: api.user.fullName
},
userVerification: "discouraged",
attestation: "direct",
pubKeyCredParams: [{
type: "public-key",
alg: -7, // "ES256" IANA COSE Algorithms registry
}]
}
}).then(res => {
if (res.response) {
let clientDataJSON = decodeText(res.response.clientDataJSON);
let attestationObject = window.btoa(String.fromCharCode.apply(null, new Uint8Array(res.response.attestationObject)));
api.register2FA(clientDataJSON, attestationObject).then((res) => {
setDialogData({show: false});
if (res.success) {
showDialog(L("account.confirm_fido_success"), L("general.success"));
set2FA({ confirmed: true, type: "fido", authenticated: true });
} else {
showDialog(res.msg, L("Error registering 2FA-Device"));
}
});
} else {
showDialog(JSON.stringify(res), L("Error registering 2FA-Device"));
}
}).catch(ex => {
setDialogData({show: false});
showDialog(ex.toString(), L("Error registering 2FA-Device"));
});
});
}, [api, showDialog, setDialogData, set2FA]);
const disabledStyle = {
background: "gray",

View File

@@ -4,7 +4,7 @@ import {LocaleContext} from "shared/locale";
export default function MfaTotp(props) {
const {setDialogData, api, showDialog, ...other} = props;
const {setDialogData, api, showDialog, set2FA, ...other} = props;
const {translate: L} = useContext(LocaleContext);
const onConfirmTOTP = useCallback((code) => {
@@ -14,10 +14,11 @@ export default function MfaTotp(props) {
} else {
setDialogData({show: false});
showDialog(L("account.confirm_totp_success"), L("general.success"));
set2FA({ confirmed: true, type: "totp", authenticated: true });
}
});
return false;
}, [api, showDialog]);
}, [api, showDialog, set2FA, setDialogData]);
const openDialog = useCallback(() => {
if (api.hasPermission("tfa/generateQR")) {
@@ -28,8 +29,8 @@ export default function MfaTotp(props) {
"On Android, you can use the Google Authenticator."),
inputs: [
{
type: "custom", element: Box, textAlign: "center", children:
<img src={"/api/tfa/generateQR?nocache=" + Math.random()} alt={"[QR-Code]"}/>
type: "custom", element: Box, textAlign: "center", key: "qr-code",
children: <img src={"/api/tfa/generateQR?nocache=" + Math.random()} alt={"[QR-Code]"} />
},
{
type: "number", placeholder: L("account.6_digit_code"),
@@ -37,6 +38,7 @@ export default function MfaTotp(props) {
sx: { "& input": { textAlign: "center", fontFamily: "monospace" } },
}
],
options: [L("general.ok"), L("general.cancel")],
onOption: (option, data) => option === 0 ? onConfirmTOTP(data.code) : true
})
}

View File

@@ -388,8 +388,10 @@ export default function ProfileView(props) {
</Button>
</Box> :
<MFAOptions>
<MfaTotp api={api} showDialog={showDialog} setDialogData={setDialogData}/>
<MfaFido api={api} showDialog={showDialog} setDialogData={setDialogData}/>
<MfaTotp api={api} showDialog={showDialog} setDialogData={setDialogData}
set2FA={token => setProfile({...profile, twoFactorToken: token })} />
<MfaFido api={api} showDialog={showDialog} setDialogData={setDialogData}
set2FA={token => setProfile({...profile, twoFactorToken: token })} />
</MFAOptions>
}
</CollapseBox>
@@ -409,7 +411,7 @@ export default function ProfileView(props) {
message={dialogData.message}
inputs={dialogData.inputs}
onClose={() => setDialogData({show: false})}
options={[L("general.ok"), L("general.cancel")]}
options={dialogData.options}
onOption={dialogData.onOption} />
</>
}