Skip to content

Commit

Permalink
IAM: Add Cache-Control max-age header to cachable IAM resources
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed May 27, 2024
1 parent 23b1a43 commit 598453d
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 7 deletions.
14 changes: 14 additions & 0 deletions auth/api/iam/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/nuts-foundation/nuts-node/http/cache"
"html/template"
"net/http"
"net/url"
Expand Down Expand Up @@ -81,6 +82,17 @@ const oid4vciSessionValidity = 15 * time.Minute
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
const userSessionCookieName = "__Host-SID"

// cacheControlMaxAgeURLs holds API endpoints that should have a max-age cache control header set.
var cacheControlMaxAgeURLs = []string{
"/.well-known/did.json",
"/iam/:id/did.json",
"/oauth2/:did/presentation_definition",
"/.well-known/oauth-authorization-server/iam/:id",
"/.well-known/oauth-authorization-server",
"/oauth2/:did/oauth-client",
"/statuslist/:did/:page",
}

//go:embed assets
var assetsFS embed.FS

Expand Down Expand Up @@ -140,6 +152,7 @@ func (r Wrapper) Routes(router core.EchoRouter) {
return next(c)
}
}, audit.Middleware(apiModuleName))
router.Use(cache.MaxAge(5*time.Minute, cacheControlMaxAgeURLs).Handle)
}

func (r Wrapper) strictMiddleware(ctx echo.Context, request interface{}, operationID string, f StrictHandlerFunc) (interface{}, error) {
Expand All @@ -159,6 +172,7 @@ func middleware(ctx echo.Context, operationID string) {
HtmlPageTemplate: assets.ErrorTemplate,
})
}

}

// ResolveStatusCode maps errors returned by this API to specific HTTP status codes.
Expand Down
35 changes: 28 additions & 7 deletions auth/api/iam/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,13 +705,34 @@ func TestWrapper_IntrospectAccessToken(t *testing.T) {
}

func TestWrapper_Routes(t *testing.T) {
ctrl := gomock.NewController(t)
router := core.NewMockEchoRouter(ctrl)

router.EXPECT().GET(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
router.EXPECT().POST(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()

(&Wrapper{}).Routes(router)
t.Run("it registers handlers", func(t *testing.T) {
ctrl := gomock.NewController(t)
router := core.NewMockEchoRouter(ctrl)

router.EXPECT().Use(gomock.Any()).AnyTimes()
router.EXPECT().GET(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
router.EXPECT().POST(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()

(&Wrapper{}).Routes(router)
})
t.Run("middleware cache-control: max-age URLs match registered paths", func(t *testing.T) {
ctrl := gomock.NewController(t)
router := core.NewMockEchoRouter(ctrl)

var registeredPaths []string
router.EXPECT().GET(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(path string, _ echo.HandlerFunc, _ ...echo.MiddlewareFunc) *echo.Route {
registeredPaths = append(registeredPaths, path)
return nil
}).AnyTimes()
router.EXPECT().POST(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
router.EXPECT().Use(gomock.Any()).AnyTimes()
(&Wrapper{}).Routes(router)

// Check that all cache-control max-age paths are actual paths
for _, path := range cacheControlMaxAgeURLs {
assert.Contains(t, registeredPaths, path)
}
})
}

func TestWrapper_middleware(t *testing.T) {
Expand Down
41 changes: 41 additions & 0 deletions http/cache/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cache

import (
"fmt"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"strings"
"time"
)

type Middleware struct {
Skipper middleware.Skipper
maxAge time.Duration
}

func (m Middleware) Handle(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if !m.Skipper(c) {
if m.maxAge > 0 {
c.Response().Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", int(m.maxAge.Seconds())))
}
}
return next(c)
}
}

// MaxAge creates a new middleware that sets the Cache-Control header to the given max-age for the given request URLs.
func MaxAge(maxAge time.Duration, requestURLs []string) Middleware {
return Middleware{
Skipper: func(c echo.Context) bool {
for _, curr := range requestURLs {
// trim leading and trailing /before comparing, just in case
if strings.Trim(c.Request().URL.Path, "/") == strings.Trim(curr, "/") {
return false
}
}
return true
},
maxAge: maxAge,
}
}
37 changes: 37 additions & 0 deletions http/cache/middleware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cache

import (
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/require"
"net/http/httptest"
"testing"
"time"
)

func TestMaxAge(t *testing.T) {
t.Run("match", func(t *testing.T) {
e := echo.New()
httpResponse := httptest.NewRecorder()
echoContext := e.NewContext(httptest.NewRequest("GET", "/a/", nil), httpResponse)

err := MaxAge(time.Minute, []string{"a", "b"}).Handle(func(c echo.Context) error {
return c.String(200, "OK")
})(echoContext)

require.NoError(t, err)
require.Equal(t, "max-age=60", httpResponse.Header().Get("Cache-Control"))
})
t.Run("no match", func(t *testing.T) {
e := echo.New()
httpResponse := httptest.NewRecorder()
echoContext := e.NewContext(httptest.NewRequest("GET", "/c", nil), httpResponse)

err := MaxAge(time.Minute, []string{"a", "b"}).Handle(func(c echo.Context) error {
return c.String(200, "OK")
})(echoContext)

require.NoError(t, err)
require.Empty(t, httpResponse.Header().Get("Cache-Control"))
})

}

0 comments on commit 598453d

Please sign in to comment.