few bugfixes, fido/u2f still WIP
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "react-app-rewired start"
|
||||
"dev": "HTTPS=true react-app-rewired start"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
}
|
||||
Reference in New Issue
Block a user