Repository: eser/golang-service-template (language: go)

Issue:
Feature request: a middleware for Service-to-Service authentication
**Is your feature request related to a problem? Please describe.**

A middleware needs to be designed to handle service-to-service authentication within the project. This middleware will ensure secure communication between internal services by validating authentication tokens or credentials.



Discussion / hints:

Fix patch:
diff --git a/go.mod b/go.mod
index f1013bb..1315880 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/eser/go-service
 go 1.23.0
 
 require (
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/getkin/kin-openapi v0.127.0
 	github.com/oklog/ulid/v2 v2.1.0
 	github.com/stretchr/testify v1.9.0
diff --git a/go.sum b/go.sum
index f1cbfeb..fb82c07 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
 github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
diff --git a/pkg/app/mod.go b/pkg/app/mod.go
index fac3d47..4b181ee 100644
--- a/pkg/app/mod.go
+++ b/pkg/app/mod.go
@@ -45,6 +45,7 @@ func RegisterRoutes(routes httpfx.Router, appConfig *AppConfig) {
 	routes.Use(middlewares.ErrorHandlerMiddleware())
 	routes.Use(middlewares.ResponseTimeMiddleware())
 	routes.Use(middlewares.CorrelationIdMiddleware())
+	routes.Use(middlewares.AuthMiddleware())
 	routes.Use(middlewares.CorsMiddleware())
 
 	routes.
diff --git a/pkg/bliss/httpfx/middlewares/auth-middleware.go b/pkg/bliss/httpfx/middlewares/auth-middleware.go
new file mode 100644
index 0000000..918398b
--- /dev/null
+++ b/pkg/bliss/httpfx/middlewares/auth-middleware.go
@@ -0,0 +1,52 @@
+package middlewares
+
+import (
+	"github.com/dgrijalva/jwt-go"
+	"github.com/eser/go-service/pkg/bliss/httpfx"
+	"strings"
+	"time"
+)
+
+func AuthMiddleware() httpfx.Handler {
+	return func(ctx *httpfx.Context) httpfx.Response {
+		authHeader := ctx.Request.Header.Get("Authorization")
+		if authHeader == "" {
+			return httpfx.Response{
+				StatusCode: 401,
+				Body:       []byte("Authorization header is missing"),
+			}
+		}
+
+		tokenString := strings.TrimPrefix(authHeader, "Bearer ")
+		if tokenString == "" {
+			return httpfx.Response{
+				StatusCode: 401,
+				Body:       []byte("Token is missing"),
+			}
+		}
+
+		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+				return httpfx.Response{StatusCode: 401, Body: nil}, nil
+			}
+			return []byte("secret"), nil
+		})
+
+		if err != nil || !token.Valid {
+			return httpfx.Response{StatusCode: 401, Body: []byte(err.Error())}
+		}
+
+		claims, ok := token.Claims.(jwt.MapClaims)
+		if !ok || !token.Valid {
+			return httpfx.Response{StatusCode: 401, Body: []byte("Invalid token")}
+		}
+
+		if exp, ok := claims["exp"].(float64); ok {
+			if time.Unix(int64(exp), 0).Before(time.Now()) {
+				return httpfx.Response{StatusCode: 401, Body: []byte("Token is expired")}
+			}
+		}
+
+		return ctx.Next()
+	}
+}


Test patch:
diff --git a/pkg/bliss/httpfx/middlewares/auth-middleware_test.go b/pkg/bliss/httpfx/middlewares/auth-middleware_test.go
new file mode 100644
index 0000000..1c33220
--- /dev/null
+++ b/pkg/bliss/httpfx/middlewares/auth-middleware_test.go
@@ -0,0 +1,76 @@
+package middlewares
+
+import (
+	"github.com/eser/go-service/pkg/bliss/httpfx"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"github.com/dgrijalva/jwt-go"
+)
+
+func createToken(secret string, exp time.Time) string {
+	claims := jwt.MapClaims{
+		"exp": exp.Unix(),
+	}
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	tokenString, _ := token.SignedString([]byte(secret))
+	return tokenString
+}
+
+func TestAuthMiddleware(t *testing.T) {
+	tests := []struct {
+		name         string
+		token        string
+		expectedCode int
+	}{
+		{
+			name:         "No Authorization Header",
+			token:        "",
+			expectedCode: http.StatusUnauthorized,
+		},
+		{
+			name:         "Invalid Token Format",
+			token:        "InvalidToken",
+			expectedCode: http.StatusUnauthorized,
+		},
+		{
+			name:         "Expired Token",
+			token:        createToken("secret", time.Now().Add(-time.Hour)),
+			expectedCode: http.StatusUnauthorized,
+		},
+		{
+			name:         "Valid Token",
+			token:        createToken("secret", time.Now().Add(time.Hour)),
+			expectedCode: http.StatusOK,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := httptest.NewRequest(http.MethodGet, "/", nil)
+			if tt.token != "" {
+				req.Header.Set("Authorization", "Bearer "+tt.token)
+			}
+			res := httptest.NewRecorder()
+			ctx := httpfx.Context{
+				Request:        req,
+				ResponseWriter: res,
+			}
+
+			middleware := AuthMiddleware()
+			result := middleware(&ctx)
+
+			if result.StatusCode != tt.expectedCode {
+				t.Errorf("Expected status code %d, got %d", tt.expectedCode, result.StatusCode)
+			}
+
+			if tt.expectedCode == http.StatusOK {
+				if res.Header().Get("Authorization") == "" {
+					t.Error("Authorization header is missing")
+				}
+			}
+		})
+	}
+}