mirror of
https://github.com/thomiceli/opengist
synced 2024-11-08 12:55:50 +01:00
Reset a user password using CLI (#226)
This commit is contained in:
parent
fc9a75ce8f
commit
1c1e3a8919
7
docs/administration/reset-password.md
Normal file
7
docs/administration/reset-password.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Reset a user password
|
||||||
|
|
||||||
|
To reset a user password, run the following command using the Opengist binary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./opengist admin reset-password <username> <new-password>
|
||||||
|
```
|
50
internal/cli/admin.go
Normal file
50
internal/cli/admin.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
|
"github.com/thomiceli/opengist/internal/utils"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var CmdAdmin = cli.Command{
|
||||||
|
Name: "admin",
|
||||||
|
Usage: "Admin commands",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
&CmdAdminResetPassword,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var CmdAdminResetPassword = cli.Command{
|
||||||
|
Name: "reset-password",
|
||||||
|
Usage: "Reset the password for a given user",
|
||||||
|
ArgsUsage: "[username] [password]",
|
||||||
|
Action: func(ctx *cli.Context) error {
|
||||||
|
initialize(ctx)
|
||||||
|
if ctx.NArg() < 2 {
|
||||||
|
return fmt.Errorf("username and password are required")
|
||||||
|
}
|
||||||
|
username := ctx.Args().Get(0)
|
||||||
|
plainPassword := ctx.Args().Get(1)
|
||||||
|
|
||||||
|
user, err := db.GetUserByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Cannot get user %s: %s\n", username, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
password, err := utils.Argon2id.Hash(plainPassword)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Cannot hash password for user %s: %s\n", username, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Password = password
|
||||||
|
|
||||||
|
if err = user.Update(); err != nil {
|
||||||
|
fmt.Printf("Cannot update password for user %s: %s\n", username, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Password for user %s has been reset.\n", username)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
@ -48,7 +48,7 @@ func App() error {
|
|||||||
app.Usage = "A self-hosted pastebin powered by Git."
|
app.Usage = "A self-hosted pastebin powered by Git."
|
||||||
app.HelpName = "opengist"
|
app.HelpName = "opengist"
|
||||||
|
|
||||||
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook}
|
app.Commands = []*cli.Command{&CmdVersion, &CmdStart, &CmdHook, &CmdAdmin}
|
||||||
app.DefaultCommand = CmdStart.Name
|
app.DefaultCommand = CmdStart.Name
|
||||||
app.Flags = []cli.Flag{
|
app.Flags = []cli.Flag{
|
||||||
&ConfigFlag,
|
&ConfigFlag,
|
||||||
|
76
internal/utils/argon2id.go
Normal file
76
internal/utils/argon2id.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Argon2ID struct {
|
||||||
|
format string
|
||||||
|
version int
|
||||||
|
time uint32
|
||||||
|
memory uint32
|
||||||
|
keyLen uint32
|
||||||
|
saltLen uint32
|
||||||
|
threads uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
var Argon2id = Argon2ID{
|
||||||
|
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||||
|
version: argon2.Version,
|
||||||
|
time: 1,
|
||||||
|
memory: 64 * 1024,
|
||||||
|
keyLen: 32,
|
||||||
|
saltLen: 16,
|
||||||
|
threads: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Argon2ID) Hash(plain string) (string, error) {
|
||||||
|
salt := make([]byte, a.saltLen)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
|
||||||
|
|
||||||
|
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
|
||||||
|
base64.RawStdEncoding.EncodeToString(salt),
|
||||||
|
base64.RawStdEncoding.EncodeToString(hash),
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Argon2ID) Verify(plain, hash string) (bool, error) {
|
||||||
|
if hash == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hashParts := strings.Split(hash, "$")
|
||||||
|
|
||||||
|
if len(hashParts) != 6 {
|
||||||
|
return false, errors.New("invalid hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
|
||||||
|
|
||||||
|
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
|
||||||
|
}
|
@ -74,7 +74,7 @@ func processRegister(ctx echo.Context) error {
|
|||||||
|
|
||||||
user := dto.ToUser()
|
user := dto.ToUser()
|
||||||
|
|
||||||
password, err := argon2id.hash(user.Password)
|
password, err := utils.Argon2id.Hash(user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot hash password", err)
|
return errorRes(500, "Cannot hash password", err)
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ func processLogin(ctx echo.Context) error {
|
|||||||
return redirect(ctx, "/login")
|
return redirect(ctx, "/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := argon2id.verify(password, user.Password); !ok {
|
if ok, err := utils.Argon2id.Verify(password, user.Password); !ok {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot check for password", err)
|
return errorRes(500, "Cannot check for password", err)
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/thomiceli/opengist/internal/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -98,7 +99,7 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
return plainText(ctx, 404, "Check your credentials or make sure you have access to the Gist")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := argon2id.verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
|
if ok, err := utils.Argon2id.Verify(authPassword, gist.User.Password); !ok || gist.User.Username != authUsername {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot verify password", err)
|
return errorRes(500, "Cannot verify password", err)
|
||||||
}
|
}
|
||||||
@ -115,7 +116,7 @@ func gitHttp(ctx echo.Context) error {
|
|||||||
return errorRes(401, "Invalid credentials", nil)
|
return errorRes(401, "Invalid credentials", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := argon2id.verify(authPassword, user.Password); !ok {
|
if ok, err := utils.Argon2id.Verify(authPassword, user.Password); !ok {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot check for password", err)
|
return errorRes(500, "Cannot check for password", err)
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ func passwordProcess(ctx echo.Context) error {
|
|||||||
return html(ctx, "settings.html")
|
return html(ctx, "settings.html")
|
||||||
}
|
}
|
||||||
|
|
||||||
password, err := argon2id.hash(dto.Password)
|
password, err := utils.Argon2id.Hash(dto.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorRes(500, "Cannot hash password", err)
|
return errorRes(500, "Cannot hash password", err)
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,12 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/thomiceli/opengist/internal/config"
|
"github.com/thomiceli/opengist/internal/config"
|
||||||
"github.com/thomiceli/opengist/internal/db"
|
"github.com/thomiceli/opengist/internal/db"
|
||||||
"github.com/thomiceli/opengist/internal/i18n"
|
"github.com/thomiceli/opengist/internal/i18n"
|
||||||
"golang.org/x/crypto/argon2"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -219,68 +214,3 @@ func addMetadataToSearchQuery(input, key, value string) string {
|
|||||||
|
|
||||||
return strings.TrimSpace(resultBuilder.String())
|
return strings.TrimSpace(resultBuilder.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
type Argon2ID struct {
|
|
||||||
format string
|
|
||||||
version int
|
|
||||||
time uint32
|
|
||||||
memory uint32
|
|
||||||
keyLen uint32
|
|
||||||
saltLen uint32
|
|
||||||
threads uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
var argon2id = Argon2ID{
|
|
||||||
format: "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
|
||||||
version: argon2.Version,
|
|
||||||
time: 1,
|
|
||||||
memory: 64 * 1024,
|
|
||||||
keyLen: 32,
|
|
||||||
saltLen: 16,
|
|
||||||
threads: 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argon2ID) hash(plain string) (string, error) {
|
|
||||||
salt := make([]byte, a.saltLen)
|
|
||||||
if _, err := rand.Read(salt); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, a.keyLen)
|
|
||||||
|
|
||||||
return fmt.Sprintf(a.format, a.version, a.memory, a.time, a.threads,
|
|
||||||
base64.RawStdEncoding.EncodeToString(salt),
|
|
||||||
base64.RawStdEncoding.EncodeToString(hash),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Argon2ID) verify(plain, hash string) (bool, error) {
|
|
||||||
if hash == "" {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hashParts := strings.Split(hash, "$")
|
|
||||||
|
|
||||||
if len(hashParts) != 6 {
|
|
||||||
return false, errors.New("invalid hash")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &a.memory, &a.time, &a.threads)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hashToCompare := argon2.IDKey([]byte(plain), salt, a.time, a.memory, a.threads, uint32(len(decodedHash)))
|
|
||||||
|
|
||||||
return subtle.ConstantTimeCompare(decodedHash, hashToCompare) == 1, nil
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user