Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9e10073
feat(middleware): implement authentication and context storage
nexus49 Sep 13, 2025
01644fd
test: increased unit test coverage
nexus49 Sep 13, 2025
613ac7f
chore: address lint warnings
nexus49 Sep 13, 2025
77a5ab3
chore(middleware): exclude test support from coverage
nexus49 Sep 13, 2025
3dc15df
refactor(middleware): remove deprecated CreateMiddlewares function
nexus49 Sep 13, 2025
354b35f
docs(middleware): update JWT parsing documentation
nexus49 Sep 13, 2025
174e71a
📝 Add docstrings to `feat/middlewares` (#65)
coderabbitai[bot] Sep 13, 2025
5cb4704
refactor(middleware): simplify CreateAuthMiddleware function
nexus49 Sep 13, 2025
4237a62
refactor(middleware): update context usage in StoreWebToken
nexus49 Sep 13, 2025
8a29af1
refactor(middleware): unexport the signatureAlgorithms variable
nexus49 Sep 13, 2025
48d891a
refactor(middleware): simplify CreateAuthMiddleware function
nexus49 Sep 13, 2025
4ca88f7
refactor(test): remove unused imports in authzMiddlewares_test.go
nexus49 Sep 13, 2025
382fea6
refactor(middleware): simplify authorization token parsing
nexus49 Sep 13, 2025
7bfce74
refactor(middleware): handle nil logger in StoreLoggerMiddleware
nexus49 Sep 13, 2025
4a86fd7
refactor(test): replace real looking token to avoid false-positive se…
nexus49 Sep 13, 2025
8982072
refactor: Improve auth middleware tests and add build constraints
nexus49 Sep 13, 2025
35d71b9
refactor(middleware): simplify auth middleware structure
nexus49 Sep 13, 2025
cf55d0c
Update middleware/auth.go
nexus49 Sep 14, 2025
ec597d7
Update middleware/requestid_test.go
nexus49 Sep 14, 2025
cbe5e72
Update middleware/token_test.go
nexus49 Sep 14, 2025
d01fc84
refactor(middleware): clean up auth middleware structure
nexus49 Sep 14, 2025
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
1 change: 1 addition & 0 deletions .testcoverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ exclude:
- mocks # exclude generated mock files
- ^test/
- ^logger/testlogger
- middleware/test_support

1 change: 1 addition & 0 deletions jwt/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type WebToken struct {

// New retrieves a new WebToken from an id_token string provided by OpenID communication
// When not able to parse or deserialize the requested claims, it will return an error
// JWT Claims are parsed without verification, ensure properer JWT verification before calling this function, eg. with istio
func New(idToken string, signatureAlgorithms []jose.SignatureAlgorithm) (webToken WebToken, err error) {
token, parseErr := jwt.ParseSigned(idToken, signatureAlgorithms)
if parseErr != nil {
Expand Down
26 changes: 26 additions & 0 deletions middleware/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package middleware

import (
"net/http"

"github.com/go-http-utils/headers"

appctx "github.com/platform-mesh/golang-commons/context"
)

// StoreAuthHeader returns HTTP middleware that reads the request's Authorization header and stores it in the request context.
// The middleware wraps a handler, extracts the Authorization header (using headers.Authorization), calls
// appctx.AddAuthHeaderToContext with the existing request context and the header value, and invokes the next handler
// with the request updated to use that context. If the Authorization header is absent or empty, nothing is stored.
func StoreAuthHeader() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
auth := request.Header.Get(headers.Authorization)
ctx := request.Context()
if auth != "" {
ctx = appctx.AddAuthHeaderToContext(ctx, auth)
}
next.ServeHTTP(responseWriter, request.WithContext(ctx))
})
}
}
101 changes: 101 additions & 0 deletions middleware/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package middleware

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

"github.com/go-http-utils/headers"
"github.com/platform-mesh/golang-commons/context"
"github.com/stretchr/testify/assert"
)

func TestStoreAuthHeader_WithAuthHeader(t *testing.T) {
expectedAuth := "Bearer token123"

nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify auth header is stored in context
authFromContext, err := context.GetAuthHeaderFromContext(r.Context())
assert.NoError(t, err)
assert.Equal(t, expectedAuth, authFromContext)

w.WriteHeader(http.StatusOK)
})

middleware := StoreAuthHeader()
handlerToTest := middleware(nextHandler)

req := httptest.NewRequest("GET", "http://testing", nil)
req.Header.Set(headers.Authorization, expectedAuth)
recorder := httptest.NewRecorder()

handlerToTest.ServeHTTP(recorder, req)

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

func TestStoreAuthHeader_WithoutAuthHeader(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify empty auth header returns error when not set
_, err := context.GetAuthHeaderFromContext(r.Context())
assert.Error(t, err) // Should return error when no auth header is set

w.WriteHeader(http.StatusOK)
})

middleware := StoreAuthHeader()
handlerToTest := middleware(nextHandler)

req := httptest.NewRequest("GET", "http://testing", nil)
// No authorization header set
recorder := httptest.NewRecorder()

handlerToTest.ServeHTTP(recorder, req)

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

func TestStoreAuthHeader_WithEmptyAuthHeader(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify empty auth header returns error when empty
_, err := context.GetAuthHeaderFromContext(r.Context())
assert.Error(t, err) // Should return error when auth header is empty

w.WriteHeader(http.StatusOK)
})

middleware := StoreAuthHeader()
handlerToTest := middleware(nextHandler)

req := httptest.NewRequest("GET", "http://testing", nil)
req.Header.Set(headers.Authorization, "")
recorder := httptest.NewRecorder()

handlerToTest.ServeHTTP(recorder, req)

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

func TestStoreAuthHeader_MultipleAuthHeaders(t *testing.T) {
// Test behavior when multiple authorization headers are present
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Should get the first header value since http.Header.Get returns only the first value
authFromContext, err := context.GetAuthHeaderFromContext(r.Context())
assert.NoError(t, err)
assert.Equal(t, "Bearer token1", authFromContext)

w.WriteHeader(http.StatusOK)
})

middleware := StoreAuthHeader()
handlerToTest := middleware(nextHandler)

req := httptest.NewRequest("GET", "http://testing", nil)
req.Header.Add(headers.Authorization, "Bearer token1")
req.Header.Add(headers.Authorization, "Bearer token2")
recorder := httptest.NewRecorder()

handlerToTest.ServeHTTP(recorder, req)

assert.Equal(t, http.StatusOK, recorder.Code)
}
18 changes: 18 additions & 0 deletions middleware/authzMiddlewares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package middleware

import (
"net/http"
)

// Middleware defines a function that wraps an http.Handler.
type Middleware func(http.Handler) http.Handler

// CreateAuthMiddleware returns a slice of Middleware functions for authentication and authorization.
// The returned middlewares are: StoreWebToken, StoreAuthHeader, and StoreSpiffeHeader.
func CreateAuthMiddleware() []Middleware {
return []Middleware{
StoreWebToken(),
StoreAuthHeader(),
StoreSpiffeHeader(),
}
}
32 changes: 32 additions & 0 deletions middleware/authzMiddlewares_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package middleware

import (
"testing"

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

func TestCreateAuthMiddleware(t *testing.T) {
middlewares := CreateAuthMiddleware()

// Expect 3 middlewares: StoreWebToken, StoreAuthHeader, StoreSpiffeHeader
assert.Len(t, middlewares, 3)

// Each middleware should not be nil
for _, mw := range middlewares {
assert.NotNil(t, mw)
}
}

func TestCreateAuthMiddleware_ReturnsCorrectMiddlewares(t *testing.T) {
middlewares := CreateAuthMiddleware()

// Should return exactly 3 middlewares
assert.Len(t, middlewares, 3)

// Each middleware should be a valid function
for _, mw := range middlewares {
assert.NotNil(t, mw)
// Signature is implicitly tested by compilation and return type
}
}
21 changes: 21 additions & 0 deletions middleware/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package middleware

import (
"net/http"

"github.com/platform-mesh/golang-commons/logger"
)

// StoreLoggerMiddleware returns an HTTP middleware that injects the provided
// logger into each request's context so downstream handlers can retrieve it.
func StoreLoggerMiddleware(log *logger.Logger) func(http.Handler) http.Handler {
if log == nil {
log = logger.StdLogger
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := logger.SetLoggerInContext(r.Context(), log)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
56 changes: 56 additions & 0 deletions middleware/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package middleware

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

"github.com/platform-mesh/golang-commons/logger"
"github.com/platform-mesh/golang-commons/logger/testlogger"
"github.com/stretchr/testify/assert"
)

func TestStoreLoggerMiddleware(t *testing.T) {
testLog := testlogger.New()

nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify logger is stored in context
logFromContext := logger.LoadLoggerFromContext(r.Context())
assert.NotNil(t, logFromContext)

// The logger should be the same instance we passed
assert.Equal(t, testLog.Logger, logFromContext)

w.WriteHeader(http.StatusOK)
})

middleware := StoreLoggerMiddleware(testLog.Logger)
handlerToTest := middleware(nextHandler)

req := httptest.NewRequest("GET", "http://testing", nil)
recorder := httptest.NewRecorder()

handlerToTest.ServeHTTP(recorder, req)

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

func TestStoreLoggerMiddleware_NilLogger(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Even with nil logger, the middleware should not panic
w.WriteHeader(http.StatusOK)
})

middleware := StoreLoggerMiddleware(nil)
handlerToTest := middleware(nextHandler)

req := httptest.NewRequest("GET", "http://testing", nil)
recorder := httptest.NewRecorder()

// Should not panic
assert.NotPanics(t, func() {
handlerToTest.ServeHTTP(recorder, req)
})

assert.Equal(t, http.StatusOK, recorder.Code)
}
18 changes: 18 additions & 0 deletions middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package middleware

import (
"net/http"

"github.com/platform-mesh/golang-commons/logger"
)

// attaches a request-scoped logger (using the provided logger), assigns a request ID, and propagates that ID into the logger.
func CreateMiddleware(log *logger.Logger) []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
SetOtelTracingContext(),
SentryRecoverer,
StoreLoggerMiddleware(log),
SetRequestId(),
SetRequestIdInLogger(),
}
}
41 changes: 41 additions & 0 deletions middleware/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package middleware

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

"github.com/platform-mesh/golang-commons/logger/testlogger"
"github.com/stretchr/testify/assert"
)

func TestCreateMiddleware(t *testing.T) {
log := testlogger.New()
middlewares := CreateMiddleware(log.Logger)

// Should return 5 middlewares
assert.Len(t, middlewares, 5)

// Each middleware should be a valid function
for _, mw := range middlewares {
assert.NotNil(t, mw)
}

// Test that middlewares can be chained
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

// Apply all middlewares
var finalHandler http.Handler = handler
for i := len(middlewares) - 1; i >= 0; i-- {
finalHandler = middlewares[i](finalHandler)
}

req := httptest.NewRequest("GET", "http://testing", nil)
recorder := httptest.NewRecorder()

finalHandler.ServeHTTP(recorder, req)

assert.Equal(t, http.StatusOK, recorder.Code)
}
25 changes: 25 additions & 0 deletions middleware/otel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package middleware

import (
"net/http"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)

// SetOtelTracingContext returns an HTTP middleware that extracts OpenTelemetry
// tracing context from incoming request headers and injects it into the request's
// context before passing the request to the next handler.
//
// The middleware uses the global OpenTelemetry text map propagator and
// propagation.HeaderCarrier to read trace/span context from the request headers.
// Any extraction behavior (including failure handling) is delegated to the
// propagator implementation.
func SetOtelTracingContext() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
ctx := otel.GetTextMapPropagator().Extract(request.Context(), propagation.HeaderCarrier(request.Header))
next.ServeHTTP(responseWriter, request.WithContext(ctx))
})
}
}
Loading