mirror of
https://github.com/balzack/databag.git
synced 2025-04-20 08:35:15 +00:00
adding server support for mfa
This commit is contained in:
parent
f5461cf870
commit
19248eee7c
@ -634,7 +634,7 @@ paths:
|
||||
schema:
|
||||
type: boolean
|
||||
|
||||
/account/authentication:
|
||||
/account/mfauth:
|
||||
post:
|
||||
tags:
|
||||
- account
|
||||
@ -681,6 +681,10 @@ paths:
|
||||
description: success
|
||||
'401':
|
||||
description: permission denied
|
||||
'405':
|
||||
description: totp code required but not set
|
||||
'429':
|
||||
description: temporarily locked due to too many failures
|
||||
'500':
|
||||
description: internal server error
|
||||
delete:
|
||||
@ -973,7 +977,7 @@ paths:
|
||||
description: invalid token
|
||||
'406':
|
||||
description: app limit reached
|
||||
'409':
|
||||
'405':
|
||||
description: totp code required but not set
|
||||
'410':
|
||||
description: account disabled
|
||||
|
@ -4,8 +4,11 @@ import (
|
||||
"databag/internal/store"
|
||||
"encoding/hex"
|
||||
"github.com/theckman/go-securerandom"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
//AddAccountApp with access token, attach an app to an account generating agent token
|
||||
@ -17,6 +20,47 @@ func AddAccountApp(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
curTime := time.Now().Unix()
|
||||
if account.MFAFailedTime + APPMFAFailPeriod > curTime && account.MFAFailedCount > APPMFAFailCount {
|
||||
ErrResponse(w, http.StatusTooManyRequests, errors.New("temporarily locked"))
|
||||
return;
|
||||
}
|
||||
|
||||
if account.MFAEnabled && account.MFAConfirmed {
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp code required"))
|
||||
return;
|
||||
}
|
||||
|
||||
if !totp.Validate(account.MFASecret, code) {
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if account.MFAFailedTime + APPMFAFailPeriod > curTime {
|
||||
account.MFAFailedCount += 1;
|
||||
if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
} else {
|
||||
account.MFAFailedTime = curTime
|
||||
if res := tx.Model(account).Update("mfa_failed_time", account.MFAFailedTime).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
account.MFAFailedCount = 1
|
||||
if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
LogMsg("failed to increment fail count");
|
||||
}
|
||||
|
||||
ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// parse authentication token
|
||||
appName := r.FormValue("appName")
|
||||
appVersion := r.FormValue("appVersion")
|
||||
|
56
net/server/internal/api_addMultiFactorAuth.go
Normal file
56
net/server/internal/api_addMultiFactorAuth.go
Normal file
@ -0,0 +1,56 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"databag/internal/store"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
//AddMultiFactorAuth enables multi-factor auth on the given account
|
||||
func AddMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
account, code, err := ParamAgentToken(r, true)
|
||||
if err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: APPMFAIssuer,
|
||||
AccountName: account.GUID,
|
||||
Digits: otp.DigitsSix,
|
||||
Algorithm: otp.AlgorithmSHA256,
|
||||
})
|
||||
|
||||
err = store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
account.MFAConfirmed = false
|
||||
if res := tx.Model(account).Update("mfa_confirmed", account.MFAConfirmed).Error; res != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, res)
|
||||
return res
|
||||
}
|
||||
account.MFAEnabled = true
|
||||
if res := tx.Model(account).Update("mfa_enabled", account.MFAEnabled).Error; res != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, res)
|
||||
return res
|
||||
}
|
||||
account.MFASecret = key.Secret()
|
||||
if res := tx.Model(account).Update("mfa_secret", account.MFASecret).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, account.MFASecret)
|
||||
}
|
42
net/server/internal/api_removeMultiFactorAuth.go
Normal file
42
net/server/internal/api_removeMultiFactorAuth.go
Normal file
@ -0,0 +1,42 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"databag/internal/store"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
//Disable multi-factor auth on account
|
||||
func RemoveMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
account, code, err := ParamAgentToken(r, true)
|
||||
if err != nil {
|
||||
ErrResponse(w, code, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
account.MFAConfirmed = false
|
||||
if res := tx.Model(account).Update("mfa_confirmed", account.MFAConfirmed).Error; res != nil {
|
||||
ErrResponse(w, http.StatusInternalServerError, res)
|
||||
return res
|
||||
}
|
||||
account.MFAEnabled = false
|
||||
if res := tx.Model(account).Update("mfa_enabled", account.MFAEnabled).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)
|
||||
}
|
66
net/server/internal/api_setMultiFactorAuth.go
Normal file
66
net/server/internal/api_setMultiFactorAuth.go
Normal file
@ -0,0 +1,66 @@
|
||||
package databag
|
||||
|
||||
import (
|
||||
"databag/internal/store"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
//SetMultiFactorAuth
|
||||
func SetMultiFactorAuth(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
account, ret, err := ParamAgentToken(r, true)
|
||||
if err != nil {
|
||||
ErrResponse(w, ret, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !account.MFAEnabled {
|
||||
ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp not enabled"))
|
||||
return;
|
||||
}
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
ErrResponse(w, http.StatusMethodNotAllowed, errors.New("totp code required"))
|
||||
return;
|
||||
}
|
||||
|
||||
curTime := time.Now().Unix()
|
||||
if account.MFAFailedTime + APPMFAFailPeriod > curTime && account.MFAFailedCount > APPMFAFailCount {
|
||||
ErrResponse(w, http.StatusTooManyRequests, errors.New("temporarily locked"))
|
||||
return;
|
||||
}
|
||||
|
||||
if !totp.Validate(account.MFASecret, code) {
|
||||
err := store.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if account.MFAFailedTime + APPMFAFailPeriod > curTime {
|
||||
account.MFAFailedCount += 1
|
||||
if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
} else {
|
||||
account.MFAFailedTime = curTime
|
||||
if res := tx.Model(account).Update("mfa_failed_time", account.MFAFailedTime).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
account.MFAFailedCount = 1
|
||||
if res := tx.Model(account).Update("mfa_failed_count", account.MFAFailedCount).Error; res != nil {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
LogMsg("failed to increment fail count");
|
||||
}
|
||||
|
||||
ErrResponse(w, http.StatusUnauthorized, errors.New("invalid code"))
|
||||
return
|
||||
}
|
||||
|
||||
SetStatus(account)
|
||||
WriteResponse(w, nil)
|
||||
}
|
@ -147,6 +147,15 @@ const APPQueueDefault = ""
|
||||
//APPDefaultPath config for default path to store assets
|
||||
const APPDefaultPath = "/tmp/databag/assets"
|
||||
|
||||
//APPMFAIssuer name servive
|
||||
const APPMFAIssuer = "databag"
|
||||
|
||||
//APPMFAFailPeriod time window login failures can occur
|
||||
const APPMFAFailPeriod = 300
|
||||
|
||||
//APPMFAFailCount limit of login failures in period
|
||||
const APPMFAFailCount = 4
|
||||
|
||||
//AppCardStatus compares cards status with string
|
||||
func AppCardStatus(status string) bool {
|
||||
if status == APPCardPending {
|
||||
|
@ -202,6 +202,27 @@ var endpoints = routes{
|
||||
SetAccountSearchable,
|
||||
},
|
||||
|
||||
route{
|
||||
"AddMultiFactorAuth",
|
||||
strings.ToUpper("Post"),
|
||||
"/account/mfauth",
|
||||
AddMultiFactorAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"SetMultiFactorAuth",
|
||||
strings.ToUpper("Put"),
|
||||
"/account/mfauth",
|
||||
SetMultiFactorAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"RemoveMultiFactorAuth",
|
||||
strings.ToUpper("Delete"),
|
||||
"/account/mfauth",
|
||||
RemoveMultiFactorAuth,
|
||||
},
|
||||
|
||||
route{
|
||||
"AddNodeAccount",
|
||||
strings.ToUpper("Post"),
|
||||
|
@ -81,6 +81,11 @@ type Account struct {
|
||||
Updated int64 `gorm:"autoUpdateTime"`
|
||||
Disabled bool `gorm:"not null;default:false"`
|
||||
Searchable bool `gorm:"not null;default:false"`
|
||||
MFAEnabled bool `gorm:"not null;default:false"`
|
||||
MFAConfirmed bool `gorm:"not null;default:false"`
|
||||
MFASecret string
|
||||
MFAFailedTime int64
|
||||
MFAFailedCount uint
|
||||
Forward string
|
||||
AccountDetail AccountDetail
|
||||
Apps []App
|
||||
|
Loading…
x
Reference in New Issue
Block a user