mirror of
https://github.com/balzack/databag.git
synced 2025-04-23 18:15:19 +00:00
allow for login with MFA token
This commit is contained in:
parent
810009f7aa
commit
5336d19608
@ -681,6 +681,8 @@ paths:
|
||||
description: success
|
||||
'401':
|
||||
description: permission denied
|
||||
'403':
|
||||
description: totp code not correct
|
||||
'405':
|
||||
description: totp code required but not set
|
||||
'429':
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"github.com/theckman/go-securerandom"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/pquerna/otp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"errors"
|
||||
@ -33,7 +34,8 @@ func AddAccountApp(w http.ResponseWriter, r *http.Request) {
|
||||
return;
|
||||
}
|
||||
|
||||
if !totp.Validate(account.MFASecret, code) {
|
||||
opts := totp.ValidateOpts{Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA256}
|
||||
if valid, _ := totp.ValidateCustom(code, account.MFASecret, time.Now(), opts); !valid {
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if account.MFAFailedTime + APPMFAFailPeriod > curTime {
|
||||
account.MFAFailedCount += 1;
|
||||
@ -56,7 +58,7 @@ func AddAccountApp(w http.ResponseWriter, r *http.Request) {
|
||||
LogMsg("failed to increment fail count");
|
||||
}
|
||||
|
||||
ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
|
||||
ErrResponse(w, http.StatusForbidden, errors.New("invalid code"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Button, Modal, Form, Input } from 'antd';
|
||||
import { SettingOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { LoginWrapper } from './Login.styled';
|
||||
import { LoginWrapper, MFAModal } from './Login.styled';
|
||||
import { useLogin } from './useLogin.hook';
|
||||
|
||||
export function Login() {
|
||||
@ -27,6 +27,8 @@ export function Login() {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(state.mfaError);
|
||||
|
||||
return (
|
||||
<LoginWrapper>
|
||||
{ modalContext }
|
||||
@ -73,6 +75,27 @@ export function Login() {
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
<Modal centerd closable={false} footer={null} visible={state.mfaModal} destroyOnClose={true} bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} onCancel={actions.dismissMFA}>
|
||||
<MFAModal>
|
||||
<div className="title">{state.strings.mfaTitle}</div>
|
||||
<div className="description">{state.strings.mfaEnter}</div>
|
||||
<Input.OTP onChange={actions.setCode} />
|
||||
<div className="alert">
|
||||
{ state.mfaError == 'Error: 403' && (
|
||||
<span>{state.strings.mfaError}</span>
|
||||
)}
|
||||
{ state.mfaError == 'Error: 429' && (
|
||||
<span>{state.strings.mfaDisabled}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="controls">
|
||||
<Button key="back" onClick={actions.dismissMFA}>{state.strings.cancel}</Button>
|
||||
<Button key="save" type="primary" className={state.mfaCode ? 'saveEnabled' : 'saveDisabled'} onClick={login}
|
||||
disabled={!state.mfaCode} loading={state.busy}>{state.strings.login}</Button>
|
||||
</div>
|
||||
|
||||
</MFAModal>
|
||||
</Modal>
|
||||
</LoginWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -80,4 +80,53 @@ export const LoginWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const MFAModal = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 1.0rem;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.code {
|
||||
padding-top: 4px;
|
||||
border-bottom: 1px solid ${props => props.theme.sectionBorder};
|
||||
}
|
||||
|
||||
.alert {
|
||||
height: 24px;
|
||||
color: ${props => props.theme.alertText};
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
|
||||
.saveDisabled {
|
||||
background-color: ${props => props.theme.disabledArea};
|
||||
|
||||
button {
|
||||
color: ${props => props.theme.idleText};
|
||||
}
|
||||
}
|
||||
|
||||
.saveEnabled {
|
||||
background-color: ${props => props.theme.enabledArea};
|
||||
|
||||
button {
|
||||
color: ${props => props.theme.activeText};
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
@ -15,6 +15,9 @@ export function useLogin() {
|
||||
busy: false,
|
||||
strings: {},
|
||||
menuStyle: {},
|
||||
mfaModal: false,
|
||||
mfaCode: null,
|
||||
mfaError: null,
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
@ -46,12 +49,17 @@ export function useLogin() {
|
||||
if (!state.busy && state.username !== '' && state.password !== '') {
|
||||
updateState({ busy: true })
|
||||
try {
|
||||
await app.actions.login(state.username, state.password)
|
||||
await app.actions.login(state.username, state.password, state.mfaCode)
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
updateState({ busy: false })
|
||||
throw new Error('login failed: check your username and password');
|
||||
if (err == 'Error: 405' || err == 'Error: 403' || err == 'Error: 429') {
|
||||
updateState({ busy: false, mfaModal: true, mfaError: err.toString() });
|
||||
}
|
||||
else {
|
||||
console.log(err);
|
||||
updateState({ busy: false })
|
||||
throw new Error('login failed: check your username and password');
|
||||
}
|
||||
}
|
||||
updateState({ busy: false })
|
||||
}
|
||||
@ -59,6 +67,12 @@ export function useLogin() {
|
||||
onCreate: () => {
|
||||
navigate('/create');
|
||||
},
|
||||
setCode: (mfaCode) => {
|
||||
updateState({ mfaCode });
|
||||
},
|
||||
dismissMFA: () => {
|
||||
updateState({ mfaModal: false, mfaCode: null });
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { checkResponse, fetchWithTimeout } from './fetchUtil';
|
||||
var base64 = require('base-64');
|
||||
|
||||
export async function setLogin(username, password, appName, appVersion, userAgent) {
|
||||
export async function setLogin(username, password, code, appName, appVersion, userAgent) {
|
||||
const platform = encodeURIComponent(userAgent);
|
||||
const mfa = code ? `&code=${code}` : '';
|
||||
let headers = new Headers()
|
||||
headers.append('Authorization', 'Basic ' + base64.encode(username + ":" + password));
|
||||
let login = await fetchWithTimeout(`/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}`, { method: 'POST', body: JSON.stringify([]), headers: headers })
|
||||
let login = await fetchWithTimeout(`/account/apps?appName=${appName}&appVersion=${appVersion}&platform=${platform}${mfa}`, { method: 'POST', body: JSON.stringify([]), headers: headers })
|
||||
checkResponse(login)
|
||||
return await login.json()
|
||||
}
|
||||
|
@ -188,6 +188,13 @@ export const en = {
|
||||
confirmRemove: 'Are you sure you want to delete the contact?',
|
||||
message: 'Message',
|
||||
securedMessage: 'Sealed Message',
|
||||
|
||||
mfaTitle: 'Multi-Factor Authentication',
|
||||
mfaSteps: 'Store the secret and confirm the verification code',
|
||||
mfaError: 'verification code error',
|
||||
mfaDisabled: 'verification temporarily disabled',
|
||||
mfaConfirm: 'Confirm',
|
||||
mfaEnter: 'Enter your verification code',
|
||||
};
|
||||
|
||||
export const fr = {
|
||||
@ -381,6 +388,13 @@ export const fr = {
|
||||
|
||||
message: 'Message',
|
||||
sealedMessage: 'Message Sécurisé',
|
||||
|
||||
mfaTitle: 'Authentification Multi-Factor',
|
||||
mfaSteps: 'Enregistrez le secret et confirmez le code de vérification',
|
||||
mfaEnter: 'Entrez votre code de vérification',
|
||||
mfaError: 'erreur de code de vérification',
|
||||
mfaDisabled: 'vérification temporairement désactivée',
|
||||
mfaConfirm: 'Confirmer',
|
||||
};
|
||||
|
||||
export const sp = {
|
||||
@ -573,6 +587,13 @@ export const sp = {
|
||||
confirmRemove: '¿Estás seguro de que quieres eliminar el contacto?',
|
||||
message: 'Mensaje',
|
||||
sealedMessage: 'Mensaje Seguro',
|
||||
|
||||
mfaTitle: 'Autenticación de Dos Factores',
|
||||
mfaSteps: 'Guarde el secreto y confirme el código de verificación',
|
||||
mfaEnter: 'Ingresa tu código de verificación',
|
||||
mfaError: 'error de código de verificación',
|
||||
mfaDisabled: 'verificación temporalmente deshabilitada',
|
||||
mfaConfirm: 'Confirmar',
|
||||
};
|
||||
|
||||
export const pt = {
|
||||
@ -765,6 +786,13 @@ export const pt = {
|
||||
confirmRemove: 'Tem certeza de que deseja remover o contato?',
|
||||
message: 'Mensagem',
|
||||
sealedMessage: 'Mensagem Segura',
|
||||
|
||||
mfaTitle: 'Autenticação de Dois Fatores',
|
||||
mfaSteps: 'Salve o segredo e confirme o código de verificação',
|
||||
mfaEnter: 'Digite seu código de verificação',
|
||||
mfaError: 'erro de código de verificação',
|
||||
mfaDisabled: 'verificação temporariamente desativada',
|
||||
mfaConfirm: 'Confirmar',
|
||||
};
|
||||
|
||||
export const de = {
|
||||
@ -957,6 +985,13 @@ export const de = {
|
||||
confirmRemove: 'Sind Sie sicher, dass Sie den Kontakt löschen möchten?',
|
||||
message: 'Nachricht',
|
||||
sealedMessage: 'Gesicherte Nachricht',
|
||||
|
||||
mfaTitle: 'Zwei-Faktor-Authentifizierung',
|
||||
mfaSteps: 'Speichern Sie das Geheimnis und bestätigen Sie den Verifizierungscode',
|
||||
mfaEnter: 'Geben Sie Ihren Bestätigungs-Code ein',
|
||||
mfaError: 'Verifizierungscodefehler',
|
||||
mfaDisabled: 'Verifizierung vorübergehend deaktiviert',
|
||||
mfaConfirm: 'Bestätigen',
|
||||
};
|
||||
|
||||
export const ru = {
|
||||
@ -1149,4 +1184,11 @@ export const ru = {
|
||||
confirmRemove: 'Вы уверены, что хотите удалить контакт?',
|
||||
message: 'Cообщение',
|
||||
sealedMessage: 'Защищенное Cообщение',
|
||||
|
||||
mfaTitle: 'Двухфакторная аутентификация',
|
||||
mfaSteps: 'Сохраните секрет и подтвердите проверочный код',
|
||||
mfaEnter: 'Введите Ваш верификационный код',
|
||||
mfaError: 'ошибка проверочного кода',
|
||||
mfaDisabled: 'проверка временно отключена',
|
||||
mfaConfirm: 'Подтвердить',
|
||||
};
|
||||
|
@ -77,8 +77,8 @@ export function useAppContext(websocket) {
|
||||
access: async (token) => {
|
||||
await appAccess(token)
|
||||
},
|
||||
login: async (username, password) => {
|
||||
await appLogin(username, password)
|
||||
login: async (username, password, code) => {
|
||||
await appLogin(username, password, code)
|
||||
},
|
||||
create: async (username, password, token) => {
|
||||
await appCreate(username, password, token)
|
||||
@ -96,7 +96,7 @@ export function useAppContext(websocket) {
|
||||
throw new Error('invalid session state');
|
||||
}
|
||||
await addAccount(username, password, token);
|
||||
const access = await setLogin(username, password, appName, appVersion, userAgent);
|
||||
const access = await setLogin(username, password, null, appName, appVersion, userAgent);
|
||||
storeContext.actions.setValue('login:timestamp', access.created);
|
||||
setSession(access.appToken);
|
||||
appToken.current = access.appToken;
|
||||
@ -108,11 +108,11 @@ export function useAppContext(websocket) {
|
||||
return access.created;
|
||||
}
|
||||
|
||||
const appLogin = async (username, password) => {
|
||||
const appLogin = async (username, password, code) => {
|
||||
if (appToken.current || !checked.current) {
|
||||
throw new Error('invalid session state');
|
||||
}
|
||||
const access = await setLogin(username, password, appName, appVersion, userAgent);
|
||||
const access = await setLogin(username, password, code, appName, appVersion, userAgent);
|
||||
storeContext.actions.setValue('login:timestamp', access.created);
|
||||
setSession(access.appToken);
|
||||
appToken.current = access.appToken;
|
||||
|
@ -268,8 +268,8 @@ export function AccountAccess() {
|
||||
</Modal>
|
||||
<Modal centerd closable={false} footer={null} visible={state.mfaModal} destroyOnClose={true} bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} onCancel={actions.dismissMFA}>
|
||||
<MFAModal>
|
||||
<div className="title">Multi-Factor Authentication</div>
|
||||
<div className="description">Store the secret and confirm the verification code</div>
|
||||
<div className="title">{state.strings.mfaTitle}</div>
|
||||
<div className="description">{state.strings.mfaSteps}</div>
|
||||
<img src={state.mfaImage} alt="QRCode" />
|
||||
<div className="secret">
|
||||
<div className="label">{ state.mfaSecret }</div>
|
||||
@ -278,16 +278,16 @@ export function AccountAccess() {
|
||||
<Input.OTP onChange={actions.setCode} />
|
||||
<div className="alert">
|
||||
{ state.mfaError && state.mfaErrorCode == 'Error: 401' && (
|
||||
<span>verification code error</span>
|
||||
<span>{state.strings.mfaError}</span>
|
||||
)}
|
||||
{ state.mfaError && state.mfaErrorCode == 'Error: 429' && (
|
||||
<span>verification temporarily disabled</span>
|
||||
<span>{state.strings.mfaDisabled}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="controls">
|
||||
<Button key="back" onClick={actions.dismissMFA}>{state.strings.cancel}</Button>
|
||||
<Button key="save" type="primary" className={state.mfaCode ? 'saveEnabled' : 'saveDisabled'} onClick={actions.confirmMFA}
|
||||
disabled={!state.mfaCode} loading={state.busy}>Confirm</Button>
|
||||
disabled={!state.mfaCode} loading={state.busy}>{state.strings.mfaConfirm}</Button>
|
||||
</div>
|
||||
</MFAModal>
|
||||
</Modal>
|
||||
|
Loading…
x
Reference in New Issue
Block a user