Compare commits

...

12 Commits

10 changed files with 298 additions and 138 deletions

View File

@ -1,4 +1,7 @@
package domain
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 {
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
type ErrorResponse struct {
HttpCode int `json:"code"`
Message string `json:"message"`
Success bool `json:"success"`
Message string `json:"message"`
}
type HelloWhoResponse struct {

View File

@ -5,134 +5,55 @@ import (
"go-cook/api/domain"
"go-cook/api/repositories"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
const (
ErrJwtMissing = "auth token is missing"
ErrJwtClaimsMissing = "claims missing on token"
ErrJwtExpired = "auth token has expired"
ErrJwtScopeMissing = "required scope is missing"
ErrUserNotFound = "requested user does not exist"
ErrJwtMissing = "auth token is missing"
ErrJwtClaimsMissing = "claims missing on token"
ErrJwtExpired = "auth token has expired"
ErrJwtScopeMissing = "required scope is missing"
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 {
username := c.QueryParam("username")
_, err := h.userRepo.GetByName(username)
username := c.FormValue("username")
password := c.FormValue("password")
//username := c.QueryParam("username")
exists, err := h.userRepo.GetByName(username)
if err != nil {
// 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 err.Error() != repositories.ErrUserNotFound {
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)
if err != nil {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError,
Message: err.Error(),
Success: false,
Message: err.Error(),
})
}
_, err = h.userRepo.Create(username, password)
_, err = h.userRepo.Create(username, password, domain.ScopeRecipeRead)
if err != nil {
return c.JSON(http.StatusInternalServerError, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError,
Message: err.Error(),
Success: false,
Message: err.Error(),
})
}
@ -140,21 +61,12 @@ func (h *Handler) AuthRegister(c echo.Context) error {
}
func (h *Handler) AuthLogin(c echo.Context) error {
username := c.QueryParam("username")
password := c.QueryParam("password")
username := c.FormValue("name")
password := c.FormValue("password")
// Check to see if they are trying to login with the admin token
if username == "" {
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)
return h.validateAdminToken(c, password)
}
// check if the user exists
@ -177,9 +89,55 @@ func (h *Handler) AuthLogin(c echo.Context) error {
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 {
return nil

View File

@ -37,10 +37,14 @@ func (h *Handler) Register(v1 *echo.Group) {
SigningKey: []byte(h.Config.JwtSecret),
}
v1.POST("/login", h.AuthLogin)
v1.POST("/register", h.AuthRegister)
demo := v1.Group("/demo")
auth := v1.Group("/auth")
auth.POST("/login", h.AuthLogin)
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/:who", h.HelloWho)
@ -61,14 +65,14 @@ func (h *Handler) Register(v1 *echo.Group) {
func (h *Handler) ReturnUnauthorizedResponse(c echo.Context, message string) error {
return c.JSON(http.StatusUnauthorized, domain.ErrorResponse{
HttpCode: http.StatusUnauthorized,
Message: message,
Success: false,
Message: message,
})
}
func (h *Handler) InternalServerErrorResponse(c echo.Context, message string) error {
return c.JSON(http.StatusServiceUnavailable, domain.ErrorResponse{
HttpCode: http.StatusInternalServerError,
Success: false,
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 {
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
UpdatePassword(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
@ -56,7 +56,7 @@ func (ur UserRepository) GetByName(name string) (domain.UserEntity, error) {
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)
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
if err != nil {
@ -66,8 +66,8 @@ func (ur UserRepository) Create(name, password string) (int64, error) {
dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("users")
queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt")
queryBuilder.Values(name, string(hash), dt, dt)
queryBuilder.Cols("Name", "Hash", "LastUpdated", "CreatedAt", "Scopes")
queryBuilder.Values(name, string(hash), dt, dt, scope)
query, args := queryBuilder.Build()
_, err = ur.connection.Exec(query, args...)
@ -110,7 +110,7 @@ func (ur UserRepository) CheckUserHash(name, password string) error {
return nil
}
func (ur UserRepository) AddScope(name, scope string) error {
func (ur UserRepository) UpdateScopes(name, scope string) error {
builder := sqlbuilder.NewUpdateBuilder()
builder.Update("users")
builder.Set (

View File

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

View File

@ -53,13 +53,70 @@ func (us UserService) GetUser(username string) (domain.UserEntity, error) {
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)
if err != nil {
return domain.UserEntity{}, err
}
us.repo.Create(name, password)
us.repo.Create(name, password, domain.ScopeRecipeRead)
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/login?username=test&password=test1234!
POST http://localhost:1323/api/v1/
###
GET http://localhost:1323/api/v1/demo/hello
###