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:
4
conf.json
Normal file
4
conf.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"AllowFreshAdminGeneration": true,
|
||||||
|
"AdminEmails": ["admin@admin.invalid"]
|
||||||
|
}
|
||||||
27
config/config.go
Normal file
27
config/config.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
20
main.go
@@ -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())
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user