mirror of https://github.com/gophish/gophish
Initial Implementation of a Password Policy (#1867)
This PR adds the initial work to implement a password policy as defined in #1538. Specifically, this implements the following * Rate limiting for the login handler * Implementing the ability for system admins to require a user to reset their password * Implementing a password policy that requires passwords to be a minimum of 8 characters * Removes the default password (gophish) for admin users to instead have the password randomly generated when Gophish first starts up * Adds a password strength meter when choosing a new password Fixes #1538pull/1883/head
parent
0f6439de5a
commit
bb7de8df3e
124
auth/auth.go
124
auth/auth.go
|
@ -1,69 +1,103 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// MinPasswordLength is the minimum number of characters required in a password
|
||||
const MinPasswordLength = 8
|
||||
|
||||
// APIKeyLength is the length of Gophish API keys
|
||||
const APIKeyLength = 32
|
||||
|
||||
// ErrInvalidPassword is thrown when a user provides an incorrect password.
|
||||
var ErrInvalidPassword = errors.New("Invalid Password")
|
||||
|
||||
// ErrPasswordMismatch is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrPasswordMismatch = errors.New("Password cannot be blank")
|
||||
// ErrPasswordMismatch is thrown when a user provides a mismatching password
|
||||
// and confirmation password.
|
||||
var ErrPasswordMismatch = errors.New("Passwords do not match")
|
||||
|
||||
// ErrReusedPassword is thrown when a user attempts to change their password to
|
||||
// the existing password
|
||||
var ErrReusedPassword = errors.New("Cannot reuse existing password")
|
||||
|
||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// Login attempts to login the user given a request.
|
||||
func Login(r *http.Request) (bool, models.User, error) {
|
||||
username, password := r.FormValue("username"), r.FormValue("password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
return false, models.User{}, err
|
||||
}
|
||||
//If we've made it here, we should have a valid user stored in u
|
||||
//Let's check the password
|
||||
err = bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(password))
|
||||
if err != nil {
|
||||
return false, models.User{}, ErrInvalidPassword
|
||||
}
|
||||
return true, u, nil
|
||||
// ErrPasswordTooShort is thrown when a user provides a password that is less
|
||||
// than MinPasswordLength
|
||||
var ErrPasswordTooShort = fmt.Errorf("Password must be at least %d characters", MinPasswordLength)
|
||||
|
||||
// GenerateSecureKey returns the hex representation of key generated from n
|
||||
// random bytes
|
||||
func GenerateSecureKey(n int) string {
|
||||
k := make([]byte, n)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// ChangePassword verifies the current password provided in the request and,
|
||||
// if it's valid, changes the password for the authenticated user.
|
||||
func ChangePassword(r *http.Request) error {
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
currentPw := r.FormValue("current_password")
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_new_password")
|
||||
// Check the current password
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.Hash), []byte(currentPw))
|
||||
// GeneratePasswordHash returns the bcrypt hash for the provided password using
|
||||
// the default bcrypt cost.
|
||||
func GeneratePasswordHash(password string) (string, error) {
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return ErrInvalidPassword
|
||||
return "", err
|
||||
}
|
||||
// Check that the new password isn't blank
|
||||
if newPassword == "" {
|
||||
return string(h), nil
|
||||
}
|
||||
|
||||
// CheckPasswordPolicy ensures the provided password is valid according to our
|
||||
// password policy.
|
||||
//
|
||||
// The current password policy is simply a minimum of 8 characters, though this
|
||||
// may change in the future (see #1538).
|
||||
func CheckPasswordPolicy(password string) error {
|
||||
switch {
|
||||
// Admittedly, empty passwords are a subset of too short passwords, but it
|
||||
// helps to provide a more specific error message
|
||||
case password == "":
|
||||
return ErrEmptyPassword
|
||||
}
|
||||
// Check that new passwords match
|
||||
if newPassword != confirmPassword {
|
||||
return ErrPasswordMismatch
|
||||
}
|
||||
// Generate the new hash
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Hash = string(h)
|
||||
if err = models.PutUser(&u); err != nil {
|
||||
return err
|
||||
case len(password) < MinPasswordLength:
|
||||
return ErrPasswordTooShort
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePassword validates that the provided password matches the provided
|
||||
// bcrypt hash.
|
||||
func ValidatePassword(password string, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
|
||||
// ValidatePasswordChange validates that the new password matches the
|
||||
// configured password policy, that the new password and confirmation
|
||||
// password match.
|
||||
//
|
||||
// Note that this assumes the current password has been confirmed by the
|
||||
// caller.
|
||||
//
|
||||
// If all of the provided data is valid, then the hash of the new password is
|
||||
// returned.
|
||||
func ValidatePasswordChange(currentHash, newPassword, confirmPassword string) (string, error) {
|
||||
// Ensure the new password passes our password policy
|
||||
if err := CheckPasswordPolicy(newPassword); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Check that new passwords match
|
||||
if newPassword != confirmPassword {
|
||||
return "", ErrPasswordMismatch
|
||||
}
|
||||
// Make sure that the new password isn't the same as the old one
|
||||
err := ValidatePassword(newPassword, currentHash)
|
||||
if err == nil {
|
||||
return "", ErrReusedPassword
|
||||
}
|
||||
// Generate the new hash
|
||||
return GeneratePasswordHash(newPassword)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPasswordPolicy(t *testing.T) {
|
||||
candidate := "short"
|
||||
got := CheckPasswordPolicy(candidate)
|
||||
if got != ErrPasswordTooShort {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", ErrPasswordTooShort, got)
|
||||
}
|
||||
|
||||
candidate = "valid password"
|
||||
got = CheckPasswordPolicy(candidate)
|
||||
if got != nil {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", nil, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePasswordChange(t *testing.T) {
|
||||
newPassword := "valid password"
|
||||
confirmPassword := "invalid"
|
||||
currentPassword := "current password"
|
||||
currentHash, err := GeneratePasswordHash(currentPassword)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating password hash: %v", err)
|
||||
}
|
||||
|
||||
_, got := ValidatePasswordChange(currentHash, newPassword, confirmPassword)
|
||||
if got != ErrPasswordMismatch {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", ErrPasswordMismatch, got)
|
||||
}
|
||||
|
||||
newPassword = currentPassword
|
||||
confirmPassword = newPassword
|
||||
_, got = ValidatePasswordChange(currentHash, newPassword, confirmPassword)
|
||||
if got != ErrReusedPassword {
|
||||
t.Fatalf("unexpected error received. expected %v got %v", ErrReusedPassword, got)
|
||||
}
|
||||
}
|
|
@ -3,9 +3,9 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
)
|
||||
|
||||
// Reset (/api/reset) resets the currently authenticated user's API key
|
||||
|
@ -13,7 +13,7 @@ func (as *Server) Reset(w http.ResponseWriter, r *http.Request) {
|
|||
switch {
|
||||
case r.Method == "POST":
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
u.ApiKey = util.GenerateSecureKey()
|
||||
u.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
|
||||
err := models.PutUser(&u)
|
||||
if err != nil {
|
||||
http.Error(w, "Error setting API Key", http.StatusInternalServerError)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/middleware/ratelimit"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/worker"
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -19,14 +20,17 @@ type ServerOption func(*Server)
|
|||
type Server struct {
|
||||
handler http.Handler
|
||||
worker worker.Worker
|
||||
limiter *ratelimit.PostLimiter
|
||||
}
|
||||
|
||||
// NewServer returns a new instance of the API handler with the provided
|
||||
// options applied.
|
||||
func NewServer(options ...ServerOption) *Server {
|
||||
defaultWorker, _ := worker.New()
|
||||
defaultLimiter := ratelimit.NewPostLimiter()
|
||||
as := &Server{
|
||||
worker: defaultWorker,
|
||||
limiter: defaultLimiter,
|
||||
}
|
||||
for _, opt := range options {
|
||||
opt(as)
|
||||
|
@ -42,6 +46,12 @@ func WithWorker(w worker.Worker) ServerOption {
|
|||
}
|
||||
}
|
||||
|
||||
func WithLimiter(limiter *ratelimit.PostLimiter) ServerOption {
|
||||
return func(as *Server) {
|
||||
as.limiter = limiter
|
||||
}
|
||||
}
|
||||
|
||||
func (as *Server) registerRoutes() {
|
||||
root := mux.NewRouter()
|
||||
root = root.StrictSlash(true)
|
||||
|
|
|
@ -6,18 +6,14 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
ctx "github.com/gophish/gophish/context"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// ErrEmptyPassword is thrown when a user provides a blank password to the register
|
||||
// or change password functions
|
||||
var ErrEmptyPassword = errors.New("No password provided")
|
||||
|
||||
// ErrUsernameTaken is thrown when a user attempts to register a username that is taken.
|
||||
var ErrUsernameTaken = errors.New("Username already taken")
|
||||
|
||||
|
@ -36,6 +32,7 @@ type userRequest struct {
|
|||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role string `json:"role"`
|
||||
PasswordChangeRequired bool `json:"password_change_required"`
|
||||
}
|
||||
|
||||
func (ur *userRequest) Validate(existingUser *models.User) error {
|
||||
|
@ -89,11 +86,12 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
|||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if ur.Password == "" {
|
||||
JSONResponse(w, models.Response{Success: false, Message: ErrEmptyPassword.Error()}, http.StatusBadRequest)
|
||||
err = auth.CheckPasswordPolicy(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
hash, err := auth.GeneratePasswordHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
|
@ -106,7 +104,7 @@ func (as *Server) Users(w http.ResponseWriter, r *http.Request) {
|
|||
user := models.User{
|
||||
Username: ur.Username,
|
||||
Hash: hash,
|
||||
ApiKey: util.GenerateSecureKey(),
|
||||
ApiKey: auth.GenerateSecureKey(auth.APIKeyLength),
|
||||
Role: role,
|
||||
RoleID: role.ID,
|
||||
}
|
||||
|
@ -195,13 +193,20 @@ func (as *Server) User(w http.ResponseWriter, r *http.Request) {
|
|||
// We don't force the password to be provided, since it may be an admin
|
||||
// managing the user's account, and making a simple change like
|
||||
// updating the username or role. However, if it _is_ provided, we'll
|
||||
// update the stored hash.
|
||||
// update the stored hash after validating the new password meets our
|
||||
// password policy.
|
||||
//
|
||||
// Note that we don't force the current password to be provided. The
|
||||
// assumption here is that the API key is a proper bearer token proving
|
||||
// authenticated access to the account.
|
||||
existingUser.PasswordChangeRequired = ur.PasswordChangeRequired
|
||||
if ur.Password != "" {
|
||||
hash, err := util.NewHash(ur.Password)
|
||||
err = auth.CheckPasswordPolicy(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
hash, err := auth.GeneratePasswordHash(ur.Password)
|
||||
if err != nil {
|
||||
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
|
||||
return
|
||||
|
|
|
@ -66,7 +66,7 @@ func TestCreateUser(t *testing.T) {
|
|||
testCtx := setupTest(t)
|
||||
payload := &userRequest{
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
Password: "validpassword",
|
||||
Role: models.RoleUser,
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
"github.com/gophish/gophish/models"
|
||||
)
|
||||
|
@ -41,6 +42,10 @@ func setupTest(t *testing.T) *testContext {
|
|||
ctx.adminServer.Start()
|
||||
// Get the API key to use for these tests
|
||||
u, err := models.GetUser(1)
|
||||
// Reset the temporary password for the admin user to a value we control
|
||||
hash, err := auth.GeneratePasswordHash("gophish")
|
||||
u.Hash = hash
|
||||
models.PutUser(&u)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting first user from database: %v", err)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/gophish/gophish/controllers/api"
|
||||
log "github.com/gophish/gophish/logger"
|
||||
mid "github.com/gophish/gophish/middleware"
|
||||
"github.com/gophish/gophish/middleware/ratelimit"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/gophish/gophish/util"
|
||||
"github.com/gophish/gophish/worker"
|
||||
|
@ -36,6 +37,7 @@ type AdminServer struct {
|
|||
server *http.Server
|
||||
worker worker.Worker
|
||||
config config.AdminServer
|
||||
limiter *ratelimit.PostLimiter
|
||||
}
|
||||
|
||||
var defaultTLSConfig = &tls.Config{
|
||||
|
@ -74,9 +76,11 @@ func NewAdminServer(config config.AdminServer, options ...AdminServerOption) *Ad
|
|||
ReadTimeout: 10 * time.Second,
|
||||
Addr: config.ListenURL,
|
||||
}
|
||||
defaultLimiter := ratelimit.NewPostLimiter()
|
||||
as := &AdminServer{
|
||||
worker: defaultWorker,
|
||||
server: defaultServer,
|
||||
limiter: defaultLimiter,
|
||||
config: config,
|
||||
}
|
||||
for _, opt := range options {
|
||||
|
@ -119,8 +123,9 @@ func (as *AdminServer) registerRoutes() {
|
|||
router := mux.NewRouter()
|
||||
// Base Front-end routes
|
||||
router.HandleFunc("/", mid.Use(as.Base, mid.RequireLogin))
|
||||
router.HandleFunc("/login", as.Login)
|
||||
router.HandleFunc("/login", mid.Use(as.Login, as.limiter.Limit))
|
||||
router.HandleFunc("/logout", mid.Use(as.Logout, mid.RequireLogin))
|
||||
router.HandleFunc("/reset_password", mid.Use(as.ResetPassword, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns", mid.Use(as.Campaigns, mid.RequireLogin))
|
||||
router.HandleFunc("/campaigns/{id:[0-9]+}", mid.Use(as.CampaignID, mid.RequireLogin))
|
||||
router.HandleFunc("/templates", mid.Use(as.Templates, mid.RequireLogin))
|
||||
|
@ -132,7 +137,10 @@ func (as *AdminServer) registerRoutes() {
|
|||
router.HandleFunc("/webhooks", mid.Use(as.Webhooks, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
router.HandleFunc("/impersonate", mid.Use(as.Impersonate, mid.RequirePermission(models.PermissionModifySystem), mid.RequireLogin))
|
||||
// Create the API routes
|
||||
api := api.NewServer(api.WithWorker(as.worker))
|
||||
api := api.NewServer(
|
||||
api.WithWorker(as.worker),
|
||||
api.WithLimiter(as.limiter),
|
||||
)
|
||||
router.PathPrefix("/api/").Handler(api)
|
||||
|
||||
// Setup static file serving
|
||||
|
@ -141,7 +149,7 @@ func (as *AdminServer) registerRoutes() {
|
|||
// Setup CSRF Protection
|
||||
csrfKey := []byte(as.config.CSRFKey)
|
||||
if len(csrfKey) == 0 {
|
||||
csrfKey = []byte(util.GenerateSecureKey())
|
||||
csrfKey = []byte(auth.GenerateSecureKey(auth.APIKeyLength))
|
||||
}
|
||||
csrfHandler := csrf.Protect(csrfKey,
|
||||
csrf.FieldName("csrf_token"),
|
||||
|
@ -171,12 +179,14 @@ type templateParams struct {
|
|||
// the CSRF token.
|
||||
func newTemplateParams(r *http.Request) templateParams {
|
||||
user := ctx.Get(r, "user").(models.User)
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
modifySystem, _ := user.HasPermission(models.PermissionModifySystem)
|
||||
return templateParams{
|
||||
Token: csrf.Token(r),
|
||||
User: user,
|
||||
ModifySystem: modifySystem,
|
||||
Version: config.Version,
|
||||
Flashes: session.Flashes(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,22 +245,37 @@ func (as *AdminServer) Settings(w http.ResponseWriter, r *http.Request) {
|
|||
case r.Method == "GET":
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Settings"
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
session.Save(r, w)
|
||||
getTemplate(w, "settings").ExecuteTemplate(w, "base", params)
|
||||
case r.Method == "POST":
|
||||
err := auth.ChangePassword(r)
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
currentPw := r.FormValue("current_password")
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_new_password")
|
||||
// Check the current password
|
||||
err := auth.ValidatePassword(currentPw, u.Hash)
|
||||
msg := models.Response{Success: true, Message: "Settings Updated Successfully"}
|
||||
if err == auth.ErrInvalidPassword {
|
||||
msg.Message = "Invalid Password"
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword)
|
||||
if err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
u.Hash = string(newHash)
|
||||
if err = models.PutUser(&u); err != nil {
|
||||
msg.Message = err.Error()
|
||||
msg.Success = false
|
||||
api.JSONResponse(w, msg, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
api.JSONResponse(w, msg, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
@ -263,6 +288,39 @@ func (as *AdminServer) UserManagement(w http.ResponseWriter, r *http.Request) {
|
|||
getTemplate(w, "users").ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
func (as *AdminServer) nextOrIndex(w http.ResponseWriter, r *http.Request) {
|
||||
next := "/"
|
||||
url, err := url.Parse(r.FormValue("next"))
|
||||
if err == nil {
|
||||
path := url.Path
|
||||
if path != "" {
|
||||
next = path
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, next, 302)
|
||||
}
|
||||
|
||||
func (as *AdminServer) handleInvalidLogin(w http.ResponseWriter, r *http.Request) {
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
Flash(w, r, "danger", "Invalid Username/Password")
|
||||
params := struct {
|
||||
User models.User
|
||||
Title string
|
||||
Flashes []interface{}
|
||||
Token string
|
||||
}{Title: "Login", Token: csrf.Token(r)}
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/login.html", "templates/flashes.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
// w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
|
||||
// Webhooks is an admin-only handler that handles webhooks
|
||||
func (as *AdminServer) Webhooks(w http.ResponseWriter, r *http.Request) {
|
||||
params := newTemplateParams(r)
|
||||
|
@ -309,37 +367,25 @@ func (as *AdminServer) Login(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
case r.Method == "POST":
|
||||
//Attempt to login
|
||||
succ, u, err := auth.Login(r)
|
||||
// Find the user with the provided username
|
||||
username, password := r.FormValue("username"), r.FormValue("password")
|
||||
u, err := models.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
as.handleInvalidLogin(w, r)
|
||||
return
|
||||
}
|
||||
// Validate the user's password
|
||||
err = auth.ValidatePassword(password, u.Hash)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
as.handleInvalidLogin(w, r)
|
||||
return
|
||||
}
|
||||
// If we've logged in, save the session and redirect to the dashboard
|
||||
if succ {
|
||||
session.Values["id"] = u.Id
|
||||
session.Save(r, w)
|
||||
next := "/"
|
||||
url, err := url.Parse(r.FormValue("next"))
|
||||
if err == nil {
|
||||
path := url.Path
|
||||
if path != "" {
|
||||
next = path
|
||||
}
|
||||
}
|
||||
http.Redirect(w, r, next, http.StatusFound)
|
||||
} else {
|
||||
Flash(w, r, "danger", "Invalid Username/Password")
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/login.html", "templates/flashes.html")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
template.Must(templates, err).ExecuteTemplate(w, "base", params)
|
||||
}
|
||||
as.nextOrIndex(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,6 +398,69 @@ func (as *AdminServer) Logout(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
}
|
||||
|
||||
// ResetPassword handles the password reset flow when a password change is
|
||||
// required either by the Gophish system or an administrator.
|
||||
//
|
||||
// This handler is meant to be used when a user is required to reset their
|
||||
// password, not just when they want to.
|
||||
//
|
||||
// This is an important distinction since in this handler we don't require
|
||||
// the user to re-enter their current password, as opposed to the flow
|
||||
// through the settings handler.
|
||||
//
|
||||
// To that end, if the user doesn't require a password change, we will
|
||||
// redirect them to the settings page.
|
||||
func (as *AdminServer) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
u := ctx.Get(r, "user").(models.User)
|
||||
session := ctx.Get(r, "session").(*sessions.Session)
|
||||
if !u.PasswordChangeRequired {
|
||||
Flash(w, r, "info", "Please reset your password through the settings page")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/settings", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
params := newTemplateParams(r)
|
||||
params.Title = "Reset Password"
|
||||
switch {
|
||||
case r.Method == http.MethodGet:
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
|
||||
return
|
||||
case r.Method == http.MethodPost:
|
||||
newPassword := r.FormValue("password")
|
||||
confirmPassword := r.FormValue("confirm_password")
|
||||
newHash, err := auth.ValidatePasswordChange(u.Hash, newPassword, confirmPassword)
|
||||
if err != nil {
|
||||
Flash(w, r, "danger", err.Error())
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
|
||||
return
|
||||
}
|
||||
u.PasswordChangeRequired = false
|
||||
u.Hash = newHash
|
||||
if err = models.PutUser(&u); err != nil {
|
||||
Flash(w, r, "danger", err.Error())
|
||||
params.Flashes = session.Flashes()
|
||||
session.Save(r, w)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
getTemplate(w, "reset_password").ExecuteTemplate(w, "base", params)
|
||||
return
|
||||
}
|
||||
// TODO: We probably want to flash a message here that the password was
|
||||
// changed successfully. The problem is that when the user resets their
|
||||
// password on first use, they will see two flashes on the dashboard-
|
||||
// one for their password reset, and one for the "no campaigns created".
|
||||
//
|
||||
// The solution to this is to revamp the empty page to be more useful,
|
||||
// like a wizard or something.
|
||||
as.nextOrIndex(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make this execute the template, too
|
||||
func getTemplate(w http.ResponseWriter, tmpl string) *template.Template {
|
||||
templates := template.New("template")
|
||||
_, err := templates.ParseFiles("templates/base.html", "templates/nav.html", "templates/"+tmpl+".html", "templates/flashes.html")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE `users` ADD COLUMN password_change_required BOOLEAN;
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
ALTER TABLE users ADD COLUMN password_change_required BOOLEAN;
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
1
go.mod
1
go.mod
|
@ -27,6 +27,7 @@ require (
|
|||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
|
||||
|
|
2
go.sum
2
go.sum
|
@ -100,6 +100,8 @@ golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
|
|
|
@ -114,6 +114,15 @@ func RequireAPIKey(handler http.Handler) http.Handler {
|
|||
func RequireLogin(handler http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if u := ctx.Get(r, "user"); u != nil {
|
||||
// If a password change is required for the user, then redirect them
|
||||
// to the login page
|
||||
currentUser := u.(models.User)
|
||||
if currentUser.PasswordChangeRequired && r.URL.Path != "/reset_password" {
|
||||
q := r.URL.Query()
|
||||
q.Set("next", r.URL.Path)
|
||||
http.Redirect(w, r, fmt.Sprintf("/reset_password?%s", q.Encode()), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -162,3 +162,22 @@ func TestBearerToken(t *testing.T) {
|
|||
t.Fatalf("incorrect status code received. expected %d got %d", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordResetRequired(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req = ctx.Set(req, "user", models.User{
|
||||
PasswordChangeRequired: true,
|
||||
})
|
||||
response := httptest.NewRecorder()
|
||||
RequireLogin(successHandler).ServeHTTP(response, req)
|
||||
gotStatus := response.Code
|
||||
expectedStatus := http.StatusTemporaryRedirect
|
||||
if gotStatus != expectedStatus {
|
||||
t.Fatalf("incorrect status code received. expected %d got %d", expectedStatus, gotStatus)
|
||||
}
|
||||
expectedLocation := "/reset_password?next=%2F"
|
||||
gotLocation := response.Header().Get("Location")
|
||||
if gotLocation != expectedLocation {
|
||||
t.Fatalf("incorrect location header received. expected %s got %s", expectedLocation, gotLocation)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
// Package ratelimit provides a simple token-bucket rate limiting middleware
|
||||
// which only allows n POST requests every minute. This is meant to be used on
|
||||
// login handlers or other sensitive transactions which should be throttled to
|
||||
// prevent abuse.
|
||||
//
|
||||
// Tracked clients are stored in a locked map, with a goroutine that runs at a
|
||||
// configurable interval to clean up stale entries.
|
||||
//
|
||||
// Note that there is no enforcement for GET requests. This is an effort to be
|
||||
// opinionated in order to hit the most common use-cases. For more advanced
|
||||
// use-cases, you may consider the `github.com/didip/tollbooth` package.
|
||||
//
|
||||
// The enforcement mechanism is based on the blog post here:
|
||||
// https://www.alexedwards.net/blog/how-to-rate-limit-http-requests
|
||||
package ratelimit
|
|
@ -0,0 +1,145 @@
|
|||
package ratelimit
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// DefaultRequestsPerMinute is the number of requests to allow per minute.
|
||||
// Any requests over this interval will return a HTTP 429 error.
|
||||
const DefaultRequestsPerMinute = 5
|
||||
|
||||
// DefaultCleanupInterval determines how frequently the cleanup routine
|
||||
// executes.
|
||||
const DefaultCleanupInterval = 1 * time.Minute
|
||||
|
||||
// DefaultExpiry is the amount of time to track a bucket for a particular
|
||||
// visitor.
|
||||
const DefaultExpiry = 10 * time.Minute
|
||||
|
||||
type bucket struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
// PostLimiter is a simple rate limiting middleware which only allows n POST
|
||||
// requests per minute.
|
||||
type PostLimiter struct {
|
||||
visitors map[string]*bucket
|
||||
requestLimit int
|
||||
cleanupInterval time.Duration
|
||||
expiry time.Duration
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// PostLimiterOption is a functional option that allows callers to configure
|
||||
// the rate limiter.
|
||||
type PostLimiterOption func(*PostLimiter)
|
||||
|
||||
// WithRequestsPerMinute sets the number of requests to allow per minute.
|
||||
func WithRequestsPerMinute(requestLimit int) PostLimiterOption {
|
||||
return func(p *PostLimiter) {
|
||||
p.requestLimit = requestLimit
|
||||
}
|
||||
}
|
||||
|
||||
// WithCleanupInterval sets the interval between cleaning up stale entries in
|
||||
// the rate limit client list
|
||||
func WithCleanupInterval(interval time.Duration) PostLimiterOption {
|
||||
return func(p *PostLimiter) {
|
||||
p.cleanupInterval = interval
|
||||
}
|
||||
}
|
||||
|
||||
// WithExpiry sets the amount of time to store client entries before they are
|
||||
// considered stale.
|
||||
func WithExpiry(expiry time.Duration) PostLimiterOption {
|
||||
return func(p *PostLimiter) {
|
||||
p.expiry = expiry
|
||||
}
|
||||
}
|
||||
|
||||
// NewPostLimiter returns a new instance of a PostLimiter
|
||||
func NewPostLimiter(opts ...PostLimiterOption) *PostLimiter {
|
||||
limiter := &PostLimiter{
|
||||
visitors: make(map[string]*bucket),
|
||||
requestLimit: DefaultRequestsPerMinute,
|
||||
cleanupInterval: DefaultCleanupInterval,
|
||||
expiry: DefaultExpiry,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(limiter)
|
||||
}
|
||||
go limiter.pollCleanup()
|
||||
return limiter
|
||||
}
|
||||
|
||||
func (limiter *PostLimiter) pollCleanup() {
|
||||
ticker := time.NewTicker(time.Duration(limiter.cleanupInterval) * time.Second)
|
||||
for range ticker.C {
|
||||
limiter.Cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup removes any buckets that were last seen past the configured expiry.
|
||||
func (limiter *PostLimiter) Cleanup() {
|
||||
limiter.Lock()
|
||||
defer limiter.Unlock()
|
||||
for ip, bucket := range limiter.visitors {
|
||||
if time.Now().Sub(bucket.lastSeen) >= limiter.expiry {
|
||||
delete(limiter.visitors, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (limiter *PostLimiter) addBucket(ip string) *bucket {
|
||||
limiter.Lock()
|
||||
defer limiter.Unlock()
|
||||
limit := rate.NewLimiter(rate.Every(time.Minute/time.Duration(limiter.requestLimit)), limiter.requestLimit)
|
||||
b := &bucket{
|
||||
limiter: limit,
|
||||
}
|
||||
limiter.visitors[ip] = b
|
||||
return b
|
||||
}
|
||||
|
||||
func (limiter *PostLimiter) allow(ip string) bool {
|
||||
// Check if we have a limiter already active for this clientIP
|
||||
limiter.RLock()
|
||||
bucket, exists := limiter.visitors[ip]
|
||||
limiter.RUnlock()
|
||||
if !exists {
|
||||
bucket = limiter.addBucket(ip)
|
||||
}
|
||||
// Update the lastSeen for this bucket to assist with cleanup
|
||||
limiter.Lock()
|
||||
defer limiter.Unlock()
|
||||
bucket.lastSeen = time.Now()
|
||||
return bucket.limiter.Allow()
|
||||
}
|
||||
|
||||
// Limit enforces the configured rate limit for POST requests.
|
||||
//
|
||||
// TODO: Change the return value to an http.Handler when we clean up the
|
||||
// way Gophish routing is done.
|
||||
func (limiter *PostLimiter) Limit(next http.Handler) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to determine client IP address: %v", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && !limiter.allow(clientIP) {
|
||||
log.Error("")
|
||||
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package ratelimit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var successHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
func reachLimit(t *testing.T, handler http.Handler, limit int) {
|
||||
// Make `expected` requests and ensure that each return a successful
|
||||
// response.
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.RemoteAddr = "127.0.0.1:"
|
||||
for i := 0; i < limit; i++ {
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("no 200 on req %d got %d", i, w.Code)
|
||||
}
|
||||
}
|
||||
// Then, makes another request to ensure it returns the 429
|
||||
// status.
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusTooManyRequests {
|
||||
t.Fatalf("no 429")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitEnforcement(t *testing.T) {
|
||||
expectedLimit := 3
|
||||
limiter := NewPostLimiter(WithRequestsPerMinute(expectedLimit))
|
||||
handler := limiter.Limit(successHandler)
|
||||
reachLimit(t, handler, expectedLimit)
|
||||
}
|
||||
|
||||
func TestRateLimitCleanup(t *testing.T) {
|
||||
expectedLimit := 3
|
||||
limiter := NewPostLimiter(WithRequestsPerMinute(expectedLimit))
|
||||
handler := limiter.Limit(successHandler)
|
||||
reachLimit(t, handler, expectedLimit)
|
||||
|
||||
// Set the timeout to be
|
||||
bucket, exists := limiter.visitors["127.0.0.1"]
|
||||
if !exists {
|
||||
t.Fatalf("doesn't exist for some reason")
|
||||
}
|
||||
bucket.lastSeen = bucket.lastSeen.Add(-limiter.expiry)
|
||||
limiter.Cleanup()
|
||||
_, exists = limiter.visitors["127.0.0.1"]
|
||||
if exists {
|
||||
t.Fatalf("exists for some reason")
|
||||
}
|
||||
reachLimit(t, handler, expectedLimit)
|
||||
}
|
|
@ -12,7 +12,9 @@ import (
|
|||
"bitbucket.org/liamstask/goose/lib/goose"
|
||||
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
"github.com/gophish/gophish/auth"
|
||||
"github.com/gophish/gophish/config"
|
||||
|
||||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/mattn/go-sqlite3" // Blank import needed to import sqlite3
|
||||
|
@ -23,6 +25,9 @@ var conf *config.Config
|
|||
|
||||
const MaxDatabaseConnectionAttempts int = 10
|
||||
|
||||
// DefaultAdminUsername is the default username for the administrative user
|
||||
const DefaultAdminUsername = "admin"
|
||||
|
||||
const (
|
||||
CampaignInProgress string = "In progress"
|
||||
CampaignQueued string = "Queued"
|
||||
|
@ -82,8 +87,33 @@ func chooseDBDriver(name, openStr string) goose.DBDriver {
|
|||
return d
|
||||
}
|
||||
|
||||
// Setup initializes the Conn object
|
||||
// It also populates the Gophish Config object
|
||||
func createTemporaryPassword(u *User) error {
|
||||
// This will result in a 16 character password which could be viewed as an
|
||||
// inconvenience, but it should be ok for now.
|
||||
temporaryPassword := auth.GenerateSecureKey(auth.MinPasswordLength)
|
||||
hash, err := auth.GeneratePasswordHash(temporaryPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Hash = hash
|
||||
// Anytime a temporary password is created, we will force the user
|
||||
// to change their password
|
||||
u.PasswordChangeRequired = true
|
||||
err = db.Save(u).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Please login with the username admin and the password %s", temporaryPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup initializes the database and runs any needed migrations.
|
||||
//
|
||||
// First, it establishes a connection to the database, then runs any migrations
|
||||
// newer than the version the database is on.
|
||||
//
|
||||
// Once the database is up-to-date, we create an admin user (if needed) that
|
||||
// has a randomly generated API key and password.
|
||||
func Setup(c *config.Config) error {
|
||||
// Setup the package-scoped config
|
||||
conf = c
|
||||
|
@ -153,6 +183,7 @@ func Setup(c *config.Config) error {
|
|||
}
|
||||
// Create the admin user if it doesn't exist
|
||||
var userCount int64
|
||||
var adminUser User
|
||||
db.Model(&User{}).Count(&userCount)
|
||||
adminRole, err := GetRoleBySlug(RoleAdmin)
|
||||
if err != nil {
|
||||
|
@ -160,14 +191,38 @@ func Setup(c *config.Config) error {
|
|||
return err
|
||||
}
|
||||
if userCount == 0 {
|
||||
initUser := User{
|
||||
Username: "admin",
|
||||
Hash: "$2a$10$IYkPp0.QsM81lYYPrQx6W.U6oQGw7wMpozrKhKAHUBVL4mkm/EvAS", //gophish
|
||||
adminUser := User{
|
||||
Username: DefaultAdminUsername,
|
||||
Role: adminRole,
|
||||
RoleID: adminRole.ID,
|
||||
PasswordChangeRequired: true,
|
||||
}
|
||||
initUser.ApiKey = generateSecureKey()
|
||||
err = db.Save(&initUser).Error
|
||||
adminUser.ApiKey = auth.GenerateSecureKey(auth.APIKeyLength)
|
||||
err = db.Save(&adminUser).Error
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
// If this is the first time the user is installing Gophish, then we will
|
||||
// generate a temporary password for the admin user.
|
||||
//
|
||||
// We do this here instead of in the block above where the admin is created
|
||||
// since there's the chance the user executes Gophish and has some kind of
|
||||
// error, then tries restarting it. If they didn't grab the password out of
|
||||
// the logs, then they would have lost it.
|
||||
//
|
||||
// By doing the temporary password here, we will regenerate that temporary
|
||||
// password until the user is able to reset the admin password.
|
||||
if adminUser.Username == "" {
|
||||
adminUser, err = GetUserByUsername(DefaultAdminUsername)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if adminUser.PasswordChangeRequired {
|
||||
err = createTemporaryPassword(&adminUser)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
|
|
|
@ -19,6 +19,7 @@ type User struct {
|
|||
ApiKey string `json:"api_key" sql:"not null;unique"`
|
||||
Role Role `json:"role" gorm:"association_autoupdate:false;association_autocreate:false"`
|
||||
RoleID int64 `json:"-"`
|
||||
PasswordChangeRequired bool `json:"password_change_required"`
|
||||
}
|
||||
|
||||
// GetUser returns the user that the given id corresponds to. If no user is found, an
|
||||
|
|
|
@ -29,5 +29,8 @@
|
|||
"jshint-stylish": "^2.2.1",
|
||||
"webpack": "^4.32.2",
|
||||
"webpack-cli": "^3.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"zxcvbn": "^4.4.2"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -730,3 +730,15 @@ table.dataTable {
|
|||
.cke_autocomplete_panel>li {
|
||||
padding: 10px 5px !important;
|
||||
}
|
||||
|
||||
#password-strength {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 0px;
|
||||
height: 8px;
|
||||
}
|
||||
#password-strength-description {
|
||||
font-size: 12px;
|
||||
}
|
||||
#password-strength-container {
|
||||
height: 40px;
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var a=t[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(o,a,function(t){return e[t]}.bind(null,a));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=1)}([,function(e,t){var n=[],o=function(){$("#name").val(""),$("#url").val(""),$("#secret").val(""),$("#is_active").prop("checked",!1),$("#flashes").empty()},a=function(){$("#webhookTable").hide(),$("#loading").show(),api.webhooks.get().success(function(e){n=e,$("#loading").hide(),$("#webhookTable").show();var t=$("#webhookTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(n,function(e,n){t.row.add([escapeHtml(n.name),escapeHtml(n.url),escapeHtml(n.is_active),'\n <div class="pull-right">\n <button class="btn btn-primary ping_button" data-webhook-id="'.concat(n.id,'">\n Ping\n </button>\n <button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-pencil"></i>\n </button>\n <button class="btn btn-danger delete_button" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-trash-o"></i>\n </button>\n </div>\n ')]).draw()})}).error(function(){errorFlash("Error fetching webhooks")})},c=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){var t={name:$("#name").val(),url:$("#url").val(),secret:$("#secret").val(),is_active:$("#is_active").is(":checked")};-1!=e?(t.id=parseInt(e),api.webhookId.put(t).success(function(e){o(),a(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been updated successfully!'))}).error(function(e){modalError(e.responseJSON.message)})):api.webhooks.post(t).success(function(e){a(),o(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been created successfully!'))}).error(function(e){modalError(e.responseJSON.message)})}(e)}),-1!==e&&api.webhookId.get(e).success(function(e){$("#name").val(e.name),$("#url").val(e.url),$("#secret").val(e.secret),$("#is_active").prop("checked",e.is_active)}).error(function(){errorFlash("Error fetching webhook")})};$(document).ready(function(){a(),$("#modal").on("hide.bs.modal",function(){o()}),$("#new_button").on("click",function(){c(-1)}),$("#webhookTable").on("click",".edit_button",function(e){c($(this).attr("data-webhook-id"))}),$("#webhookTable").on("click",".delete_button",function(e){var t,o;t=$(this).attr("data-webhook-id"),(o=n.find(function(e){return e.id==t}))&&Swal.fire({title:"Are you sure?",text:"This will delete the webhook '".concat(escapeHtml(o.name),"'"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,n){api.webhookId.delete(t).success(function(t){e()}).error(function(e){n(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("Webhook Deleted!","The webhook has been deleted!","success"),$("button:contains('OK')").on("click",function(){location.reload()})})}),$("#webhookTable").on("click",".ping_button",function(e){var t,a;t=e.currentTarget,a=e.currentTarget.dataset.webhookId,o(),t.disabled=!0,api.webhookId.ping(a).success(function(e){t.disabled=!1,successFlash('Ping of "'.concat(escapeHtml(e.name),'" webhook succeeded.'))}).error(function(e){t.disabled=!1;var o=n.find(function(e){return e.id==a});o&&errorFlash('Ping of "'.concat(escapeHtml(o.name),'" webhook failed: "').concat(e.responseJSON.message,'"'))})})})}]);
|
||||
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var a=t[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,n),a.l=!0,a.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)n.d(o,a,function(t){return e[t]}.bind(null,a));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=9)}({9:function(e,t){var n=[],o=function(){$("#name").val(""),$("#url").val(""),$("#secret").val(""),$("#is_active").prop("checked",!1),$("#flashes").empty()},a=function(){$("#webhookTable").hide(),$("#loading").show(),api.webhooks.get().success(function(e){n=e,$("#loading").hide(),$("#webhookTable").show();var t=$("#webhookTable").DataTable({destroy:!0,columnDefs:[{orderable:!1,targets:"no-sort"}]});t.clear(),$.each(n,function(e,n){t.row.add([escapeHtml(n.name),escapeHtml(n.url),escapeHtml(n.is_active),'\n <div class="pull-right">\n <button class="btn btn-primary ping_button" data-webhook-id="'.concat(n.id,'">\n Ping\n </button>\n <button class="btn btn-primary edit_button" data-toggle="modal" data-backdrop="static" data-target="#modal" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-pencil"></i>\n </button>\n <button class="btn btn-danger delete_button" data-webhook-id="').concat(n.id,'">\n <i class="fa fa-trash-o"></i>\n </button>\n </div>\n ')]).draw()})}).error(function(){errorFlash("Error fetching webhooks")})},c=function(e){$("#modalSubmit").unbind("click").click(function(){!function(e){var t={name:$("#name").val(),url:$("#url").val(),secret:$("#secret").val(),is_active:$("#is_active").is(":checked")};-1!=e?(t.id=parseInt(e),api.webhookId.put(t).success(function(e){o(),a(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been updated successfully!'))}).error(function(e){modalError(e.responseJSON.message)})):api.webhooks.post(t).success(function(e){a(),o(),$("#modal").modal("hide"),successFlash('Webhook "'.concat(escapeHtml(t.name),'" has been created successfully!'))}).error(function(e){modalError(e.responseJSON.message)})}(e)}),-1!==e&&api.webhookId.get(e).success(function(e){$("#name").val(e.name),$("#url").val(e.url),$("#secret").val(e.secret),$("#is_active").prop("checked",e.is_active)}).error(function(){errorFlash("Error fetching webhook")})};$(document).ready(function(){a(),$("#modal").on("hide.bs.modal",function(){o()}),$("#new_button").on("click",function(){c(-1)}),$("#webhookTable").on("click",".edit_button",function(e){c($(this).attr("data-webhook-id"))}),$("#webhookTable").on("click",".delete_button",function(e){var t,o;t=$(this).attr("data-webhook-id"),(o=n.find(function(e){return e.id==t}))&&Swal.fire({title:"Are you sure?",text:"This will delete the webhook '".concat(escapeHtml(o.name),"'"),type:"warning",animation:!1,showCancelButton:!0,confirmButtonText:"Delete",confirmButtonColor:"#428bca",reverseButtons:!0,allowOutsideClick:!1,preConfirm:function(){return new Promise(function(e,n){api.webhookId.delete(t).success(function(t){e()}).error(function(e){n(e.responseJSON.message)})}).catch(function(e){Swal.showValidationMessage(e)})}}).then(function(e){e.value&&Swal.fire("Webhook Deleted!","The webhook has been deleted!","success"),$("button:contains('OK')").on("click",function(){location.reload()})})}),$("#webhookTable").on("click",".ping_button",function(e){var t,a;t=e.currentTarget,a=e.currentTarget.dataset.webhookId,o(),t.disabled=!0,api.webhookId.ping(a).success(function(e){t.disabled=!1,successFlash('Ping of "'.concat(escapeHtml(e.name),'" webhook succeeded.'))}).error(function(e){t.disabled=!1;var o=n.find(function(e){return e.id==a});o&&errorFlash('Ping of "'.concat(escapeHtml(o.name),'" webhook failed: "').concat(e.responseJSON.message,'"'))})})})}});
|
|
@ -0,0 +1,54 @@
|
|||
import zxcvbn from 'zxcvbn';
|
||||
|
||||
const StrengthMapping = {
|
||||
0: {
|
||||
class: 'danger',
|
||||
width: '10%',
|
||||
status: 'Very Weak'
|
||||
},
|
||||
1: {
|
||||
class: 'danger',
|
||||
width: '25%',
|
||||
status: 'Very Weak'
|
||||
},
|
||||
2: {
|
||||
class: 'warning',
|
||||
width: '50%',
|
||||
status: 'Weak'
|
||||
},
|
||||
3: {
|
||||
class: 'success',
|
||||
width: '75%',
|
||||
status: 'Good'
|
||||
},
|
||||
4: {
|
||||
class: 'success',
|
||||
width: '100%',
|
||||
status: 'Very Good'
|
||||
}
|
||||
}
|
||||
|
||||
const Progress = document.getElementById("password-strength-container")
|
||||
const ProgressBar = document.getElementById("password-strength-bar")
|
||||
const StrengthDescription = document.getElementById("password-strength-description")
|
||||
|
||||
const updatePasswordStrength = (e) => {
|
||||
const candidate = e.target.value
|
||||
// If there is no password, clear out the progress bar
|
||||
if (!candidate) {
|
||||
ProgressBar.style.width = 0
|
||||
StrengthDescription.textContent = ""
|
||||
Progress.classList.add("hidden")
|
||||
return
|
||||
}
|
||||
const score = zxcvbn(candidate).score
|
||||
const evaluation = StrengthMapping[score]
|
||||
// Update the progress bar
|
||||
ProgressBar.classList = `progress-bar progress-bar-${evaluation.class}`
|
||||
ProgressBar.style.width = evaluation.width
|
||||
StrengthDescription.textContent = evaluation.status
|
||||
StrengthDescription.classList = `text-${evaluation.class}`
|
||||
Progress.classList.remove("hidden")
|
||||
}
|
||||
|
||||
document.getElementById("password").addEventListener("input", updatePasswordStrength)
|
|
@ -10,7 +10,8 @@ const save = (id) => {
|
|||
let user = {
|
||||
username: $("#username").val(),
|
||||
password: $("#password").val(),
|
||||
role: $("#role").val()
|
||||
role: $("#role").val(),
|
||||
password_change_required: $("#force_password_change_checkbox").prop('checked')
|
||||
}
|
||||
// Submit the user
|
||||
if (id != -1) {
|
||||
|
@ -18,26 +19,26 @@ const save = (id) => {
|
|||
// we need to PUT /user/:id
|
||||
user.id = id
|
||||
api.userId.put(user)
|
||||
.success(function (data) {
|
||||
.success((data) => {
|
||||
successFlash("User " + escapeHtml(user.username) + " updated successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
.error((data) => {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
} else {
|
||||
// Else, if this is a new user, POST it
|
||||
// to /user
|
||||
api.users.post(user)
|
||||
.success(function (data) {
|
||||
.success((data) => {
|
||||
successFlash("User " + escapeHtml(user.username) + " registered successfully!")
|
||||
load()
|
||||
dismiss()
|
||||
$("#modal").modal('hide')
|
||||
})
|
||||
.error(function (data) {
|
||||
.error((data) => {
|
||||
modalError(data.responseJSON.message)
|
||||
})
|
||||
}
|
||||
|
@ -61,10 +62,11 @@ const edit = (id) => {
|
|||
$("#role").trigger("change")
|
||||
} else {
|
||||
api.userId.get(id)
|
||||
.success(function (user) {
|
||||
.success((user) => {
|
||||
$("#username").val(user.username)
|
||||
$("#role").val(user.role.slug)
|
||||
$("#role").trigger("change")
|
||||
$("#force_password_change_checkbox").prop('checked', false)
|
||||
})
|
||||
.error(function () {
|
||||
errorFlash("Error fetching user")
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
<div id="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-4x"></i>
|
||||
</div>
|
||||
<div id="emptyMessage" class="row" style="display:none;">
|
||||
{{template "flashes" .Flashes}}
|
||||
<div id="emptyMessage" style="display:none;">
|
||||
<div class="alert alert-info">
|
||||
No campaigns created yet. Let's create one!
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
fa-exclamation-triangle
|
||||
{{else if eq .Type "success"}}
|
||||
fa-check-circle
|
||||
{{else if eq .Type "info"}}
|
||||
fa-info-circle
|
||||
{{end}}"></i>
|
||||
{{.Message}}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Gophish - Open-Source Phishing Toolkit">
|
||||
<meta name="author" content="Jordan Wright (http://github.com/jordan-wright)">
|
||||
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
|
||||
|
||||
<title>Gophish - {{ .Title }}</title>
|
||||
|
||||
<link href="/css/dist/gophish.css" rel="stylesheet">
|
||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet'
|
||||
type='text/css'>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<img class="navbar-logo" src="/images/logo_inv_small.png" />
|
||||
<a class="navbar-brand" href="/"> gophish</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<div class="btn-group" id="navbar-dropdown">
|
||||
<a class="btn btn-primary" href="/logout"><i class="fa fa-user"></i> {{.User.Username}}</a>
|
||||
<a class="btn btn-primary dropdown-toggle" href="/logout">
|
||||
<i class="fa fa-sign-out"></i>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<form class="form-signin" action="" method="POST">
|
||||
<img id="logo" src="/images/logo_purple.png" />
|
||||
<h2 class="form-signin-heading">Reset Your Password</h2>
|
||||
{{template "flashes" .Flashes}}
|
||||
<input type="password" id="password" name="password" class="form-control" placeholder="Password"
|
||||
autocomplete="off" minlength="8" required autofocus>
|
||||
<div class="" id="password-strength-container">
|
||||
<div class="progress" id="password-strength">
|
||||
<div id="password-strength-bar" class="progress-bar" role="progressbar" aria-valuenow="0"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span id="password-strength-description"></span>
|
||||
</div>
|
||||
<input type="password" name="confirm_password" class="form-control" placeholder="Confirm Password"
|
||||
autocomplete="off" minlength="8" required>
|
||||
<input type="hidden" name="csrf_token" value="{{.Token}}" />
|
||||
<br />
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Save Password</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="/js/dist/app/passwords.min.js"></script>
|
||||
<script src="/js/dist/vendor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{ end }}
|
|
@ -4,6 +4,7 @@
|
|||
<h1 class="page-header">Settings</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
{{template "flashes" .Flashes}}
|
||||
<!-- Nav tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active" role="mainSettings"><a href="#mainSettings" aria-controls="mainSettings" role="tab"
|
||||
|
@ -58,8 +59,15 @@
|
|||
<div class="row">
|
||||
<label for="new_password" class="col-sm-2 control-label form-label">New Password:</label>
|
||||
<div class="col-md-6">
|
||||
<input type="password" id="new_password" name="new_password" autocomplete="off"
|
||||
<input type="password" id="password" name="new_password" autocomplete="new-password"
|
||||
class="form-control" />
|
||||
<div class="hidden" id="password-strength-container">
|
||||
<div class="progress" id="password-strength">
|
||||
<div id="password-strength-bar" class="progress-bar" role="progressbar"
|
||||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span id="password-strength-description"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -225,5 +233,6 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/passwords.min.js"></script>
|
||||
<script src="/js/dist/app/settings.min.js"></script>
|
||||
{{end}}
|
|
@ -5,7 +5,9 @@
|
|||
{{.Title}}
|
||||
</h1>
|
||||
</div>
|
||||
<div id="flashes" class="row"></div>
|
||||
<div id="flashes" class="row">
|
||||
{{template "flashes" .Flashes}}
|
||||
</div>
|
||||
<div class="row">
|
||||
<button type="button" class="btn btn-primary" id="new_button" data-toggle="modal" data-backdrop="static"
|
||||
data-user-id="-1" data-target="#modal">
|
||||
|
@ -47,13 +49,23 @@
|
|||
</div>
|
||||
<label class="control-label" for="password">Password:</label>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Password" id="password" required />
|
||||
<input type="password" class="form-control" autocomplete="new-password" placeholder="Password" id="password" required />
|
||||
<div class="hidden" id="password-strength-container">
|
||||
<div class="progress" id="password-strength">
|
||||
<div id="password-strength-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<span id="password-strength-description"></span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="control-label" for="confirm_password">Confirm Password:</label>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" placeholder="Confirm Password" id="confirm_password"
|
||||
required />
|
||||
</div>
|
||||
<div class="checkbox checkbox-primary">
|
||||
<input id="force_password_change_checkbox" type="checkbox" checked>
|
||||
<label for="force_password_change_checkbox">Require the user to set a new password</label>
|
||||
</div>
|
||||
<label class="control-label" for="role">Role:</label>
|
||||
<div class="form-group" id="role-select">
|
||||
<select class="form-control" placeholder="" id="role" />
|
||||
|
@ -70,5 +82,6 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}} {{define "scripts"}}
|
||||
<script src="/js/dist/app/passwords.min.js"></script>
|
||||
<script src="/js/dist/app/users.min.js"></script>
|
||||
{{end}}
|
19
util/util.go
19
util/util.go
|
@ -21,7 +21,6 @@ import (
|
|||
log "github.com/gophish/gophish/logger"
|
||||
"github.com/gophish/gophish/models"
|
||||
"github.com/jordan-wright/email"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -194,21 +193,3 @@ func CheckAndCreateSSL(cp string, kp string) error {
|
|||
log.Info("TLS Certificate Generation complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSecureKey creates a secure key to use as an API key
|
||||
func GenerateSecureKey() string {
|
||||
// Inspired from gorilla/securecookie
|
||||
k := make([]byte, 32)
|
||||
io.ReadFull(rand.Reader, k)
|
||||
return fmt.Sprintf("%x", k)
|
||||
}
|
||||
|
||||
// NewHash hashes the provided password and returns the bcrypt hash (using the
|
||||
// default 10 rounds) as a string.
|
||||
func NewHash(pass string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ const path = require('path');
|
|||
module.exports = {
|
||||
context: path.resolve(__dirname, 'static', 'js', 'src', 'app'),
|
||||
entry: {
|
||||
passwords: './passwords',
|
||||
users: './users',
|
||||
webhooks: './webhooks',
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue