Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bin/nestled
Binary file not shown.
64 changes: 64 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package auth

import (
"fmt"
"time"

"github.com/albertoadami/nestled/internal/errors"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)

type Token struct {
Value string
ExpirationTime time.Time
}

type TokenInfo struct {
UserId string
ExpirationTime time.Time
}

func GenerateToken(userId uuid.UUID, secretKey string, expireHours int) (*Token, error) {
expirationTime := time.Now().Add(time.Hour * time.Duration(expireHours)).Local().UTC()

claims := jwt.MapClaims{
"userId": userId.String(),
"exp": expirationTime.Unix(),
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(secretKey))

if err != nil {
return nil, errors.ErrGeneratingToken
}

return &Token{
Value: signed,
ExpirationTime: expirationTime,
}, nil
}

func ParseToken(tokenValue string, secretKey string) (*TokenInfo, error) {
token, err := jwt.Parse(tokenValue, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(secretKey), nil
})

if err != nil || !token.Valid {
return nil, errors.ErrInvalidToken
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.ErrInvalidToken
}

return &TokenInfo{
UserId: claims["userId"].(string),
ExpirationTime: time.Unix(int64(claims["exp"].(float64)), 0).UTC(),
}, nil
}
46 changes: 46 additions & 0 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package auth

import (
"testing"
"time"

"github.com/albertoadami/nestled/internal/errors"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

func TestGeneratingToken(t *testing.T) {
userId := uuid.New()
secretKey := "test_secret_key"
expireHours := 6

token, err := GenerateToken(userId, secretKey, expireHours)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
assert.NotNil(t, token.Value, "expected token value to be set, got nil")

}

func TestParsingInvalidToken(t *testing.T) {
token := "invalid.token.value"

_, err := ParseToken(token, "test_secret_key")

assert.ErrorIs(t, err, errors.ErrInvalidToken)

}

func TestParsingValidTokenAndExtractInfo(t *testing.T) {
userId := uuid.New()
secretKey := "test_secret_key"

token, err := GenerateToken(userId, secretKey, 6)
assert.NoError(t, err)

tokenInfo, err := ParseToken(token.Value, secretKey)
assert.NoError(t, err)

assert.Equal(t, userId.String(), tokenInfo.UserId)
assert.Equal(t, token.ExpirationTime.Truncate(time.Second), tokenInfo.ExpirationTime.Truncate(time.Second))
}
35 changes: 0 additions & 35 deletions internal/auth/jwt.go

This file was deleted.

21 changes: 0 additions & 21 deletions internal/auth/jwt_test.go

This file was deleted.

1 change: 1 addition & 0 deletions internal/dto/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ func NewErrorResponse(message string, details string) *ErrorResponse {
const (
ErrUsernameAlreadyExists = "username already exists"
ErrEmailAlreadyExists = "email already exists"
ErrInvalidToken = "invalid token"
)
2 changes: 2 additions & 0 deletions internal/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ var (
ErrEmailAlreadyExists = errors.New("email already exists")
ErrPasswordTooWeak = errors.New("password is too weak")
CredentialsInvalid = errors.New("invalid credentials")
ErrInvalidToken = errors.New("invalid token")
ErrGeneratingToken = errors.New("error generating token")
)
34 changes: 34 additions & 0 deletions internal/middleware/authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package middleware

import (
"net/http"
"strings"

"github.com/albertoadami/nestled/internal/auth"
"github.com/albertoadami/nestled/internal/dto"
"github.com/gin-gonic/gin"
)

func BearerAuthentication(secretKey string) gin.HandlerFunc {

return func(c *gin.Context) {

authHeader := c.GetHeader("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, &dto.ErrorResponse{Message: "missing or invalid Authorization header"})
return
}

token := strings.TrimPrefix(authHeader, "Bearer ")
tokenInfo, err := auth.ParseToken(token, secretKey)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, &dto.ErrorResponse{Message: "invalid token"})
return
}

c.Set("userId", tokenInfo.UserId)
c.Next()

}

}
77 changes: 77 additions & 0 deletions internal/middleware/authentication_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package middleware

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/albertoadami/nestled/internal/auth"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)

func setupMiddlewareRouter(secretKey string) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(BearerAuthentication(secretKey))
router.GET("/test", func(c *gin.Context) {
userId, _ := c.Get("userId")
c.JSON(http.StatusOK, gin.H{"userId": userId})
})
return router
}

func TestBearerAuthentication_MissingHeader(t *testing.T) {
router := setupMiddlewareRouter("secret")

req, _ := http.NewRequest("GET", "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusUnauthorized, w.Code)
}

func TestBearerAuthentication_InvalidToken(t *testing.T) {
router := setupMiddlewareRouter("secret")

req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer invalid.token.here")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusUnauthorized, w.Code)
}

func TestBearerAuthentication_ValidToken(t *testing.T) {
secretKey := "secret"
userId := uuid.New()

token, _ := auth.GenerateToken(userId, secretKey, 6)

router := setupMiddlewareRouter(secretKey)

req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token.Value)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
}

func TestBearerAuthentication_ExpiredToken(t *testing.T) {
secretKey := "secret"
userId := uuid.New()

// generate token already expired (-1 hour)
token, _ := auth.GenerateToken(userId, secretKey, -1)

router := setupMiddlewareRouter(secretKey)

req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("Authorization", "Bearer "+token.Value)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusUnauthorized, w.Code)
}