diff --git a/backend/internal/middleware/csp_middleware.go b/backend/internal/middleware/csp_middleware.go index 2b6f9f88..b3ccfda5 100644 --- a/backend/internal/middleware/csp_middleware.go +++ b/backend/internal/middleware/csp_middleware.go @@ -21,12 +21,8 @@ func (m *CspMiddleware) Add() gin.HandlerFunc { // Generate a random base64 nonce for this request nonce := utils.GenerateCSPNonce() utils.SetCSPNonce(c, nonce) - c.Writer.Header().Set("Content-Security-Policy", BuildCSP(nonce)) + c.Writer.Header().Set("Content-Security-Policy", utils.BuildCSP(nonce)) c.Next() } } - -func BuildCSP(nonce string, formActionExtra ...string) string { - return utils.BuildCSP(nonce, formActionExtra...) -} diff --git a/backend/internal/middleware/csp_middleware_test.go b/backend/internal/middleware/csp_middleware_test.go deleted file mode 100644 index 67f90464..00000000 --- a/backend/internal/middleware/csp_middleware_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package middleware - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBuildCSP(t *testing.T) { - t.Run("uses self form action by default", func(t *testing.T) { - csp := BuildCSP("test-nonce") - - assert.Contains(t, csp, "form-action 'self';") - assert.Contains(t, csp, "script-src 'self' 'nonce-test-nonce'") - }) - - t.Run("adds validated form action targets", func(t *testing.T) { - csp := BuildCSP("test-nonce", "https://example.com/callback") - - assert.Contains(t, csp, "form-action 'self' https://example.com/callback;") - assert.Equal(t, 1, strings.Count(csp, "form-action")) - }) -} diff --git a/backend/internal/oidc/authorization_handler.go b/backend/internal/oidc/authorization_handler.go index fa036c62..d8a41b27 100644 --- a/backend/internal/oidc/authorization_handler.go +++ b/backend/internal/oidc/authorization_handler.go @@ -99,9 +99,10 @@ func (h *authorizationHandler) authorize(c *gin.Context) { } response.AddParameter("iss", h.baseURL) - if ar.GetResponseMode() == fosite.ResponseModeFormPost && ar.GetRedirectURI() != nil { - c.Header("Content-Security-Policy", utils.BuildCSP(utils.GetCSPNonce(c), ar.GetRedirectURI().String())) - } + + // fosite renders an auto-submitting HTML page for response_mode=form_post, which needs a relaxed CSP + h.relaxCSPForFormPost(c, ar) + h.provider.WriteAuthorizeResponse(ctx, c.Writer, ar, response) } @@ -141,6 +142,8 @@ func (h *authorizationHandler) completeInteraction(c *gin.Context) { func (h *authorizationHandler) writeAuthorizeError(ctx context.Context, c *gin.Context, ar fosite.AuthorizeRequester, err error) { if ar.IsRedirectURIValid() { // Send the error to the client + // fosite delivers the error through response_mode=form_post as well, so it needs the same CSP relaxation as the success path + h.relaxCSPForFormPost(c, ar) h.provider.WriteAuthorizeError(ctx, c.Writer, ar, err) return } @@ -177,3 +180,11 @@ func authorizeRequestParams(requester fosite.AuthorizeRequester) map[string]stri return params } + +// relaxCSPForFormPost loosens the per-request Content-Security-Policy when the response is delivered via response_mode=form_post +func (h *authorizationHandler) relaxCSPForFormPost(c *gin.Context, ar fosite.AuthorizeRequester) { + if ar.GetResponseMode() != fosite.ResponseModeFormPost || ar.GetRedirectURI() == nil { + return + } + c.Header("Content-Security-Policy", utils.BuildFormPostCSP(utils.GetCSPNonce(c), ar.GetRedirectURI().String(), formPostScriptCSPHash)) +} diff --git a/backend/internal/oidc/form_post.go b/backend/internal/oidc/form_post.go new file mode 100644 index 00000000..72ca3068 --- /dev/null +++ b/backend/internal/oidc/form_post.go @@ -0,0 +1,40 @@ +package oidc + +import ( + "crypto/sha256" + "encoding/base64" + "html/template" +) + +// formPostAutoSubmitScript submits the response_mode=form_post page back to the client as soon as it loads +// Pocket ID's Content-Security-Policy forbids 'unsafe-inline' scripts and inline event handlers, which is exactly what fosite's default form_post template relies on (
), so that page silently never submits and strands the user on a blank page +// We instead deliver the auto-submit as a regular inline + +