adding server support for mfa

This commit is contained in:
Roland Osborne 2024-05-15 14:48:38 -07:00
parent f5461cf870
commit 19248eee7c
8 changed files with 249 additions and 2 deletions

View File

@ -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

View File

@ -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")

View 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)
}

View 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)
}

View 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)
}

View File

@ -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 {

View File

@ -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"),

View File

@ -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