add users

This commit is contained in:
u80864958
2025-04-30 16:41:30 +02:00
parent f4adfb6a62
commit eed1718a7e
17 changed files with 538 additions and 60 deletions

View File

@ -2,38 +2,115 @@ package auth
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"slices"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model"
)
func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
login := model.Login{}
if err := json.NewDecoder(r.Body).Decode(&login); err != nil {
func (s *Service) Signup(w http.ResponseWriter, r *http.Request) {
var err error
var login Login
user := model.NewUser()
if err = json.NewDecoder(r.Body).Decode(&login); err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
if login.Name == s.cfg.AdminName && login.Password == s.cfg.AdminPassword {
token, err := createJWT([]byte(s.cfg.Secret))
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
if len([]byte(login.Password)) > 72 {
fmt.Fprint(w, "Password to long, max 72 bytes")
w.WriteHeader(http.StatusBadRequest)
return
}
if user.Password, err = bcrypt.GenerateFromPassword([]byte(login.Password), 6); err != nil {
log.Println("Error: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
user.Name = login.Name
user.Role = s.cfg.DefaultRole
err = s.db.Save(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrCheckConstraintViolated) {
fmt.Fprint(w, "Username is already in use")
w.WriteHeader(http.StatusBadRequest)
return
}
err = json.NewEncoder(w).Encode(&model.LoginResponse{
Token: token,
})
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
return
log.Printf("Error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
}
}
func (s *Service) Authenticated(next http.HandlerFunc) http.Handler {
func (s *Service) Login(w http.ResponseWriter, r *http.Request) {
var login Login
var user model.User
if err := json.NewDecoder(r.Body).Decode(&login); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if err := s.db.First(&user).Error; err != nil {
fmt.Fprint(w, "user not found")
w.WriteHeader(http.StatusBadRequest)
}
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(login.Password)); err != nil {
fmt.Fprint(w, "Invalid Password")
w.WriteHeader(http.StatusBadRequest)
}
token, err := s.createJWT(&user)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
res, err := json.Marshal(&LoginResponse{
Token: token,
})
if err != nil {
log.Println("Error: ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(res)
}
func (s *Service) Logout(w http.ResponseWriter, r *http.Request) {
token, err := extractToken(r)
if err != nil {
log.Printf("Error while extracting token: %s", err.Error())
w.WriteHeader(http.StatusInternalServerError)
return
}
claims, err := s.validateJWT(token)
if err != nil {
fmt.Fprint(w, "Invalid token")
w.WriteHeader(http.StatusBadRequest)
return
}
if err = s.db.Save(&model.InvalidJWT{JWT: token, ValidUntil: claims.ExpiresAt.Time}).Error; err != nil {
log.Printf("Error while saving logout token: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Service) Authenticated(next http.HandlerFunc, roles ...model.Role) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Our middleware logic goes here...
token, err := extractToken(r)
@ -42,12 +119,18 @@ func (s *Service) Authenticated(next http.HandlerFunc) http.Handler {
return
}
err = validateJWT(token, []byte(s.cfg.Secret))
claims, err := s.validateJWT(token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
// if roles specified check if satisfied
if len(roles) > 0 && !slices.Contains(roles, claims.Role) {
w.WriteHeader(http.StatusForbidden)
return
}
r = writeToContext(r, &claims)
next(w, r)
})
}

View File

@ -0,0 +1,22 @@
package auth
import (
"context"
"net/http"
)
type authkey int
const keyClaims = iota
func writeToContext(r *http.Request, claims *Claims) *http.Request {
ctx := context.WithValue(r.Context(), claims, claims)
return r.WithContext(ctx)
}
// ExtractClaims extracts user claims from given context. If no claims in context ok = false.
func ExtractClaims(ctx context.Context) (claims *Claims, ok bool) {
val := ctx.Value(keyClaims)
claims, ok = val.(*Claims)
return
}

View File

@ -3,35 +3,61 @@ package auth
import (
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model"
jwt "github.com/golang-jwt/jwt/v5"
)
func createJWT(secret []byte) (token string, err error) {
return jwt.NewWithClaims(jwt.SigningMethodHS512, jwt.MapClaims{
"exp": time.Now().Add(time.Hour * 24).Unix(),
}).SignedString(secret)
var ErrJWTInvalid = errors.New("JWT not valid")
func (s *Service) createJWT(user *model.User) (token string, err error) {
claims := &Claims{
Role: user.Role,
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
Subject: user.UUID.String(),
ExpiresAt: &jwt.NumericDate{
Time: time.Now().Add(s.cfg.ValidDuration),
},
},
}
return jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString([]byte(s.cfg.Secret))
}
func validateJWT(tokenString string, secret []byte) (err error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
func (s *Service) validateJWT(tokenString string) (claims Claims, err error) {
_, err = jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (any, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
return []byte(s.cfg.Secret), nil
})
if err != nil {
return
}
if date, err := token.Claims.GetExpirationTime(); err == nil && date.After(time.Now()) {
return nil
log.Println(claims)
if claims.ExpiresAt.Before(time.Now()) {
err = ErrJWTInvalid
return
}
return errors.New("JWT not valid")
var invalidated bool
err = s.db.Model(&model.InvalidJWT{}).
Select("count(*) > 0").
Where("jwt = ?", tokenString).
Find(&invalidated).
Error
if invalidated || err != nil {
err = ErrJWTInvalid
return
}
return
}
func extractToken(r *http.Request) (token string, err error) {

View File

@ -0,0 +1,76 @@
package auth
import (
"log"
"testing"
"time"
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model"
"github.com/google/uuid"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func testDB() (db *gorm.DB) {
db, err := gorm.Open(sqlite.Open(":memory:"))
if err != nil {
log.Panic(err)
}
db.AutoMigrate(&model.User{}, &model.InvalidJWT{})
return
}
func TestService_JWT(t *testing.T) {
t.Parallel()
tests := []struct {
user model.User
}{
{
user: model.User{
ID: 0,
Name: "Hans de Admin",
Role: model.RoleAdmin,
UUID: uuid.MustParse("9d8973b7-2005-4ca6-a4bf-7bae5aad2916"),
},
},
{
user: model.User{
ID: 1,
Name: "Ueli de User",
Role: model.RoleUser,
UUID: uuid.MustParse("e1b7099f-a3be-4d77-b33f-389e27123187"),
},
},
}
for _, tt := range tests {
t.Run(tt.user.Name, func(t *testing.T) {
t.Parallel()
s := New(&Config{
Secret: "asdf",
ValidDuration: time.Hour,
AdminName: "adsf",
AdminPassword: "adsf",
}, testDB())
jwt, err := s.createJWT(&tt.user)
if err != nil {
t.Errorf("Error while creating JWT: %v", err)
}
claims, err := s.validateJWT(jwt)
if err != nil {
t.Errorf("Error while creating JWT: %v", err)
}
if claims.Subject != tt.user.UUID.String() {
t.Error("Subject does not match")
}
if claims.Role != tt.user.Role {
t.Error("Roles did not match")
}
})
}
}

View File

@ -1,9 +1,14 @@
package auth
import (
"log"
"time"
"git.schreifuchs.ch/schreifuchs/ng-blog/backend/internal/model"
jwt "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Config struct {
@ -11,6 +16,7 @@ type Config struct {
ValidDuration time.Duration `env:"VALID_DURATION"`
AdminName string `env:"ADMIN_NAME"`
AdminPassword string `env:"ADMIN_PASSWORD"`
DefaultRole model.Role `env:"DEFAULT_ROLE"`
}
type Service struct {
@ -19,8 +25,33 @@ type Service struct {
}
func New(cfg *Config, db *gorm.DB) *Service {
user := model.NewUser()
var err error
if user.Password, err = bcrypt.GenerateFromPassword([]byte(cfg.AdminName), 6); err != nil {
log.Fatalf("Error while creating default user: %v", err)
}
user.Name = cfg.AdminName
user.Role = model.RoleAdmin
// add default user
_ = db.Clauses(clause.OnConflict{DoNothing: true}).Save(&user).Error
return &Service{
cfg,
db,
}
}
type Claims struct {
Role model.Role `json:"rl"`
UserID uint `json:"uid"`
jwt.RegisteredClaims
}
type Login struct {
Name string `json:"name"`
Password string `json:"Password"`
}
type LoginResponse struct {
Token string `json:"token"`
}