Thanks to visit codestin.com
Credit goes to github.com

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
9 changes: 9 additions & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,11 @@ func New(options *Options) *API {
r.Route("/oauth2", func(r chi.Router) {
r.Use(
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2),
// Every response from this tree may carry a credential, so none of
// them may be retained by an intermediary cache. Mounted after
// the gate, so a request the gate rejects gets no headers. That
// rejection carries no credential, so it needs none.
httpmw.NoStore,
)
r.Route("/authorize", func(r chi.Router) {
r.Use(
Expand Down Expand Up @@ -2108,6 +2113,10 @@ func New(options *Options) *API {
r.Use(
apiKeyMiddleware,
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentOAuth2),
// POST /apps/{app}/secrets returns a plaintext client secret,
// so this tree falls under the same RFC 6749 §5.1 requirement
// as /oauth2.
httpmw.NoStore,
)
r.Route("/apps", func(r chi.Router) {
r.Get("/", api.oAuth2ProviderApps())
Expand Down
29 changes: 29 additions & 0 deletions coderd/httpmw/nostore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package httpmw

import "net/http"

// NoStore sets the response caching headers that OAuth2 requires on any
Comment thread
BobbyHo marked this conversation as resolved.
// response that may contain a credential. RFC 6749 §5.1 makes both headers a
// MUST for the authorization server; OAuth 2.1 §3.2.3 keeps only no-store,
// because RFC 9111 §5.4 deprecates Pragma as a request-only field. Both are
// sent so that a client or auditor reading either specification sees a
// conformant response.
//
// The headers are set before the wrapped handler runs, so a handler that
// writes its own Cache-Control would win. None does today; the integration
// tests pin that across the /oauth2 tree and spot-check
// /api/v2/oauth2-provider. Pragma is written unconditionally, so such a
// handler's Cache-Control ships alongside Pragma: no-cache.
//
// chi's middleware.NoCache is not used, though that package is already
// imported at the mount site. It strips the ETag-family headers from the
// request, which an authorization server has no business doing, and it sends
// directives neither specification asks for, where OAuth 2.1 narrows the
// requirement rather than widening it.
func NoStore(next http.Handler) http.Handler {
Comment thread
BobbyHo marked this conversation as resolved.
Comment thread
BobbyHo marked this conversation as resolved.
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("Cache-Control", "no-store")
rw.Header().Set("Pragma", "no-cache")
next.ServeHTTP(rw, r)
})
}
102 changes: 102 additions & 0 deletions coderd/httpmw/nostore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package httpmw_test

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

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/httpmw"
)

func TestNoStore(t *testing.T) {
t.Parallel()

tests := []struct {
Name string
Handler http.HandlerFunc

expectStatus int
expectCacheControl string
expectPragma string
assert func(t *testing.T, res *httptest.ResponseRecorder)
}{
{
// The POST /oauth2/tokens shape.
Name: "OK",
Handler: func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(`{"access_token":"secret"}`))
},
expectStatus: http.StatusOK,
expectCacheControl: "no-store",
expectPragma: "no-cache",
},
{
// The DELETE /oauth2/clients/{client_id} shape: headers on a
// response with no body.
Name: "NoContent",
Handler: func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusNoContent)
},
expectStatus: http.StatusNoContent,
expectCacheControl: "no-store",
expectPragma: "no-cache",
},
{
// The POST /oauth2/authorize shape: http.Redirect writes its own
// headers without clearing the map.
Name: "Redirect",
Handler: func(rw http.ResponseWriter, r *http.Request) {
http.Redirect(rw, r, "https://example.com/callback?code=abc", http.StatusFound)
},
expectStatus: http.StatusFound,
expectCacheControl: "no-store",
expectPragma: "no-cache",
assert: func(t *testing.T, res *httptest.ResponseRecorder) {
require.Equal(t, "https://example.com/callback?code=abc", res.Header().Get("Location"))
},
},
{
// The revoke.go and registration.go shape: a bare WriteHeader,
// never httpapi.Write.
Name: "BareWriteHeaderError",
Handler: func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(http.StatusBadRequest)
},
expectStatus: http.StatusBadRequest,
expectCacheControl: "no-store",
expectPragma: "no-cache",
},
{
// The headers are advisory: a handler that writes its own
// Cache-Control wins, and Pragma survives alongside it.
Name: "HandlerOverwrites",
Handler: func(rw http.ResponseWriter, _ *http.Request) {
rw.Header().Set("Cache-Control", "private")
rw.WriteHeader(http.StatusOK)
},
expectStatus: http.StatusOK,
expectCacheControl: "private",
expectPragma: "no-cache",
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()

req := httptest.NewRequest(http.MethodGet, "/", nil)
res := httptest.NewRecorder()
httpmw.NoStore(tt.Handler).ServeHTTP(res, req)

require.Equal(t, tt.expectStatus, res.Code)
require.Equal(t, tt.expectCacheControl, res.Header().Get("Cache-Control"))
require.Equal(t, tt.expectPragma, res.Header().Get("Pragma"))
if tt.assert != nil {
tt.assert(t, res)
}
})
}
}
Loading
Loading