Compare commits

...

12 Commits

10 changed files with 298 additions and 138 deletions

View File

@ -1,4 +1,7 @@
package domain package domain
type UserDto struct { type UserDto struct {
Id int `json:"id"`
Name string `json:"name"`
Scopes string `json:"scopes"`
} }

View File

@ -2,4 +2,9 @@ package domain
type HelloBodyRequest struct { type HelloBodyRequest struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
} }
type AddScopeRequest struct {
Username string `json:"name"`
Scopes []string `json:"scopes" validate:"required"`
}

View File

@ -1,8 +1,8 @@
package domain package domain
type ErrorResponse struct { type ErrorResponse struct {
HttpCode int `json:"code"` Success bool `json:"success"`
Message string `json:"message"` Message string `json:"message"`
} }
type HelloWhoResponse struct { type HelloWhoResponse struct {

View File

@ -5,134 +5,55 @@ import (
"go-cook/api/domain" "go-cook/api/domain"
"go-cook/api/repositories" "go-cook/api/repositories"
"net/http" "net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
const ( const (
ErrJwtMissing = "auth token is missing" ErrJwtMissing = "auth token is missing"
ErrJwtClaimsMissing = "claims missing on token" ErrJwtClaimsMissing = "claims missing on token"
ErrJwtExpired = "auth token has expired" ErrJwtExpired = "auth token has expired"
ErrJwtScopeMissing = "required scope is missing" ErrJwtScopeMissing = "required scope is missing"
ErrUserNotFound = "requested user does not exist" ErrUserNotFound = "requested user does not exist"
ErrUsernameAlreadyExists = "the requested username already exists"
) )
type JwtToken struct {
Exp time.Time `json:"exp"`
Iss string `json:"iss"`
Authorized bool `json:"authorized"`
UserName string `json:"username"`
Scopes []string `json:"scopes"`
jwt.RegisteredClaims
}
func (j JwtToken) IsValid(scope string) error {
err := j.hasExpired()
if err != nil {
return err
}
err = j.hasScope(scope)
if err != nil {
return err
}
return nil
}
func (j JwtToken) hasExpired() error {
// Check to see if the token has expired
hasExpired := j.Exp.Compare(time.Now())
if hasExpired == -1 {
return errors.New(ErrJwtExpired)
}
return nil
}
func (j JwtToken) hasScope(scope string) error {
// they have the scope to access everything, so let them pass.
if strings.Contains(domain.ScopeAll, scope) {
return nil
}
for _, s := range j.Scopes {
if strings.Contains(s, scope) {
return nil
}
}
return errors.New(ErrJwtScopeMissing)
}
func (h *Handler) generateJwt(username string) (string, error) {
secret := []byte(h.Config.JwtSecret)
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["username"] = username
var scopes []string
scopes = append(scopes, domain.ScopeRecipeRead)
claims["scopes"] = scopes
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}
func (h *Handler) generateAdminJwt(username string) (string, error) {
secret := []byte(h.Config.JwtSecret)
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["username"] = username
claims["scopes"] = domain.ScopeAll
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}
func (h *Handler) AuthRegister(c echo.Context) error { func (h *Handler) AuthRegister(c echo.Context) error {
username := c.QueryParam("username") username := c.FormValue("username")
_, err := h.userRepo.GetByName(username) password := c.FormValue("password")
//username := c.QueryParam("username")
exists, err := h.userRepo.GetByName(username)
if err != nil { if err != nil {
// if we have an err, validate that if its not user not found. // if we have an err, validate that if its not user not found.
// if the user is not found, we can use that name // if the user is not found, we can use that name
if err.Error() != repositories.ErrUserNotFound { if err.Error() != repositories.ErrUserNotFound {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError,
Message: err.Error(), Message: err.Error(),
Success: true,
}) })
} }
} }
if exists.Name == username {
return h.InternalServerErrorResponse(c, ErrUsernameAlreadyExists)
}
password := c.QueryParam("password") //password := c.QueryParam("password")
err = h.UserService.CheckPasswordForRequirements(password) err = h.UserService.CheckPasswordForRequirements(password)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError, Success: false,
Message: err.Error(), Message: err.Error(),
}) })
} }
_, err = h.userRepo.Create(username, password) _, err = h.userRepo.Create(username, password, domain.ScopeRecipeRead)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{ return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError, Success: false,
Message: err.Error(), Message: err.Error(),
}) })
} }
@ -140,21 +61,12 @@ func (h *Handler) AuthRegister(c echo.Context) error {
} }
func (h *Handler) AuthLogin(c echo.Context) error { func (h *Handler) AuthLogin(c echo.Context) error {
username := c.QueryParam("username") username := c.FormValue("name")
password := c.QueryParam("password") password := c.FormValue("password")
// Check to see if they are trying to login with the admin token // Check to see if they are trying to login with the admin token
if username == "" { if username == "" {
if h.Config.AdminToken != password { return h.validateAdminToken(c, password)
return h.ReturnUnauthorizedResponse(c, ErrUserNotFound)
}
token, err := h.generateAdminJwt("admin")
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
return c.JSON(http.StatusOK, token)
} }
// check if the user exists // check if the user exists
@ -177,9 +89,55 @@ func (h *Handler) AuthLogin(c echo.Context) error {
return c.JSON(http.StatusOK, token) return c.JSON(http.StatusOK, token)
} }
//func (h *Handler) AddScope(c echo.Context) error { func (h *Handler) validateAdminToken(c echo.Context, password string) error {
// if h.Config.AdminToken != password {
//} return h.ReturnUnauthorizedResponse(c, ErrUserNotFound)
}
token, err := h.generateAdminJwt("admin")
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
return c.JSON(http.StatusOK, token)
}
func (h *Handler) AddScope(c echo.Context) error {
token, err := h.getJwtToken(c)
if err != nil {
return h.ReturnUnauthorizedResponse(c, err.Error())
}
err = token.IsValid(domain.ScopeAll)
if err != nil {
return h.ReturnUnauthorizedResponse(c, err.Error())
}
request := domain.AddScopeRequest{}
err = (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil {
return c.JSON(http.StatusBadRequest, domain.ErrorResponse{
Success: false,
Message: err.Error(),
})
}
err = h.UserService.AddScopes(request.Username, request.Scopes)
if err != nil {
return h.InternalServerErrorResponse(c, err.Error())
}
return c.JSON(http.StatusOK, domain.ErrorResponse{
Success: true,
})
}
func (h *Handler) RemoveScope(c echo.Context) error {
return c.JSON(http.StatusOK, domain.ErrorResponse{
Success: false,
Message: "Not Implemented",
})
}
func (h *Handler) RefreshJwtToken(c echo.Context) error { func (h *Handler) RefreshJwtToken(c echo.Context) error {
return nil return nil

View File

@ -37,10 +37,14 @@ func (h *Handler) Register(v1 *echo.Group) {
SigningKey: []byte(h.Config.JwtSecret), SigningKey: []byte(h.Config.JwtSecret),
} }
v1.POST("/login", h.AuthLogin) auth := v1.Group("/auth")
v1.POST("/register", h.AuthRegister) auth.POST("/login", h.AuthLogin)
demo := v1.Group("/demo") auth.POST("/register", h.AuthRegister)
auth.Use(echojwt.WithConfig(jwtConfig))
auth.POST("/scopes/add", h.AddScope)
//auth.POST("/refresh", h.RefreshJwtToken)
demo := v1.Group("/demo")
demo.GET("/hello", h.DemoHello) demo.GET("/hello", h.DemoHello)
demo.GET("/hello/:who", h.HelloWho) demo.GET("/hello/:who", h.HelloWho)
@ -61,14 +65,14 @@ func (h *Handler) Register(v1 *echo.Group) {
func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) error { func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) error {
return c.JSON(http.StatusUnauthorized, domain.ErrorResponse{ return c.JSON(http.StatusUnauthorized, domain.ErrorResponse{
HttpCode: http.StatusUnauthorized, Success: false,
Message: message, Message: message,
}) })
} }
func (h *Handler) InternalServerErrorResponse(c echo.Context, message string) error { func (h *Handler) InternalServerErrorResponse(c echo.Context, message string) error {
return c.JSON(http.StatusServiceUnavailable, domain.ErrorResponse{ return c.JSON(http.StatusServiceUnavailable, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError, Success: false,
Message: message, Message: message,
}) })
} }

98
api/handlers/v1/jwt.go Normal file
View File

@ -0,0 +1,98 @@
package v1
import (
"errors"
"go-cook/api/domain"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JwtToken struct {
Exp time.Time `json:"exp"`
Iss string `json:"iss"`
Authorized bool `json:"authorized"`
UserName string `json:"username"`
Scopes []string `json:"scopes"`
jwt.RegisteredClaims
}
func (j JwtToken) IsValid(scope string) error {
err := j.hasExpired()
if err != nil {
return err
}
err = j.hasScope(scope)
if err != nil {
return err
}
return nil
}
func (j JwtToken) hasExpired() error {
// Check to see if the token has expired
hasExpired := j.Exp.Compare(time.Now())
if hasExpired == -1 {
return errors.New(ErrJwtExpired)
}
return nil
}
func (j JwtToken) hasScope(scope string) error {
// they have the scope to access everything, so let them pass.
if strings.Contains(domain.ScopeAll, scope) {
return nil
}
for _, s := range j.Scopes {
if strings.Contains(s, scope) {
return nil
}
}
return errors.New(ErrJwtScopeMissing)
}
func (h *Handler) generateJwt(username string) (string, error) {
secret := []byte(h.Config.JwtSecret)
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["username"] = username
var scopes []string
scopes = append(scopes, domain.ScopeRecipeRead)
claims["scopes"] = scopes
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}
func (h *Handler) generateAdminJwt(username string) (string, error) {
secret := []byte(h.Config.JwtSecret)
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["username"] = username
var scopes []string
scopes = append(scopes, domain.ScopeAll)
claims["scopes"] = scopes
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}

View File

@ -18,11 +18,11 @@ const (
type IUserTable interface { type IUserTable interface {
GetByName(name string) (domain.UserEntity, error) GetByName(name string) (domain.UserEntity, error)
Create(name, password string) (int64, error) Create(name, password, scope string) (int64, error)
Update(id int, entity domain.UserEntity) error Update(id int, entity domain.UserEntity) error
UpdatePassword(name, password string) error UpdatePassword(name, password string) error
CheckUserHash(name, password string) error CheckUserHash(name, password string) error
AddScope(name, scope string) error UpdateScopes(name, scope string) error
} }
// Creates a new instance of UserRepository with the bound sql // Creates a new instance of UserRepository with the bound sql
@ -56,7 +56,7 @@ func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) {
return data[0], nil return data[0], nil
} }
func (ur UserRepository) Create(name, password string) (int64, error) { func (ur UserRepository) Create(name, password, scope string) (int64, error) {
passwordBytes := []byte(password) passwordBytes := []byte(password)
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
if err != nil { if err != nil {
@ -66,8 +66,8 @@ func (ur UserRepository) Create(name, password string) (int64, error) {
dt := time.Now() dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("users") queryBuilder.InsertInto("users")
queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt") queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt", "Scopes")
queryBuilder.Values(name, string(hash), dt, dt) queryBuilder.Values(name, string(hash), dt, dt, scope)
query, args := queryBuilder.Build() query, args := queryBuilder.Build()
_, err = ur.connection.Exec(query, args...) _, err = ur.connection.Exec(query, args...)
@ -110,7 +110,7 @@ func (ur UserRepository) CheckUserHash(name, password string) error {
return nil return nil
} }
func (ur UserRepository) AddScope(name, scope string) error { func (ur UserRepository) UpdateScopes(name, scope string) error {
builder := sqlbuilder.NewUpdateBuilder() builder := sqlbuilder.NewUpdateBuilder()
builder.Update("users") builder.Update("users")
builder.Set ( builder.Set (

View File

@ -2,6 +2,7 @@ package repositories_test
import ( import (
"database/sql" "database/sql"
"go-cook/api/domain"
"go-cook/api/repositories" "go-cook/api/repositories"
"log" "log"
"testing" "testing"
@ -20,7 +21,7 @@ func TestCanCreateNewUser(t *testing.T) {
defer db.Close() defer db.Close()
repo := repositories.NewUserRepository(db) repo := repositories.NewUserRepository(db)
updated, err := repo.Create("testing", "NotSecure") updated, err := repo.Create("testing", "NotSecure", domain.ScopeRecipeRead)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
t.FailNow() t.FailNow()

View File

@ -53,13 +53,70 @@ func (us UserService) GetUser(username string) (domain.UserEntity, error) {
return us.repo.GetByName(username) return us.repo.GetByName(username)
} }
func (us UserService) CreateNewUser(name, password string) (domain.UserEntity, error) { func (us UserService) AddScopes(username string, scopes []string) error {
usr, err := us.repo.GetByName(username)
if err != nil {
return err
}
if usr.Name != username {
return errors.New(repositories.ErrUserNotFound)
}
newScopes := strings.Split(usr.Scopes, ",")
// check the current scopes
for _, item := range strings.Split(usr.Scopes, ",") {
if !us.doesScopeExist(scopes, item) {
newScopes = append(newScopes, item)
}
}
return us.repo.UpdateScopes(username, strings.Join(newScopes, ","))
}
func (us UserService) RemoveScopes(username string, scopes []string) error {
usr, err := us.repo.GetByName(username)
if err != nil {
return err
}
if usr.Name != username {
return errors.New(repositories.ErrUserNotFound)
}
var newScopes []string
// check all the scopes that are currently assigned
for _, item := range strings.Split(usr.Scopes, ",") {
// check the scopes given, if one matches skip it
if us.doesScopeExist(scopes, item) {
continue
}
// did not match, add it
newScopes = append(newScopes, item)
}
return us.repo.UpdateScopes(username, strings.Join(newScopes, ","))
}
func (us UserService) doesScopeExist(scopes []string, target string) bool {
for _, item := range scopes {
if item == target {
return true
}
}
return false
}
func (us UserService) CreateNewUser(name, password, scope string) (domain.UserEntity, error) {
err := us.CheckPasswordForRequirements(password) err := us.CheckPasswordForRequirements(password)
if err != nil { if err != nil {
return domain.UserEntity{}, err return domain.UserEntity{}, err
} }
us.repo.Create(name, password) us.repo.Create(name, password, domain.ScopeRecipeRead)
return domain.UserEntity{}, nil return domain.UserEntity{}, nil
} }

View File

@ -1,7 +1,41 @@
### Create a standard User
POST http://localhost:1323/api/v1/auth/register?username=test&password=test1234!
Content-Type: application/x-www-form-urlencoded
name=test&password=test1234!
### Login with user
POST http://localhost:1323/api/v1/auth/login
Content-Type: application/x-www-form-urlencoded
name=test&password=test1234!
### Login with the admin token
POST http://localhost:1323/api/v1/auth/login
Content-Type: application/x-www-form-urlencoded
password=lol
### Add Scope to test user
POST http://localhost:1323/api/v1/auth/scopes/add
Content-Type: application/json
Authorization: Bearer
{
"name": "test",
"scopes": [
"recipe:create"
]
}
### Remove scope from test user
POST http://localhost:1323/api/v1/auth/scopes/remove
Content-Type: application/json
Authorization: Bearer
### ###
POST http://localhost:1323/api/v1/register?username=test&password=test1234! POST http://localhost:1323/api/v1/
###
POST http://localhost:1323/api/v1/login?username=test&password=test1234!
### ###
GET http://localhost:1323/api/v1/demo/hello GET http://localhost:1323/api/v1/demo/hello
### ###