Skip to content

Commit

Permalink
(feat) Add auth middleware (#30)
Browse files Browse the repository at this point in the history
* progress with jwt token generation

* progress with Login service; interface segregation refactor, check if everything behaves correctly; adding tests

* refactor with interface segregation and interface composition done properly

* progress with ed25519 key generation; still fails at unmarshalling

* interface segregation and composition working; testing working; login and jwt token generation working; pending auth middleware and public rsa pkcs8 key workflows
  • Loading branch information
mezdelex authored Sep 29, 2023
1 parent 300bb62 commit 10e6984
Show file tree
Hide file tree
Showing 31 changed files with 640 additions and 72 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ go.work

# Environment variables
.env

# Config
config.json
28 changes: 28 additions & 0 deletions application/dtos/login_dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dtos

import (
"regexp"
"strings"
)

type LoginDTO struct {
Email string
Password string
Token *string
}

// Validator
func (dto *LoginDTO) Validate() bool {
rfc2822EmailPattern := `[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?`
trimmedEmail := strings.TrimSpace((*dto).Email)
trimmedPassword := strings.TrimSpace((*dto).Password)

isOk, _ := regexp.MatchString(rfc2822EmailPattern, trimmedEmail)

// TODO: check password for certain special characters
if !isOk || len(trimmedPassword) < 8 || len(trimmedPassword) > 16 {
return false
}

return true
}
117 changes: 117 additions & 0 deletions application/dtos/login_validator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package dtos

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestLoginValidateShouldReturnFalseIfEmailIsNil(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Password: "aaaaaaaa",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnFalseIfEmailIsEmpty(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "",
Password: "aaaaaaaa",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnFalseIfEmailIsMalformed(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "[email protected]",
Password: "aaaaaaaa",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnFalseIfPasswordIsNil(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "[email protected]",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnFalseIfPasswordIsEmpty(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "[email protected]",
Password: "",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnFalseIfPasswordIsShorterThanEightCharacters(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "[email protected]",
Password: "aaaa",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnFalseIfPasswordIsLongerThanSixteenCharacters(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "[email protected]",
Password: "aaaaaaaaaaaaaaaaa",
}

// Act
result := loginDTO.Validate()

// Assert
assert.False(t, result)
}

func TestLoginValidateShouldReturnTrueIfAllRequiredValuesArePresent(t *testing.T) {
// Arrange
loginDTO := LoginDTO{
Email: "[email protected]",
Password: "aaaaaaaaaaa",
}

// Act
result := loginDTO.Validate()

// Assert
assert.True(t, result)
}
2 changes: 1 addition & 1 deletion application/dtos/user_dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (dto *UserDTO) Validate() bool {
isOk, _ := regexp.MatchString(rfc2822EmailPattern, trimmedEmail)

// TODO: check password for certain special characters
if strings.TrimSpace((*dto).Name) == "" || trimmedEmail == "" || !isOk || len(trimmedPassword) < 8 || len(trimmedPassword) > 16 {
if strings.TrimSpace((*dto).Name) == "" || !isOk || len(trimmedPassword) < 8 || len(trimmedPassword) > 16 {
return false
}

Expand Down
8 changes: 4 additions & 4 deletions application/dtos/user_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestUserValidateShouldReturnFalseIfEmailIsMalformed(t *testing.T) {
assert.False(t, result)
}

func TestUserValidateShouldReturnFalseIfIsPasswordIsNil(t *testing.T) {
func TestUserValidateShouldReturnFalseIfPasswordIsNil(t *testing.T) {
// Arrange
userDTO := UserDTO{
Name: "Test name",
Expand All @@ -126,7 +126,7 @@ func TestUserValidateShouldReturnFalseIfIsPasswordIsNil(t *testing.T) {
assert.False(t, result)
}

func TestUserValidateShouldReturnFalseIfIsPasswordIsEmpty(t *testing.T) {
func TestUserValidateShouldReturnFalseIfPasswordIsEmpty(t *testing.T) {
// Arrange
userDTO := UserDTO{
Name: "Test name",
Expand All @@ -141,7 +141,7 @@ func TestUserValidateShouldReturnFalseIfIsPasswordIsEmpty(t *testing.T) {
assert.False(t, result)
}

func TestUserValidateShouldReturnFalseIfIsPasswordIsShorterThanEightCharacters(t *testing.T) {
func TestUserValidateShouldReturnFalseIfPasswordIsShorterThanEightCharacters(t *testing.T) {
// Arrange
userDTO := UserDTO{
Name: "Test name",
Expand All @@ -156,7 +156,7 @@ func TestUserValidateShouldReturnFalseIfIsPasswordIsShorterThanEightCharacters(t
assert.False(t, result)
}

func TestUserValidateShouldReturnFalseIfIsPasswordIsLongerThan16Characters(t *testing.T) {
func TestUserValidateShouldReturnFalseIfPasswordIsLongerThanSixteenCharacters(t *testing.T) {
// Arrange
userDTO := UserDTO{
Name: "Test name",
Expand Down
8 changes: 5 additions & 3 deletions application/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

type Errors struct{}

func (_ Errors) FiberValidationError(itemName string) error {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid %s.", itemName))
}
func (_ Errors) CannotReadFileError(fileName string) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Could not read %s file.", fileName)) }
func (_ Errors) FiberValidationError(itemName string) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid %s.", itemName)) }
func (_ Errors) IncorrectPasswordError() error { return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintln("Incorrect password.")) }
func (_ Errors) ItemNotFoundError(itemName string) error { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("%s not found.", itemName)) }
func (_ Errors) ItemNotParsedError(itemName string) error { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("%s is not in PKCS8 RSA format.", itemName)) }
64 changes: 64 additions & 0 deletions application/services/login_service_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package services

import (
"context"
"fmt"
"os"
"time"

"github.com/golang-jwt/jwt/v5"
"todoapp.com/application/dtos"
"todoapp.com/application/errors"
"todoapp.com/domain/interfaces"
"todoapp.com/domain/models"
)

type LoginServiceImpl struct {
userRepository interfaces.UsersRepositoryEmail
config *models.Config
}

func NewLoginService(userRepository interfaces.UsersRepositoryEmail, config *models.Config) *LoginServiceImpl {
return &LoginServiceImpl{
userRepository: userRepository,
config: config,
}
}

func (ls *LoginServiceImpl) Login(context context.Context, login *dtos.LoginDTO) error {
dbUser := ls.userRepository.GetByEmail(context, &login.Email)

if dbUser.Email == "" {
return errors.Errors{}.ItemNotFoundError("User")
}
if dbUser.Password != (*login).Password {
return errors.Errors{}.IncorrectPasswordError()
}

return ls.GenerateToken(login)
}

func (ls *LoginServiceImpl) GenerateToken(login *dtos.LoginDTO) error {
fmt.Println(ls.config.PrivateKeyPath)
fmt.Println("Llego aquí")
encodedKey, error := os.ReadFile(ls.config.PrivateKeyPath)
if error != nil {
return errors.Errors{}.CannotReadFileError("OPENSSH private key")
}

privateKey, error := jwt.ParseRSAPrivateKeyFromPEM(encodedKey)
if error != nil {
return errors.Errors{}.ItemNotParsedError("OPENSSH private key")
}

claims := jwt.MapClaims{
"email": login.Email,
"expiration": time.Now().UTC().Add(time.Hour * 24).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

signedToken, error := token.SignedString(privateKey)
login.Token = &signedToken

return error
}
119 changes: 119 additions & 0 deletions application/services/login_service_impl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package services

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"todoapp.com/application/dtos"
"todoapp.com/domain/models"
)

func TestLoginGenerateTokenShouldReturnTokenAndNoError(t *testing.T) {
// Arrange
testLoginDTO := &dtos.LoginDTO{
Email: "[email protected]",
Password: "aaaaaaaa",
}
testConfig := &models.Config{
PrivateKeyPath: "../../testing/id_rsa",
PublicKeyPath: "../../testing/id_rsa.pub",
}
MockedUsersRepository := new(MockedUsersRepository)

// Act
testLoginService := NewLoginService(MockedUsersRepository, testConfig)
error := testLoginService.GenerateToken(testLoginDTO)

// Assert
assert.Nil(t, error)
assert.NotNil(t, testLoginDTO.Token)
assert.NotEmpty(t, testLoginDTO.Token)
}

func TestLoginLoginShouldReturnErrorIfGivenPasswordDoesNotMatch(t *testing.T) {
// Arrange
id1 := uint(1)
testUser := models.User{
ID: &id1,
Name: "test 1",
Email: "[email protected]",
Password: "aaaaaaaa",
}
testLoginDTO := &dtos.LoginDTO{
Email: "[email protected]",
Password: "bbbbb",
}
testContext := context.Background()
MockedUserRepository := new(MockedUsersRepository)
MockedUserRepository.mock.On("GetByEmail", testContext, &testLoginDTO.Email).Return(testUser)
testConfig := &models.Config{
PrivateKeyPath: "../../testing/id_rsa",
PublicKeyPath: "../../testing/id_rsa.pub",
}

// Act
testLoginService := NewLoginService(MockedUserRepository, testConfig)
error := testLoginService.Login(testContext, testLoginDTO)

// Assert
assert.NotNil(t, error)
}

func TestLoginLoginShouldReturnErrorIfUserIsNotFound(t *testing.T) {
// Arrange
id1 := uint(1)
testUser := models.User{
ID: &id1,
Name: "test 1",
Email: "[email protected]",
Password: "aaaaaaaa",
}
testLoginDTO := &dtos.LoginDTO{
Email: "[email protected]",
Password: "aaaaaaaa",
}
testContext := context.Background()
MockedUserRepository := new(MockedUsersRepository)
MockedUserRepository.mock.On("GetByEmail", testContext, &testLoginDTO.Email).Return(testUser)
testConfig := &models.Config{
PrivateKeyPath: "../../testing/id_rsa",
PublicKeyPath: "../../testing/id_rsa.pub",
}

// Act
testLoginService := NewLoginService(MockedUserRepository, testConfig)
error := testLoginService.Login(testContext, testLoginDTO)

// Assert
assert.NotNil(t, error)
}

func TestLoginLoginShouldNotReturnAnyErrorsIfEverythingIsOk(t *testing.T) {
// Arrange
id1 := uint(1)
testUser := models.User{
ID: &id1,
Name: "test 1",
Email: "[email protected]",
Password: "aaaaaaaa",
}
testLoginDTO := &dtos.LoginDTO{
Email: "[email protected]",
Password: "aaaaaaaa",
}
testContext := context.Background()
MockedUserRepository := new(MockedUsersRepository)
MockedUserRepository.mock.On("GetByEmail", testContext, &testLoginDTO.Email).Return(testUser)
testConfig := &models.Config{
PrivateKeyPath: "../../testing/id_rsa",
PublicKeyPath: "../../testing/id_rsa.pub",
}

// Act
testLoginService := NewLoginService(MockedUserRepository, testConfig)
error := testLoginService.Login(testContext, testLoginDTO)

// Assert
assert.Nil(t, error)
}
Loading

0 comments on commit 10e6984

Please sign in to comment.