Skip to content

Commit

Permalink
59/password login (#60)
Browse files Browse the repository at this point in the history
* Add password login

* Allow multiple wAuthN logins

* Improvements and show user always
  • Loading branch information
KordonDev authored Oct 4, 2024
1 parent 969af87 commit be892c4
Show file tree
Hide file tree
Showing 16 changed files with 376 additions and 71 deletions.
6 changes: 3 additions & 3 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
)
6 changes: 6 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
Expand All @@ -167,10 +169,14 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.1-0.20231027082548-f4a6c1f6e5c1 h1:fk72uXZyuZiTtW5tgd63jyVK6582lF61nRC/kGv6vCA=
Expand Down
5 changes: 3 additions & 2 deletions backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ func main() {
c.String(http.StatusOK, "pong")
})

authorizeJWTMiddleware := security.AuthorizeJWTMiddleware(configuration.Origin, jwtService, configuration.Domain)
userService := users.NewUserService(userDB, jwtService)
webAuthNService, err := security.NewWebAuthNService(userService, configuration.Origin, configuration.Domain, jwtService, db)
if err != nil {
panic(fmt.Sprintf("Error creating webAuthn: %v", err))
}
security.NewController(api, webAuthNService, configuration.Domain)
security.NewController(api, webAuthNService, configuration.Domain, authorizeJWTMiddleware)
// TODO: security.Controller
api.Use(security.AuthorizeJWTMiddleware(configuration.Origin, jwtService, configuration.Domain))
api.Use(authorizeJWTMiddleware)

changeWriter := changes.NewChangeWriterService(db, userService)

Expand Down
1 change: 1 addition & 0 deletions backend/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type DbUser struct {
CreatedAt time.Time
UpdatedAt time.Time
Name string `gorm:"unique"`
Password string `gorm:"default:''"`
IsApproved bool
IsAdmin bool
Credentials []DbCredential `gorm:"foreignKey:UserID"`
Expand Down
99 changes: 94 additions & 5 deletions backend/security/controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package security

import (
"context"
"net/http"

"github.com/gin-gonic/gin"
Expand All @@ -10,36 +11,44 @@ import (
)

type Service interface {
startRegister(string) (*protocol.CredentialCreation, error)
startRegisterExistingUser(string) (*protocol.CredentialCreation, error)
startRegisterNewUser(string) (*protocol.CredentialCreation, error)
finishRegistration(string, *http.Request) (*models.User, error)
startLogin(string, *http.Request) (*protocol.CredentialAssertion, error)
finishLogin(string, *http.Request) (*models.User, string, error)
passwordLogin(ctx context.Context, username, password string) (*models.User, string, error)
changePassword(ctx context.Context, username, password string) error
}

type Controller struct {
service Service
domain string
}

func NewController(baseUrl *gin.RouterGroup, service Service, domain string) error {
func NewController(baseUrl *gin.RouterGroup, service Service, domain string, authorizeMiddleware gin.HandlerFunc) error {

ctrl := Controller{
service: service,
domain: domain,
}
baseUrl.GET("/register/:username", ctrl.startRegister)
baseUrl.GET("/register/:username", ctrl.startRegisterNewUser)
baseUrl.POST("/register/:username", ctrl.finishRegistration)
baseUrl.GET("/login/:username", ctrl.startLogin)
baseUrl.POST("/login/:username", ctrl.finishLogin)
baseUrl.POST("/logout", ctrl.logout)
baseUrl.POST("/password-login", ctrl.passwordLogin)

baseUrl.PATCH("/change-password", authorizeMiddleware, ctrl.changePassword)
baseUrl.GET("/add-authentication", authorizeMiddleware, ctrl.startRegisterExistingUser)
baseUrl.POST("/add-authentication", authorizeMiddleware, ctrl.finishRegisterExistingUser)

return nil
}

func (ctrl Controller) startRegister(c *gin.Context) {
func (ctrl Controller) startRegisterNewUser(c *gin.Context) {
username := c.Param("username")

options, err := ctrl.service.startRegister(username)
options, err := ctrl.service.startRegisterNewUser(username)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
Expand All @@ -48,6 +57,38 @@ func (ctrl Controller) startRegister(c *gin.Context) {
c.JSON(http.StatusOK, options)
}

func (ctrl Controller) startRegisterExistingUser(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.AbortWithStatus(http.StatusBadRequest)
return
}

options, err := ctrl.service.startRegisterExistingUser(username)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}

c.JSON(http.StatusOK, options)
}

func (ctrl Controller) finishRegisterExistingUser(c *gin.Context) {
username := c.GetString("username")
if username == "" {
c.AbortWithStatus(http.StatusBadRequest)
return
}

user, err := ctrl.service.finishRegistration(username, c.Request)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}

c.JSON(http.StatusOK, user)
}

func (ctrl Controller) finishRegistration(c *gin.Context) {
username := c.Param("username")

Expand Down Expand Up @@ -86,6 +127,54 @@ func (ctrl Controller) finishLogin(c *gin.Context) {
c.JSON(http.StatusOK, user)
}

type changePasswordRequest struct {
Password string `json:"password"`
}

func (ctrl Controller) changePassword(c *gin.Context) {
var p changePasswordRequest
if err := c.BindJSON(&p); err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}

username := c.GetString("username")
if username == "" {
c.AbortWithStatus(http.StatusBadRequest)
return
}

err := ctrl.service.changePassword(c, username, p.Password)
if err != nil {
c.AbortWithStatus(http.StatusInternalServerError)
return
}

c.Status(http.StatusOK)
}

type passwordLogin struct {
Password string `json:"password"`
Username string `json:"username"`
}

func (ctrl Controller) passwordLogin(c *gin.Context) {
var login passwordLogin
if err := c.BindJSON(&login); err != nil {
c.AbortWithStatus(http.StatusBadRequest)
return
}

user, token, err := ctrl.service.passwordLogin(c, login.Username, login.Password)
if err != nil {
c.AbortWithError(http.StatusUnauthorized, err)
return
}

url.SetCookie(c, token, ctrl.domain)
c.JSON(http.StatusOK, user)
}

func (ctrl Controller) logout(c *gin.Context) {
url.RemoveCookie(c, ctrl.domain)
c.JSON(http.StatusUnauthorized, gin.H{
Expand Down
66 changes: 55 additions & 11 deletions backend/security/service.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package security

import (
"context"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"net/http"
"time"

Expand All @@ -16,6 +19,8 @@ type userService interface {
AddUser(*models.User) (*models.User, error)
SaveUser(*models.User) (*models.User, error)
HasApprovedAndAdminUser() bool
CheckLogin(username, password string) error
ChangePassword(ctx context.Context, username, password string) error
}

type SessionStore interface {
Expand Down Expand Up @@ -65,20 +70,38 @@ func NewWebAuthNService(userService userService, origin string, domain string, j
}, nil
}

func (w WebAuthNService) startRegister(username string) (*protocol.CredentialCreation, error) {
func (w WebAuthNService) startRegisterNewUser(username string) (*protocol.CredentialCreation, error) {
if w.userExists(username) {
return nil, errors.New("user already exists")
}

user := &models.User{
Name: username,
IsApproved: false,
IsAdmin: false,
Credentials: []webauthn.Credential{},
}
user, err := w.userService.AddUser(user)
if err != nil {
return nil, err
}
return w.beginRegistration(user, username)
}

func (w WebAuthNService) startRegisterExistingUser(username string) (*protocol.CredentialCreation, error) {
user, err := w.userService.GetUser(username)
if err != nil {
user = &models.User{
Name: username,
IsApproved: false,
IsAdmin: false,
Credentials: []webauthn.Credential{},
}
user, err = w.userService.AddUser(user)
if err != nil {
return nil, err
}
return nil, err
}
return w.beginRegistration(user, username)
}

func (w WebAuthNService) userExists(username string) bool {
_, err := w.userService.GetUser(username)
return err == nil
}

func (w WebAuthNService) beginRegistration(user *models.User, username string) (*protocol.CredentialCreation, error) {
registerOpts := func(credOptions *protocol.PublicKeyCredentialCreationOptions) {
credOptions.CredentialExcludeList = user.ExcludedCredentials()
}
Expand Down Expand Up @@ -175,3 +198,24 @@ func (w WebAuthNService) finishLogin(username string, request *http.Request) (*m
token := w.jwtService.GenerateToken(*user)
return user, token, nil
}

func (w WebAuthNService) changePassword(ctx context.Context, username, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return w.userService.ChangePassword(ctx, username, string(hashedPassword))
}

func (w WebAuthNService) passwordLogin(ctx context.Context, username, password string) (*models.User, string, error) {
if err := w.userService.CheckLogin(username, password); err != nil {
return nil, "", err
}
user, err := w.userService.GetUser(username)
if err != nil {
return nil, "", err
}

token := w.jwtService.GenerateToken(*user)
return user, token, nil
}
22 changes: 22 additions & 0 deletions backend/users/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,25 @@ func listFromDb(dbu []*models.DbUser) []*models.User {
}
return users
}

func (u *userDB) changePassword(username, password string) error {
return u.DB.Exec("UPDATE users SET password = ? WHERE name = ?", password, username).Error
}

type password struct {
Password string `gorm:"password"`
}

func (password) TableName() string {
return "users"
}

func (u *userDB) getPasswordHashForUser(username string) (string, error) {
var p password
err := u.Where("name = ?", username).First(&p).Error
if err != nil {
return "", err
}

return p.Password, nil
}
20 changes: 20 additions & 0 deletions backend/users/service.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package users

import (
"context"
"github.com/kordondev/equipment-watchdog/models"

"golang.org/x/crypto/bcrypt"
)

type UserDatabase interface {
Expand All @@ -11,6 +14,8 @@ type UserDatabase interface {
addUser(*models.User) (*models.User, error)
hasApprovedAndAdminUser() bool
getForIds([]uint64) ([]*models.User, error)
changePassword(string, string) error
getPasswordHashForUser(username string) (string, error)
}

type JwtService interface {
Expand Down Expand Up @@ -78,3 +83,18 @@ func (u *userService) HasApprovedAndAdminUser() bool {
func (u *userService) GetForIds(ids []uint64) ([]*models.User, error) {
return u.db.getForIds(ids)
}

func (u *userService) ChangePassword(c context.Context, username, password string) error {
return u.db.changePassword(username, password)
}

func (u *userService) CheckLogin(username, password string) error {
pwHash, err := u.db.getPasswordHashForUser(username)
if err != nil {
return err
}
if err := bcrypt.CompareHashAndPassword([]byte(pwHash), []byte(password)); err != nil {
return err
}
return nil
}
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 9000",
"start": "vite --port 9000",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json"
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import OrderDetails from "./views/order/OrderDetails.svelte";
import AddOrder from "./views/order/NewOrder.svelte";
import FulfilledOrders from "./views/order/FulfilledOrders.svelte";
import PasswordLogin from "./views/security/PasswordLogin.svelte";
if (window.location.pathname === "/" && window.location.hash === "") {
replace(routes.MemberOverview.link);
Expand All @@ -39,6 +40,7 @@
[routes.OrderDetails.path]: OrderDetails,
[routes.AddOrder.path]: AddOrder,
[routes.NotApproved.path]: NotApproved,
[routes.PasswordLogin.path]: PasswordLogin,
}}
/>
</main>
Expand Down
Loading

0 comments on commit be892c4

Please sign in to comment.