Notes2/authentication/pkg.go
2024-11-27 21:41:48 +01:00

193 lines
4.0 KiB
Go

package authentication
import (
// External
_ "git.gibonuddevalla.se/go/wrappederror"
"github.com/golang-jwt/jwt/v5"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
// Standard
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"time"
)
type Manager struct {
db *sqlx.DB
log *slog.Logger
secret []byte
ExpireDays int
}
type User struct {
ID int
Username string
Name string
}
func httpError(w http.ResponseWriter, err error) { // {{{
j, _ := json.Marshal(struct {
OK bool
Error string
}{
false,
err.Error(),
})
w.Write(j)
} // }}}
func NewManager(db *sqlx.DB, log *slog.Logger, secret string, expireDays int) (mngr Manager, err error) { // {{{
mngr.db = db
mngr.log = log
mngr.secret, err = hex.DecodeString(secret)
mngr.ExpireDays = expireDays
return
} // }}}
func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
now := time.Now()
data["iat"] = now.Unix()
data["exp"] = now.Add(time.Hour * 24 * time.Duration(mngr.ExpireDays)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(data))
// Sign and get the complete encoded token as a string using the secret.
return token.SignedString(mngr.secret)
} // }}}
func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Request) { // {{{
var request struct {
Username string
Password string
}
body, _ := io.ReadAll(r.Body)
err := json.Unmarshal(body, &request)
if err != nil {
httpError(w, err)
}
// Verify username and password against the db user table.
authenticated, user, err := mngr.Authenticate(request.Username, request.Password)
if err != nil {
httpError(w, err)
}
if !authenticated {
httpError(w, errors.New("Authentication failed"))
}
j, _ := json.Marshal(struct {
OK bool
User User
}{true, user})
w.Write(j)
} // }}}
func (mngr *Manager) Authenticate(username, password string) (authenticated bool, user User, err error) { // {{{
var row *sql.Row
row = mngr.db.QueryRow(`
SELECT id, username, name
FROM public.user
WHERE
LOWER(username) = LOWER($1) AND
password = password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
`,
username,
password,
)
err = row.Scan(&user.ID, &user.Username, &user.Name)
authenticated = user.ID > 0
return
} // }}}
func (mngr *Manager) CreateUser(username, password, name string) (alreadyExists bool, err error) { // {{{
_, err = mngr.db.Exec(`
INSERT INTO public.user(username, password, name, totp)
VALUES(
$1,
public.password_hash(
/* salt in hex */
ENCODE(public.gen_random_bytes(16), 'hex'),
/* password */
$2::bytea
),
$3,
''
)
`,
username,
password,
name,
)
if err != nil {
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" {
err = errors.New("User already exists")
alreadyExists = true
return
}
}
return
} // }}}
func (mngr *Manager) ChangePassword(username, currentPassword, newPassword string, forceChange bool) (changed bool, err error) { // {{{
var res sql.Result
if forceChange {
res, err = mngr.db.Exec(`
UPDATE public.user
SET
"password" = public.password_hash(
/* salt in hex */
ENCODE(public.gen_random_bytes(16), 'hex'),
/* password */
$2::bytea
)
WHERE
username = $1
`,
username,
newPassword,
)
} else {
res, err = mngr.db.Exec(`
UPDATE public.user
SET
"password" = public.password_hash(
/* salt in hex */
ENCODE(public.gen_random_bytes(16), 'hex'),
/* password */
$3::bytea
)
WHERE
username = $1 AND
"password" = public.password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
`,
username,
currentPassword,
newPassword,
)
}
var rowsAffected int64
rowsAffected, err = res.RowsAffected()
if err != nil {
return
}
changed = (rowsAffected == 1)
return
} // }}}