mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
sending mfa qr code for setup
This commit is contained in:
parent
e5fe393b43
commit
810009f7aa
@ -1,10 +1,13 @@
|
|||||||
package databag
|
package databag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"image/png"
|
||||||
"github.com/pquerna/otp"
|
"github.com/pquerna/otp"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"databag/internal/store"
|
"databag/internal/store"
|
||||||
|
"encoding/base64"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,6 +54,14 @@ func AddMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
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 (
|
import (
|
||||||
"databag/internal/store"
|
"databag/internal/store"
|
||||||
|
"github.com/pquerna/otp"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -34,7 +35,8 @@ func SetMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
return;
|
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 {
|
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if account.MFAFailedTime + APPMFAFailPeriod > curTime {
|
if account.MFAFailedTime + APPMFAFailPeriod > curTime {
|
||||||
account.MFAFailedCount += 1
|
account.MFAFailedCount += 1
|
||||||
@ -61,6 +63,23 @@ func SetMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
SetStatus(account)
|
||||||
WriteResponse(w, nil)
|
WriteResponse(w, nil)
|
||||||
}
|
}
|
||||||
|
@ -148,7 +148,7 @@ const APPQueueDefault = ""
|
|||||||
const APPDefaultPath = "/tmp/databag/assets"
|
const APPDefaultPath = "/tmp/databag/assets"
|
||||||
|
|
||||||
//APPMFAIssuer name servive
|
//APPMFAIssuer name servive
|
||||||
const APPMFAIssuer = "databag"
|
const APPMFAIssuer = "Databag"
|
||||||
|
|
||||||
//APPMFAFailPeriod time window login failures can occur
|
//APPMFAFailPeriod time window login failures can occur
|
||||||
const APPMFAFailPeriod = 300
|
const APPMFAFailPeriod = 300
|
||||||
|
@ -53,6 +53,13 @@ type Announce struct {
|
|||||||
AppToken string `json:"appToken"`
|
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
|
//Notification describes type of notifications to receive
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
Event string `json:"event,omitempty"`
|
Event string `json:"event,omitempty"`
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"@charliewilco/gluejar": "^1.0.0",
|
"@charliewilco/gluejar": "^1.0.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
"antd": "^5.0.4",
|
"antd": "^5.17.2",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"base-64": "^1.0.0",
|
"base-64": "^1.0.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
@ -8,7 +8,7 @@ export function createWebsocket(url) {
|
|||||||
|
|
||||||
export function checkResponse(response) {
|
export function checkResponse(response) {
|
||||||
if(response.status >= 400 && response.status < 600) {
|
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);
|
await removeAccountMFA(access.current);
|
||||||
},
|
},
|
||||||
confirmMFA: async (code) => {
|
confirmMFA: async (code) => {
|
||||||
|
console.log("CONFIRMING: ", code);
|
||||||
|
|
||||||
await setAccountMFA(access.current, code);
|
await setAccountMFA(access.current, code);
|
||||||
},
|
},
|
||||||
setSeal: async (seal, sealKey) => {
|
setSeal: async (seal, sealKey) => {
|
||||||
|
@ -4,7 +4,7 @@ import { ExclamationCircleOutlined, SettingOutlined, UserAddOutlined, LogoutOutl
|
|||||||
import { ThemeProvider } from "styled-components";
|
import { ThemeProvider } from "styled-components";
|
||||||
import { useDashboard } from './useDashboard.hook';
|
import { useDashboard } from './useDashboard.hook';
|
||||||
import { AccountItem } from './accountItem/AccountItem';
|
import { AccountItem } from './accountItem/AccountItem';
|
||||||
import { CopyButton } from './copyButton/CopyButton';
|
import { CopyButton } from '../copyButton/CopyButton';
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { AccountItemWrapper, AccessLayout, DeleteButton, EnableButton, DisableBu
|
|||||||
import { useAccountItem } from './useAccountItem.hook';
|
import { useAccountItem } from './useAccountItem.hook';
|
||||||
import { ExclamationCircleOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, UserDeleteOutlined, UnlockOutlined, CloseCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||||
import { Modal, Tooltip, Button } from 'antd';
|
import { Modal, Tooltip, Button } from 'antd';
|
||||||
import { CopyButton } from '../copyButton/CopyButton';
|
import { CopyButton } from '../../copyButton/CopyButton';
|
||||||
|
|
||||||
export function AccountItem({ item, remove }) {
|
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 { useAccountAccess } from './useAccountAccess.hook';
|
||||||
import { Button, Modal, Switch, Input, Radio, Select } from 'antd';
|
import { Button, Modal, Switch, Input, Radio, Select, Flex, Typography } from 'antd';
|
||||||
import { LogoutOutlined, SettingOutlined, UserOutlined, LockOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
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';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
export function AccountAccess() {
|
export function AccountAccess() {
|
||||||
@ -40,7 +43,6 @@ export function AccountAccess() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const enableMFA = async (enable) => {
|
const enableMFA = async (enable) => {
|
||||||
console.log("ENABLE: ", enable);
|
|
||||||
try {
|
try {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
await actions.enableMFA();
|
await actions.enableMFA();
|
||||||
@ -264,8 +266,30 @@ console.log("ENABLE: ", enable);
|
|||||||
</div>
|
</div>
|
||||||
</LoginModal>
|
</LoginModal>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal centerd closable={false} footer={null} visible={state.mfaModal} bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} onCancel={actions.dismissMFA}>
|
<Modal centerd closable={false} footer={null} visible={state.mfaModal} destroyOnClose={true} bodyStyle={{ borderRadius: 8, padding: 16, ...state.menuStyle }} onCancel={actions.dismissMFA}>
|
||||||
<div>{ state.mfaSecret }</div>
|
<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>
|
</Modal>
|
||||||
</AccountAccessWrapper>
|
</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`
|
export const LoginModal = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -43,7 +43,10 @@ export function useAccountAccess() {
|
|||||||
mfaModal: false,
|
mfaModal: false,
|
||||||
mfaEnabled: null,
|
mfaEnabled: null,
|
||||||
mfaSecret: null,
|
mfaSecret: null,
|
||||||
|
mfaImage: null,
|
||||||
mfaCode: null,
|
mfaCode: null,
|
||||||
|
mfaError: false,
|
||||||
|
mfaErrorCode: null,
|
||||||
|
|
||||||
seal: null,
|
seal: null,
|
||||||
sealKey: null,
|
sealKey: null,
|
||||||
@ -319,8 +322,8 @@ export function useAccountAccess() {
|
|||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
try {
|
try {
|
||||||
updateState({ busy: true });
|
updateState({ busy: true });
|
||||||
const secret = await account.actions.enableMFA();
|
const mfa = await account.actions.enableMFA();
|
||||||
updateState({ busy: false, mfaModal: true, mfaSecret: secret, mfaCode: '' });
|
updateState({ busy: false, mfaModal: true, mfaError: false, mfaSecret: mfa.secretText, mfaImage: mfa.secretImage, mfaCode: '' });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@ -347,12 +350,12 @@ export function useAccountAccess() {
|
|||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
try {
|
try {
|
||||||
updateState({ busy: true });
|
updateState({ busy: true });
|
||||||
await account.actions.confirmMFA(state.code);
|
await account.actions.confirmMFA(state.mfaCode);
|
||||||
updateState({ busy: false });
|
updateState({ busy: false, mfaModal: false });
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.log(err);
|
console.log("error code: ", err);
|
||||||
updateState({ busy: false });
|
updateState({ busy: false, mfaError: true, mfaErrorCode: err });
|
||||||
throw new Error('failed to confirm mfa');
|
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