Adding admin creation, conf and other items

* Config now added, accessible via config.Config
* Admin can now be generated via a randomized URL if there are no admins
in the system
* Added a shared floor to login attempts to block enumeration attacks
This commit is contained in:
🐙PiperYxzzy
2022-05-03 18:46:22 +02:00
parent 3c1970698b
commit 66c35e7e4a
8 changed files with 134 additions and 22 deletions

4
conf.json Normal file
View File

@@ -0,0 +1,4 @@
{
"AllowFreshAdminGeneration": true,
"AdminEmails": ["admin@admin.invalid"]
}

27
config/config.go Normal file
View File

@@ -0,0 +1,27 @@
package config
import (
"encoding/json"
"os"
)
type StackConfiguration struct {
ConfigLoaded bool
AllowFreshAdminGeneration bool
AdminEmails []string
AdminHmacEnv string
UserHmacEnv string
}
var Config = StackConfiguration{}
func LoadConfig() {
file, _ := os.Open("conf.json")
defer file.Close()
dec := json.NewDecoder(file)
if err := dec.Decode(&Config); err != nil {
panic(err)
}
Config.ConfigLoaded = true
}

View File

@@ -6,11 +6,13 @@ package core
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/yxzzy-wtf/gin-gonic-prepack/config"
"github.com/yxzzy-wtf/gin-gonic-prepack/database" "github.com/yxzzy-wtf/gin-gonic-prepack/database"
"github.com/yxzzy-wtf/gin-gonic-prepack/models" "github.com/yxzzy-wtf/gin-gonic-prepack/models"
"github.com/yxzzy-wtf/gin-gonic-prepack/util" "github.com/yxzzy-wtf/gin-gonic-prepack/util"
@@ -74,12 +76,12 @@ func UserSignup() gin.HandlerFunc {
return return
} else { } else {
// Email conflict means we should still mock verify // Email conflict means we should still mock verify
go util.SendEmail("Signup Attempt", "Someone tried to sign up with this email. This is a cursory warning. If it was you, good news! You're already signed up!", u.Email) go util.SendEmail("Signup Attempt", "Someone tried to sign up with this email. This is a cursory warning. If it was you, good news! You're already signed up!", []string{u.Email})
} }
} else { } else {
// Send verification // Send verification
verifyToken := u.GetVerificationJwt() verifyToken := u.GetVerificationJwt()
go util.SendEmail("Verify Email", "Helloooo! Go here to verify: http://localhost:9091/v1/verify?verify="+verifyToken, u.Email) go util.SendEmail("Verify Email", "Helloooo! Go here to verify: http://localhost:9091/v1/verify?verify="+verifyToken, []string{u.Email})
} }
c.JSON(http.StatusOK, util.NextMsg{Next: "verification pending"}) c.JSON(http.StatusOK, util.NextMsg{Next: "verification pending"})
@@ -91,15 +93,28 @@ func UserSignup() gin.HandlerFunc {
// are only displayed IFF the user exists AND the password is correct, otherwise a 401 is returned // are only displayed IFF the user exists AND the password is correct, otherwise a 401 is returned
func UserLogin() gin.HandlerFunc { func UserLogin() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// Why do we do this? Assuming a consistent and stable service and an attacker
// with an equally consistent internet connection, it is possible to
// still launch an enumeration attack by comparing the time of a known
// extant address and a known non-extant one. For this reason, login duration is
// floored to at least 5 seconds
minTime := make(chan bool)
go func(c chan bool) {
time.Sleep(time.Second * 5)
minTime <- true
}(minTime)
var loginVals login var loginVals login
if err := c.ShouldBind(&loginVals); err != nil { if err := c.ShouldBind(&loginVals); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "Requires username and password"}) c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "Requires username and password"})
<-minTime
return return
} }
u := models.User{} u := models.User{}
if err := u.ByEmail(loginVals.UserKey); err != nil { if err := u.ByEmail(loginVals.UserKey); err != nil {
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
<-minTime
return return
} }
@@ -109,17 +124,20 @@ func UserLogin() gin.HandlerFunc {
} else { } else {
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
} }
<-minTime
return return
} }
if loginVals.TwoFactor != "" && !checkTwoFactorNotReused(&u.Auth, loginVals.TwoFactor) { if loginVals.TwoFactor != "" && !checkTwoFactorNotReused(&u.Auth, loginVals.TwoFactor) {
fmt.Printf("WARNING: two factor code %v reused for %v\n", loginVals.TwoFactor, u.Uid) fmt.Printf("WARNING: two factor code %v reused for %v\n", loginVals.TwoFactor, u.Uid)
c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: "2fa reused"}) c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: "2fa reused"})
<-minTime
return return
} }
jwt, maxAge := u.GetJwt() jwt, maxAge := u.GetJwt()
c.SetCookie(JwtHeader, jwt, maxAge, "/v1/sec/", "", true, true) c.SetCookie(JwtHeader, jwt, maxAge, "/v1/sec/", "", true, true)
<-minTime
} }
} }
@@ -191,7 +209,7 @@ func UserForgotPassword() gin.HandlerFunc {
if err := u.ByEmail(forgotVals.UserKey); err == nil { if err := u.ByEmail(forgotVals.UserKey); err == nil {
// Actually send renew token // Actually send renew token
forgotJwt := u.GetResetPasswordJwt() forgotJwt := u.GetResetPasswordJwt()
go util.SendEmail("Forgot Password", "Token to reset password: "+forgotJwt, u.Email) go util.SendEmail("Forgot Password", "Token to reset password: "+forgotJwt, []string{u.Email})
} }
c.JSON(http.StatusOK, util.NextMsg{Next: "check email to reset"}) c.JSON(http.StatusOK, util.NextMsg{Next: "check email to reset"})
@@ -249,19 +267,24 @@ func UserResetForgottenPassword() gin.HandlerFunc {
// Admin login functionality, similar to user login but requires 2FA to be set up. // Admin login functionality, similar to user login but requires 2FA to be set up.
func AdminLogin() gin.HandlerFunc { func AdminLogin() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
var loginVals login // Same as user slowdown
if err := c.ShouldBind(&loginVals); err != nil { minTime := make(chan bool)
c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "requires username and password"}) go func(c chan bool) {
} time.Sleep(time.Second * 5)
minTime <- true
}(minTime)
if loginVals.TwoFactor == "" { var loginVals login
c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: "admin access requires 2FA"}) if err := c.ShouldBind(&loginVals); err != nil || loginVals.TwoFactor == "" {
c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "Requires username, 2FA and password"})
<-minTime
return return
} }
a := models.Admin{} a := models.Admin{}
if err := a.ByEmail(loginVals.UserKey); err != nil { if err := a.ByEmail(loginVals.UserKey); err != nil {
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
<-minTime
return return
} }
@@ -271,17 +294,20 @@ func AdminLogin() gin.HandlerFunc {
} else { } else {
c.AbortWithStatus(http.StatusUnauthorized) c.AbortWithStatus(http.StatusUnauthorized)
} }
<-minTime
return return
} }
if loginVals.TwoFactor != "" && !checkTwoFactorNotReused(&a.Auth, loginVals.TwoFactor) { if loginVals.TwoFactor != "" && !checkTwoFactorNotReused(&a.Auth, loginVals.TwoFactor) {
fmt.Printf("WARNING: two factor code %v reused for admin %v\n", loginVals.TwoFactor, a.Uid) fmt.Printf("WARNING: two factor code %v reused by admin %v\n", loginVals.TwoFactor, a.Uid)
c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: "2fa reused"}) c.AbortWithStatusJSON(http.StatusUnauthorized, util.FailMsg{Reason: "2fa reused"})
<-minTime
return return
} }
jwt, maxAge := a.GetJwt() jwt, maxAge := a.GetJwt()
c.SetCookie(JwtHeader, jwt, maxAge, "/v1/adm", "", true, true) c.SetCookie(JwtHeader, jwt, maxAge, "/v1/sec/", "", true, true)
<-minTime
} }
} }
@@ -344,7 +370,6 @@ func LiveTwoFactor() gin.HandlerFunc {
} }
var a models.Auth var a models.Auth
fmt.Println(p)
if p.Role == "user" { if p.Role == "user" {
u := models.User{} u := models.User{}
if err := database.Db.Find(&u, "uid = ?", p.Uid).Error; err != nil { if err := database.Db.Find(&u, "uid = ?", p.Uid).Error; err != nil {
@@ -382,6 +407,37 @@ func LiveTwoFactor() gin.HandlerFunc {
} }
} }
func StarterAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
var count int64
database.Db.Model(&models.Admin{}).Count(&count)
if count != 0 {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
var signupVals signup
if err := c.ShouldBind(&signupVals); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "invalid fields, requires userkey=email and password"})
return
}
a := models.Admin{}
if err := a.ByEmail(signupVals.UserKey); err == nil {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
a.Email = signupVals.UserKey
a.SetPassword(signupVals.Password)
a.GenerateNewTwoFactorSecret()
go util.SendEmail("Admin Created", "A new admin, "+a.Email+", has been created", config.Config.AdminEmails)
c.JSON(http.StatusOK, util.NextMsg{Next: "db verify"})
}
}
// A simple context-aware ping method. If there is a "principal" in this request context // A simple context-aware ping method. If there is a "principal" in this request context
// it will indicate the principal Uid and role in the response // it will indicate the principal Uid and role in the response
func Doot() gin.HandlerFunc { func Doot() gin.HandlerFunc {

20
main.go
View File

@@ -1,9 +1,12 @@
package main package main
import ( import (
"fmt"
"log" "log"
"net/http" "net/http"
"github.com/google/uuid"
"github.com/yxzzy-wtf/gin-gonic-prepack/config"
"github.com/yxzzy-wtf/gin-gonic-prepack/controllers/core" "github.com/yxzzy-wtf/gin-gonic-prepack/controllers/core"
"github.com/yxzzy-wtf/gin-gonic-prepack/database" "github.com/yxzzy-wtf/gin-gonic-prepack/database"
"github.com/yxzzy-wtf/gin-gonic-prepack/models" "github.com/yxzzy-wtf/gin-gonic-prepack/models"
@@ -19,6 +22,8 @@ func Migrate(g *gorm.DB) {
} }
func main() { func main() {
config.LoadConfig()
db := database.Init() db := database.Init()
Migrate(db) Migrate(db)
@@ -28,6 +33,21 @@ func main() {
// Ping functionality // Ping functionality
v1.GET("/doot", core.Doot()) v1.GET("/doot", core.Doot())
if config.Config.AllowFreshAdminGeneration {
var adminCount int64
database.Db.Model(models.Admin{}).Count(&adminCount)
if adminCount == 0 {
randUri := uuid.New()
v1.POST("/"+randUri.String(), core.StarterAdmin())
fmt.Println("#################")
fmt.Println("No admins and AllowFreshAdminGeneration=TRUE")
fmt.Println("Sign up starter at: /" + randUri.String())
fmt.Println("#################")
}
}
// Standard user signup, verify, login and forgot/reset pw // Standard user signup, verify, login and forgot/reset pw
v1.POST("/signup", core.UserSignup()) v1.POST("/signup", core.UserSignup())
v1.POST("/login", core.UserLogin()) v1.POST("/login", core.UserLogin())

View File

@@ -16,7 +16,7 @@ type Admin struct {
const adminJwtDuration = time.Hour * 2 const adminJwtDuration = time.Hour * 2
var AdminHmac = util.GenerateHmac() var AdminHmac = util.GenerateHmac(64)
func (a *Admin) GetJwt() (string, int) { func (a *Admin) GetJwt() (string, int) {
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{

View File

@@ -7,15 +7,16 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"github.com/yxzzy-wtf/gin-gonic-prepack/util"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type Auth struct { type Auth struct {
Base Base
PasswordHash string PasswordHash string `json:"-"`
TwoFactorSecret string TwoFactorSecret string `json:"-"`
TwoFactorRecovery string TwoFactorRecovery string `json:"-"`
Verified bool Verified bool `json:"-"`
} }
func (a *Auth) SetPassword(pass string) error { func (a *Auth) SetPassword(pass string) error {
@@ -85,3 +86,7 @@ func (a *Auth) ValidateTwoFactor(tfCode string, stamp time.Time) error {
return errors.New("unlock invalid") return errors.New("unlock invalid")
} }
} }
func (a *Auth) GenerateNewTwoFactorSecret() {
a.TwoFactorSecret = string(util.GenerateHmac(20))
}

View File

@@ -17,7 +17,7 @@ type User struct {
const userJwtDuration = time.Hour * 24 const userJwtDuration = time.Hour * 24
var UserHmac = util.GenerateHmac() var UserHmac = util.GenerateHmac(64)
func (u *User) GetJwt() (string, int) { func (u *User) GetJwt() (string, int) {
j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{

View File

@@ -8,8 +8,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
func GenerateHmac() []byte { func GenerateHmac(length int) []byte {
b := make([]byte, 64) b := make([]byte, length)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
panic(err) panic(err)
} }
@@ -30,9 +30,9 @@ type NextMsg struct {
Next string `json:"nextaction"` Next string `json:"nextaction"`
} }
func SendEmail(title string, body string, recipient string) { func SendEmail(title string, body string, recipients []string) {
//TODO //TODO
fmt.Println("Send", title, body, "to", recipient) fmt.Println("Send", title, body, "to", recipients)
} }
func ParseJwt(tokenStr string, hmac []byte) (jwt.MapClaims, error) { func ParseJwt(tokenStr string, hmac []byte) (jwt.MapClaims, error) {