Skip to content

Commit 4ace4ab

Browse files
authored
feat: adding various middlewares for services (#64)
1 parent 698f3ed commit 4ace4ab

21 files changed

+1175
-0
lines changed

.testcoverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ exclude:
44
- mocks # exclude generated mock files
55
- ^test/
66
- ^logger/testlogger
7+
- middleware/test_support
78

jwt/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type WebToken struct {
3333

3434
// New retrieves a new WebToken from an id_token string provided by OpenID communication
3535
// When not able to parse or deserialize the requested claims, it will return an error
36+
// JWT Claims are parsed without verification, ensure properer JWT verification before calling this function, eg. with istio
3637
func New(idToken string, signatureAlgorithms []jose.SignatureAlgorithm) (webToken WebToken, err error) {
3738
token, parseErr := jwt.ParseSigned(idToken, signatureAlgorithms)
3839
if parseErr != nil {

middleware/auth.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/go-http-utils/headers"
7+
8+
appctx "github.com/platform-mesh/golang-commons/context"
9+
)
10+
11+
// StoreAuthHeader returns HTTP middleware that reads the request's Authorization header and stores it in the request context.
12+
// The middleware wraps a handler, extracts the Authorization header (using headers.Authorization), calls
13+
// appctx.AddAuthHeaderToContext with the existing request context and the header value, and invokes the next handler
14+
// with the request updated to use that context. If the Authorization header is absent or empty, nothing is stored.
15+
func StoreAuthHeader() func(http.Handler) http.Handler {
16+
return func(next http.Handler) http.Handler {
17+
return http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
18+
auth := request.Header.Get(headers.Authorization)
19+
ctx := request.Context()
20+
if auth != "" {
21+
ctx = appctx.AddAuthHeaderToContext(ctx, auth)
22+
}
23+
next.ServeHTTP(responseWriter, request.WithContext(ctx))
24+
})
25+
}
26+
}

middleware/auth_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/go-http-utils/headers"
9+
"github.com/platform-mesh/golang-commons/context"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestStoreAuthHeader_WithAuthHeader(t *testing.T) {
14+
expectedAuth := "Bearer token123"
15+
16+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
// Verify auth header is stored in context
18+
authFromContext, err := context.GetAuthHeaderFromContext(r.Context())
19+
assert.NoError(t, err)
20+
assert.Equal(t, expectedAuth, authFromContext)
21+
22+
w.WriteHeader(http.StatusOK)
23+
})
24+
25+
middleware := StoreAuthHeader()
26+
handlerToTest := middleware(nextHandler)
27+
28+
req := httptest.NewRequest("GET", "http://testing", nil)
29+
req.Header.Set(headers.Authorization, expectedAuth)
30+
recorder := httptest.NewRecorder()
31+
32+
handlerToTest.ServeHTTP(recorder, req)
33+
34+
assert.Equal(t, http.StatusOK, recorder.Code)
35+
}
36+
37+
func TestStoreAuthHeader_WithoutAuthHeader(t *testing.T) {
38+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39+
// Verify empty auth header returns error when not set
40+
_, err := context.GetAuthHeaderFromContext(r.Context())
41+
assert.Error(t, err) // Should return error when no auth header is set
42+
43+
w.WriteHeader(http.StatusOK)
44+
})
45+
46+
middleware := StoreAuthHeader()
47+
handlerToTest := middleware(nextHandler)
48+
49+
req := httptest.NewRequest("GET", "http://testing", nil)
50+
// No authorization header set
51+
recorder := httptest.NewRecorder()
52+
53+
handlerToTest.ServeHTTP(recorder, req)
54+
55+
assert.Equal(t, http.StatusOK, recorder.Code)
56+
}
57+
58+
func TestStoreAuthHeader_WithEmptyAuthHeader(t *testing.T) {
59+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60+
// Verify empty auth header returns error when empty
61+
_, err := context.GetAuthHeaderFromContext(r.Context())
62+
assert.Error(t, err) // Should return error when auth header is empty
63+
64+
w.WriteHeader(http.StatusOK)
65+
})
66+
67+
middleware := StoreAuthHeader()
68+
handlerToTest := middleware(nextHandler)
69+
70+
req := httptest.NewRequest("GET", "http://testing", nil)
71+
req.Header.Set(headers.Authorization, "")
72+
recorder := httptest.NewRecorder()
73+
74+
handlerToTest.ServeHTTP(recorder, req)
75+
76+
assert.Equal(t, http.StatusOK, recorder.Code)
77+
}
78+
79+
func TestStoreAuthHeader_MultipleAuthHeaders(t *testing.T) {
80+
// Test behavior when multiple authorization headers are present
81+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82+
// Should get the first header value since http.Header.Get returns only the first value
83+
authFromContext, err := context.GetAuthHeaderFromContext(r.Context())
84+
assert.NoError(t, err)
85+
assert.Equal(t, "Bearer token1", authFromContext)
86+
87+
w.WriteHeader(http.StatusOK)
88+
})
89+
90+
middleware := StoreAuthHeader()
91+
handlerToTest := middleware(nextHandler)
92+
93+
req := httptest.NewRequest("GET", "http://testing", nil)
94+
req.Header.Add(headers.Authorization, "Bearer token1")
95+
req.Header.Add(headers.Authorization, "Bearer token2")
96+
recorder := httptest.NewRecorder()
97+
98+
handlerToTest.ServeHTTP(recorder, req)
99+
100+
assert.Equal(t, http.StatusOK, recorder.Code)
101+
}

middleware/authzMiddlewares.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
)
6+
7+
// Middleware defines a function that wraps an http.Handler.
8+
type Middleware func(http.Handler) http.Handler
9+
10+
// CreateAuthMiddleware returns a slice of Middleware functions for authentication and authorization.
11+
// The returned middlewares are: StoreWebToken, StoreAuthHeader, and StoreSpiffeHeader.
12+
func CreateAuthMiddleware() []Middleware {
13+
return []Middleware{
14+
StoreWebToken(),
15+
StoreAuthHeader(),
16+
StoreSpiffeHeader(),
17+
}
18+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package middleware
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestCreateAuthMiddleware(t *testing.T) {
10+
middlewares := CreateAuthMiddleware()
11+
12+
// Expect 3 middlewares: StoreWebToken, StoreAuthHeader, StoreSpiffeHeader
13+
assert.Len(t, middlewares, 3)
14+
15+
// Each middleware should not be nil
16+
for _, mw := range middlewares {
17+
assert.NotNil(t, mw)
18+
}
19+
}
20+
21+
func TestCreateAuthMiddleware_ReturnsCorrectMiddlewares(t *testing.T) {
22+
middlewares := CreateAuthMiddleware()
23+
24+
// Should return exactly 3 middlewares
25+
assert.Len(t, middlewares, 3)
26+
27+
// Each middleware should be a valid function
28+
for _, mw := range middlewares {
29+
assert.NotNil(t, mw)
30+
// Signature is implicitly tested by compilation and return type
31+
}
32+
}

middleware/logger.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/platform-mesh/golang-commons/logger"
7+
)
8+
9+
// StoreLoggerMiddleware returns an HTTP middleware that injects the provided
10+
// logger into each request's context so downstream handlers can retrieve it.
11+
func StoreLoggerMiddleware(log *logger.Logger) func(http.Handler) http.Handler {
12+
if log == nil {
13+
log = logger.StdLogger
14+
}
15+
return func(next http.Handler) http.Handler {
16+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
ctx := logger.SetLoggerInContext(r.Context(), log)
18+
next.ServeHTTP(w, r.WithContext(ctx))
19+
})
20+
}
21+
}

middleware/logger_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/platform-mesh/golang-commons/logger"
9+
"github.com/platform-mesh/golang-commons/logger/testlogger"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestStoreLoggerMiddleware(t *testing.T) {
14+
testLog := testlogger.New()
15+
16+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
// Verify logger is stored in context
18+
logFromContext := logger.LoadLoggerFromContext(r.Context())
19+
assert.NotNil(t, logFromContext)
20+
21+
// The logger should be the same instance we passed
22+
assert.Equal(t, testLog.Logger, logFromContext)
23+
24+
w.WriteHeader(http.StatusOK)
25+
})
26+
27+
middleware := StoreLoggerMiddleware(testLog.Logger)
28+
handlerToTest := middleware(nextHandler)
29+
30+
req := httptest.NewRequest("GET", "http://testing", nil)
31+
recorder := httptest.NewRecorder()
32+
33+
handlerToTest.ServeHTTP(recorder, req)
34+
35+
assert.Equal(t, http.StatusOK, recorder.Code)
36+
}
37+
38+
func TestStoreLoggerMiddleware_NilLogger(t *testing.T) {
39+
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40+
// Even with nil logger, the middleware should not panic
41+
w.WriteHeader(http.StatusOK)
42+
})
43+
44+
middleware := StoreLoggerMiddleware(nil)
45+
handlerToTest := middleware(nextHandler)
46+
47+
req := httptest.NewRequest("GET", "http://testing", nil)
48+
recorder := httptest.NewRecorder()
49+
50+
// Should not panic
51+
assert.NotPanics(t, func() {
52+
handlerToTest.ServeHTTP(recorder, req)
53+
})
54+
55+
assert.Equal(t, http.StatusOK, recorder.Code)
56+
}

middleware/middleware.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/platform-mesh/golang-commons/logger"
7+
)
8+
9+
// attaches a request-scoped logger (using the provided logger), assigns a request ID, and propagates that ID into the logger.
10+
func CreateMiddleware(log *logger.Logger) []func(http.Handler) http.Handler {
11+
return []func(http.Handler) http.Handler{
12+
SetOtelTracingContext(),
13+
SentryRecoverer,
14+
StoreLoggerMiddleware(log),
15+
SetRequestId(),
16+
SetRequestIdInLogger(),
17+
}
18+
}

middleware/middleware_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/platform-mesh/golang-commons/logger/testlogger"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestCreateMiddleware(t *testing.T) {
13+
log := testlogger.New()
14+
middlewares := CreateMiddleware(log.Logger)
15+
16+
// Should return 5 middlewares
17+
assert.Len(t, middlewares, 5)
18+
19+
// Each middleware should be a valid function
20+
for _, mw := range middlewares {
21+
assert.NotNil(t, mw)
22+
}
23+
24+
// Test that middlewares can be chained
25+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+
w.WriteHeader(http.StatusOK)
27+
})
28+
29+
// Apply all middlewares
30+
var finalHandler http.Handler = handler
31+
for i := len(middlewares) - 1; i >= 0; i-- {
32+
finalHandler = middlewares[i](finalHandler)
33+
}
34+
35+
req := httptest.NewRequest("GET", "http://testing", nil)
36+
recorder := httptest.NewRecorder()
37+
38+
finalHandler.ServeHTTP(recorder, req)
39+
40+
assert.Equal(t, http.StatusOK, recorder.Code)
41+
}

0 commit comments

Comments
 (0)