mirror of
https://github.com/balzack/databag.git
synced 2025-02-11 19:19:16 +00:00
sending mfa qr code for setup
This commit is contained in:
parent
e5fe393b43
commit
810009f7aa
@ -1,10 +1,13 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"image/png"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"databag/internal/store"
|
||||
"encoding/base64"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@ -51,6 +54,14 @@ func AddMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
png.Encode(&buf, img)
|
||||
enc := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
|
||||
SetStatus(account)
|
||||
WriteResponse(w, account.MFASecret)
|
||||
WriteResponse(w, MFASecret{ Image: "data:image/png;base64," + enc, Text: account.MFASecret })
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package databag
|
||||
|
||||
import (
|
||||
"databag/internal/store"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
@ -34,7 +35,8 @@ func SetMultiFactorAuth(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
|
||||
@ -61,6 +63,23 @@ func SetMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
account.MFAConfirmed = true
|
||||
if res := tx.Model(account).Update("mfa_confirmed", account.MFAConfirmed).Error; res != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, res)
|
||||
return res
|
||||
}
|
||||
account.AccountRevision += 1;
|
||||
if res := tx.Model(&account).Update("account_revision", account.AccountRevision).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
SetStatus(account)
|
||||
WriteResponse(w, nil)
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ const APPQueueDefault = ""
|
||||
const APPDefaultPath = "/tmp/databag/assets"
|
||||
|
||||
//APPMFAIssuer name servive
|
||||
const APPMFAIssuer = "databag"
|
||||
const APPMFAIssuer = "Databag"
|
||||
|
||||
//APPMFAFailPeriod time window login failures can occur
|
||||
const APPMFAFailPeriod = 300
|
||||
|
@ -53,6 +53,13 @@ type Announce struct {
|
||||
AppToken string `json:"appToken"`
|
||||
}
|
||||
|
||||
//MFASecret values for configuring TOTP
|
||||
type MFASecret struct {
|
||||
Image string `json:"secretImage"`
|
||||
|
||||
Text string `json:"secretText"`
|
||||
}
|
||||
|
||||
//Notification describes type of notifications to receive
|
||||
type Notification struct {
|
||||
Event string `json:"event,omitempty"`
|
||||
|
@ -19,7 +19,7 @@
|
||||
"@charliewilco/gluejar": "^1.0.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"antd": "^5.0.4",
|
||||
"antd": "^5.17.2",
|
||||
"axios": "^0.27.2",
|
||||
"base-64": "^1.0.0",
|
||||
"crypto-js": "^4.1.1",
|
||||
|
@ -8,7 +8,7 @@ export function createWebsocket(url) {
|
||||
|
||||
export function checkResponse(response) {
|
||||
if(response.status >= 400 && response.status < 600) {
|
||||
throw new Error(response.url + " failed");
|
||||
throw new Error(response.status);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,6 +84,8 @@ export function useAccountContext() {
|
||||
await removeAccountMFA(access.current);
|
||||
},
|
||||
confirmMFA: async (code) => {
|
||||
console.log("CONFIRMING: ", code);
|
||||
|
||||
await setAccountMFA(access.current, code);
|
||||
},
|
||||
setSeal: async (seal, sealKey) => {
|
||||
|
@ -4,7 +4,7 @@ import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutl
|
||||
import { ThemeProvider } from "styled-components";
|
||||
import { useDashboard } from './useDashboard.hook';
|
||||
import { AccountItem } from './accountItem/AccountItem';
|
||||
import { CopyButton } from './copyButton/CopyButton';
|
||||
import { CopyButton } from '../copyButton/CopyButton';
|
||||
|
||||
export function Dashboard() {
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableBu
|
||||
import { useAccountItem } from './useAccountItem.hook';
|
||||
import { ExclamationCircleOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { Modal, Tooltip, Button } from 'antd';
|
||||
import { CopyButton } from '../copyButton/CopyButton';
|
||||
import { CopyButton } from '../../copyButton/CopyButton';
|
||||
|
||||
export function AccountItem({ item, remove }) {
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { AccountAccessWrapper, LoginModal, SealModal, LogoutContent } from './AccountAccess.styled';
|
||||
import { AccountAccessWrapper, LoginModal, MFAModal, SealModal, LogoutContent } from './AccountAccess.styled';
|
||||
import { useAccountAccess } from './useAccountAccess.hook';
|
||||
import { Button, Modal, Switch, Input, Radio, Select } from 'antd';
|
||||
import { LogoutOutlined, SettingOutlined, UserOutlined, LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal, Switch, Input, Radio, Select, Flex, Typography } from 'antd';
|
||||
import type { GetProp } from 'antd';
|
||||
import type { OTPProps } from 'antd/es/input/OTP';
|
||||
import { LogoutOutlined, SettingOutlined, UserOutlined, LockOutlined, ExclamationCircleOutlined, KeyOutlined } from '@ant-design/icons';
|
||||
import { CopyButton } from '../../../../copyButton/CopyButton';
|
||||
import { useRef } from 'react';
|
||||
|
||||
export function AccountAccess() {
|
||||
@ -40,7 +43,6 @@ export function AccountAccess() {
|
||||
};
|
||||
|
||||
const enableMFA = async (enable) => {
|
||||
console.log("ENABLE: ", enable);
|
||||
try {
|
||||
if (enable) {
|
||||
await actions.enableMFA();
|
||||
@ -264,8 +266,30 @@ console.log("ENABLE: ", enable);
|
||||
</div>
|
||||
</LoginModal>
|
||||
</Modal>
|
||||
<Modal centerd closable={false} footer={null} visible={state.mfaModal} bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} onCancel={actions.dismissMFA}>
|
||||
<div>{ state.mfaSecret }</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">Multi-Factor Authentication</div>
|
||||
<div className="description">Store the secret and confirm the verification code</div>
|
||||
<img src={state.mfaImage} alt="QRCode" />
|
||||
<div className="secret">
|
||||
<div className="label">{ state.mfaSecret }</div>
|
||||
<CopyButton onCopy={async () => await navigator.clipboard.writeText(state.mfaSecret)} />
|
||||
</div>
|
||||
<Input.OTP onChange={actions.setCode} />
|
||||
<div className="alert">
|
||||
{ state.mfaError && state.mfaErrorCode == 'Error: 401' && (
|
||||
<span>verification code error</span>
|
||||
)}
|
||||
{ state.mfaError && state.mfaErrorCode == 'Error: 429' && (
|
||||
<span>verification temporarily disabled</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>
|
||||
</div>
|
||||
</MFAModal>
|
||||
</Modal>
|
||||
</AccountAccessWrapper>
|
||||
);
|
||||
|
@ -182,6 +182,73 @@ export const SealModal = 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;
|
||||
}
|
||||
|
||||
.secret {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
|
||||
.label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.code {
|
||||
padding-top: 4px;
|
||||
border-bottom: 1px solid ${props => props.theme.sectionBorder};
|
||||
}
|
||||
|
||||
.codeLabel {
|
||||
padding-top: 4px;
|
||||
font-size: 0.9.rem;
|
||||
color: ${props => props.theme.mainText};
|
||||
}
|
||||
|
||||
.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};
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const LoginModal = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -43,7 +43,10 @@ export function useAccountAccess() {
|
||||
mfaModal: false,
|
||||
mfaEnabled: null,
|
||||
mfaSecret: null,
|
||||
mfaImage: null,
|
||||
mfaCode: null,
|
||||
mfaError: false,
|
||||
mfaErrorCode: null,
|
||||
|
||||
seal: null,
|
||||
sealKey: null,
|
||||
@ -319,8 +322,8 @@ export function useAccountAccess() {
|
||||
if (!state.busy) {
|
||||
try {
|
||||
updateState({ busy: true });
|
||||
const secret = await account.actions.enableMFA();
|
||||
updateState({ busy: false, mfaModal: true, mfaSecret: secret, mfaCode: '' });
|
||||
const mfa = await account.actions.enableMFA();
|
||||
updateState({ busy: false, mfaModal: true, mfaError: false, mfaSecret: mfa.secretText, mfaImage: mfa.secretImage, mfaCode: '' });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
@ -347,12 +350,12 @@ export function useAccountAccess() {
|
||||
if (!state.busy) {
|
||||
try {
|
||||
updateState({ busy: true });
|
||||
await account.actions.confirmMFA(state.code);
|
||||
updateState({ busy: false });
|
||||
await account.actions.confirmMFA(state.mfaCode);
|
||||
updateState({ busy: false, mfaModal: false });
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
updateState({ busy: false });
|
||||
console.log("error code: ", err);
|
||||
updateState({ busy: false, mfaError: true, mfaErrorCode: err });
|
||||
throw new Error('failed to confirm mfa');
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user