Repository: google/go-safeweb (go)

Issue:
Extend the XSRF plugin to provide protection for Angular XHR
See: https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection for more details on how this can be acheived.

Pull request:
XSRF plugin for Angular
Fixes #194

Created a separate plugin that provides protection against XSRF in Angular. More details can be found here: https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection

Changed the directory tree to include the two XSRF plugins in the same directory with the functionality used by both placed in a separate file.

Patch:
diff --git a/safehttp/default_dispatcher.go b/safehttp/default_dispatcher.go
index 099f4c5..a2366b3 100644
--- a/safehttp/default_dispatcher.go
+++ b/safehttp/default_dispatcher.go
@@ -26,17 +26,11 @@ import (
 // DefaultDispatcher is responsible for writing safe responses.
 type DefaultDispatcher struct{}
 
-// ContentType returns the Content-Type of a response if it's deemed safe and
-// an error otherwise.
+// ContentType returns the Content-Type of a safe response if it's of a
+// safe-type and an error otherwise.
 func (DefaultDispatcher) ContentType(resp Response) (string, error) {
-	switch x := resp.(type) {
-	case safehtml.HTML:
-		return "text/html; charset=utf-8", nil
-	case TemplateResponse:
-		_, ok := (x.Template).(*template.Template)
-		if !ok {
-			return "", fmt.Errorf("%T is not a safe response type, a Content-Type cannot be provided", resp)
-		}
+	switch resp.(type) {
+	case safehtml.HTML, *template.Template:
 		return "text/html; charset=utf-8", nil
 	case JSONResponse:
 		return "application/json; charset=utf-8", nil
@@ -45,39 +39,49 @@ func (DefaultDispatcher) ContentType(resp Response) (string, error) {
 	}
 }
 
-// Write writes the response to the http.ResponseWriter if it's deemed safe. It
-// returns a non-nil error if the response is deemed unsafe or if the writing
-// operation fails.
-//
-// For JSONResponses, the underlying object is serialised and written if it's a
-// valid JSON.
-//
-// For TemplateResponses, the parsed template is applied to the provided data
-// object. If the funcMap is non-nil, its elements override the  existing names
-// to functions mappings in the template. An attempt to define a new name to
-// function mapping that is not already in the template will result  in a panic.
+// Write writes the response to the Response Writer if it's safe HTML. It
+// returns a non-nil error if the response is not safe HTML or if writing the
+// response fails.
 func (DefaultDispatcher) Write(rw http.ResponseWriter, resp Response) error {
-	switch x := resp.(type) {
-	case JSONResponse:
-		io.WriteString(rw, ")]}',\n") // Break parsing of JavaScript in order to prevent XSSI.
-		return json.NewEncoder(rw).Encode(x.Data)
-	case TemplateResponse:
-		t, ok := (x.Template).(*template.Template)
-		if !ok {
-			return fmt.Errorf("%T is not a safe template and it cannot be parsed and written", t)
-		}
-		if len(x.FuncMap) == 0 {
-			return t.Execute(rw, x.Data)
-		}
-		cloned, err := t.Clone()
-		if err != nil {
-			return err
-		}
-		return cloned.Funcs(x.FuncMap).Execute(rw, x.Data)
-	case safehtml.HTML:
-		_, err := io.WriteString(rw, x.String())
-		return err
-	default:
+	x, ok := resp.(safehtml.HTML)
+	if !ok {
 		return fmt.Errorf("%T is not a safe response type and it cannot be written", resp)
 	}
+	_, err := io.WriteString(rw, x.String())
+	return err
+
+}
+
+// WriteJSON serialises and writes a JSON to the underlying http.ResponseWriter.
+// If the provided response is not valid JSON or writing the response fails, the
+// method will return an error.
+func (DefaultDispatcher) WriteJSON(rw http.ResponseWriter, resp JSONResponse) error {
+	io.WriteString(rw, ")]}',\n") // Break parsing of JavaScript in order to prevent XSSI.
+	return json.NewEncoder(rw).Encode(resp.Data)
+}
+
+// ExecuteTemplate applies the parsed template to the provided data object, if
+// the template is a safe HTML template, writing the output to the  http.
+// ResponseWriter. If the funcMap is non-nil, its elements override the
+// existing names to functions mappings in the template. An attempt to define a
+// new name to function mapping that is not already in the template will result
+// in a panic.
+//
+// If an error occurs executing the template or writing its output,
+// execution stops, but partial results may already have been written to
+// the Response Writer.
+func (DefaultDispatcher) ExecuteTemplate(rw http.ResponseWriter, resp TemplateResponse) error {
+	t := *resp.Template
+	x, ok := t.(*template.Template)
+	if !ok {
+		return fmt.Errorf("%T is not a safe template and it cannot be parsed and written", resp.Template)
+	}
+	if len(resp.FuncMap) == 0 {
+		return x.Execute(rw, resp.Data)
+	}
+	cloned, err := x.Clone()
+	if err != nil {
+		return err
+	}
+	return cloned.Funcs(resp.FuncMap).Execute(rw, resp.Data)
 }
diff --git a/safehttp/plugins/csp/csp.go b/safehttp/plugins/csp/csp.go
index 122a01c..790532e 100644
--- a/safehttp/plugins/csp/csp.go
+++ b/safehttp/plugins/csp/csp.go
@@ -26,6 +26,7 @@ import (
 	"encoding/base64"
 	"errors"
 	"fmt"
+	"github.com/google/go-safeweb/safehttp/plugins/htmlinject"
 	"strings"
 
 	"github.com/google/go-safeweb/safehttp"
@@ -235,7 +236,7 @@ func (it Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingReq
 	// htmlinject
 	// TODO: What should happen if the CSPNonce is not present in the
 	// tr.FuncMap?
-	tmplResp.FuncMap["CSPNonce"] = func() string { return nonce }
+	tmplResp.FuncMap[htmlinject.CSPNoncesDefaultFuncName] = func() string { return nonce }
 	return safehttp.NotWritten()
 }
 
diff --git a/safehttp/plugins/xsrf/angular/doc.go b/safehttp/plugins/xsrf/angular/doc.go
new file mode 100644
index 0000000..cd2a2e5
--- /dev/null
+++ b/safehttp/plugins/xsrf/angular/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package xsrfangular provides a safehttp.Interceptor that ensures Cross-Site
+// Request Forgery protection for Angular applications by verifying the incoming
+// requests, rejecting those requests that are suspected to be part of an
+// attack.
+package xsrfangular
diff --git a/safehttp/plugins/xsrf/angular/xsrf.go b/safehttp/plugins/xsrf/angular/xsrf.go
new file mode 100644
index 0000000..29d6e64
--- /dev/null
+++ b/safehttp/plugins/xsrf/angular/xsrf.go
@@ -0,0 +1,120 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrfangular
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+	"time"
+)
+
+// Interceptor provides protection against Cross-Site Request Forgery attacks
+// for Angular's XHR requests.
+//
+// See https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection for more details.
+type Interceptor struct {
+	// TokenCookieName is the name of the session cookie that holds the XSRF token.
+	TokenCookieName string
+	// TokenHeaderName is the name of the HTTP header that holds the XSRF token.
+	TokenHeaderName string
+}
+
+var _ safehttp.Interceptor = &Interceptor{}
+
+// Default creates an Interceptor with TokenCookieName set to XSRF-TOKEN and
+// TokenHeaderName set to X-XSRF-TOKEN, their default values. However, in order
+// to prevent collisions when multiple applications share the same domain or
+// subdomain, each application should set a unique name for the cookie.
+//
+// See https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection for more details.
+func Default() *Interceptor {
+	return &Interceptor{
+		TokenCookieName: "XSRF-TOKEN",
+		TokenHeaderName: "X-XSRF-TOKEN",
+	}
+}
+
+// Before checks for the presence of a matching XSRF token, generated on the
+// first page access, in both a cookie and a header. Their names should be set
+// when the Interceptor is created.
+func (it *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
+	if xsrf.StatePreserving(r) {
+		return safehttp.NotWritten()
+	}
+
+	c, err := r.Cookie(it.TokenCookieName)
+	if err != nil || c.Value() == "" {
+		return w.WriteError(safehttp.StatusForbidden)
+	}
+
+	tok := r.Header.Get(it.TokenHeaderName)
+	if tok == "" || tok != c.Value() {
+		// JavaScript has access only to cookies from the domain it's running
+		// on. Hence, if the same token is found in both the cookie and the
+		// header, the request can be trusted.
+		return w.WriteError(safehttp.StatusUnauthorized)
+	}
+
+	return safehttp.NotWritten()
+}
+
+func (it *Interceptor) addTokenCookie(w *safehttp.ResponseWriter) error {
+	tok := make([]byte, 20)
+	if _, err := rand.Read(tok); err != nil {
+		return fmt.Errorf("crypto/rand.Read: %v", err)
+	}
+	c := safehttp.NewCookie(it.TokenCookieName, base64.StdEncoding.EncodeToString(tok))
+
+	c.SetSameSite(safehttp.SameSiteStrictMode)
+	c.SetPath("/")
+	day := 24 * time.Hour
+	c.SetMaxAge(int(day.Seconds()))
+	// Needed in order to make the cookie accessible by JavaScript
+	// running on the same domain.
+	c.DisableHTTPOnly()
+
+	return w.SetCookie(c)
+}
+
+// Commit generates a cryptographically secure random cookie on the first state
+// preserving request (GET, HEAD or OPTION) and sets it in the response. On
+// every subsequent request the cookie is expected alongside a header that
+// matches its value.
+func (it *Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) safehttp.Result {
+	if c, err := r.Cookie(it.TokenCookieName); err == nil && c.Value() != "" {
+		// The XSRF cookie is there so we don't need to do anything else.
+		return safehttp.NotWritten()
+	}
+
+	if !xsrf.StatePreserving(r) {
+		// This should never happen as, if this is a state-changing request and
+		// it lacks the cookie, it would've been already rejected by Before.
+		return w.WriteError(safehttp.StatusInternalServerError)
+	}
+
+	if err := it.addTokenCookie(w); err != nil {
+		// This is a server misconfiguration.
+		return w.WriteError(safehttp.StatusInternalServerError)
+	}
+	return safehttp.NotWritten()
+}
+
+// OnError is a no-op, required to satisfy the safehttp.Interceptor interface.
+func (it *Interceptor) OnError(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) safehttp.Result {
+	return safehttp.NotWritten()
+}
diff --git a/safehttp/plugins/xsrf/html/doc.go b/safehttp/plugins/xsrf/html/doc.go
new file mode 100644
index 0000000..52c5c15
--- /dev/null
+++ b/safehttp/plugins/xsrf/html/doc.go
@@ -0,0 +1,19 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package xsrfhtml provides a safehttp.Interceptor that ensures Cross-Site
+// Request Forgery by verifying the incoming requests for the presence of an
+// XSRF token, rejecting those requests that are suspected to be part of an
+// attack.
+package xsrfhtml
diff --git a/safehttp/plugins/xsrf/html/xsrf.go b/safehttp/plugins/xsrf/html/xsrf.go
new file mode 100644
index 0000000..1cf1af0
--- /dev/null
+++ b/safehttp/plugins/xsrf/html/xsrf.go
@@ -0,0 +1,161 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrfhtml
+
+import (
+	"context"
+	"crypto/rand"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"github.com/google/go-safeweb/safehttp/plugins/htmlinject"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+
+	"github.com/google/go-safeweb/safehttp"
+	"golang.org/x/net/xsrftoken"
+)
+
+const (
+	// TokenKey is the form key used when sending the token as part of POST
+	// request.
+	TokenKey    = "xsrf-token"
+	cookieIDKey = "xsrf-cookie"
+)
+
+// Interceptor implements XSRF protection.
+type Interceptor struct {
+	// SecretAppKey uniquely identifies each registered service and should have
+	// high entropy as it is used for generating the XSRF token.
+	SecretAppKey string
+}
+
+var _ safehttp.Interceptor = &Interceptor{}
+
+type tokenCtxKey struct{}
+
+// Token extracts the XSRF token from the incoming request. If it is not
+// present, it returns a non-nil error.
+func Token(r *safehttp.IncomingRequest) (string, error) {
+	tok := r.Context().Value(tokenCtxKey{})
+	if tok == nil {
+		return "", errors.New("xsrf token not found")
+	}
+	return tok.(string), nil
+}
+
+func addCookieID(w *safehttp.ResponseWriter) (*safehttp.Cookie, error) {
+	buf := make([]byte, 20)
+	if _, err := rand.Read(buf); err != nil {
+		return nil, fmt.Errorf("crypto/rand.Read: %v", err)
+	}
+
+	c := safehttp.NewCookie(cookieIDKey, base64.StdEncoding.EncodeToString(buf))
+	c.SetSameSite(safehttp.SameSiteStrictMode)
+	if err := w.SetCookie(c); err != nil {
+		return nil, err
+	}
+	return c, nil
+}
+
+// Before should be executed before directing the safehttp.IncomingRequest to
+// the handler to ensure it is not part of a Cross-Site Request
+// Forgery attack.
+//
+// On first user visit through a state preserving request (GET, HEAD or
+// OPTIONS), a nonce-based cookie will be set in the response as a way to
+// distinguish between users and prevent pre-login XSRF attacks. The cookie will
+// be used in the token generation and verification algorithm and is expected to
+// be present in all subsequent incoming requests.
+//
+// For every authorized request, the interceptor will also generate a
+// cryptographically-safe XSRF token using the appKey, the cookie and the path
+// visited. This can be later extracted using Token and should be injected as a
+// hidden input field in HTML forms.
+//
+// In case of state changing requests (all except GET, HEAD and OPTIONS), the
+// interceptor checks for the presence of the XSRF token in the request body
+// (expected to have been injected) and validates it.
+func (it *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
+	needsValidation := !xsrf.StatePreserving(r)
+	cookieID, err := r.Cookie(cookieIDKey)
+	if err != nil {
+		if needsValidation {
+			return w.WriteError(safehttp.StatusForbidden)
+		}
+		cookieID, err = addCookieID(w)
+		if err != nil {
+			// An error is returned when the plugin fails to Set the Set-Cookie
+			// header in the response writer as this is a server misconfiguration.
+			return w.WriteError(safehttp.StatusInternalServerError)
+		}
+	}
+
+	actionID := r.URL.Path()
+	if needsValidation {
+		f, err := r.PostForm()
+		if err != nil {
+			// We fallback to checking whether the form is multipart. Both types
+			// are valid in an incoming request as long as the XSRF token is
+			// present.
+			mf, err := r.MultipartForm(32 << 20)
+			if err != nil {
+				return w.WriteError(safehttp.StatusBadRequest)
+			}
+			f = &mf.Form
+		}
+
+		tok := f.String(TokenKey, "")
+		if f.Err() != nil || tok == "" {
+			return w.WriteError(safehttp.StatusUnauthorized)
+		}
+
+		if ok := xsrftoken.Valid(tok, it.SecretAppKey, cookieID.Value(), actionID); !ok {
+			return w.WriteError(safehttp.StatusForbidden)
+		}
+	}
+
+	tok := xsrftoken.Generate(it.SecretAppKey, cookieID.Value(), actionID)
+	r.SetContext(context.WithValue(r.Context(), tokenCtxKey{}, tok))
+	return safehttp.NotWritten()
+}
+
+// Commit adds the XSRF token corresponding to the safehttp.TemplateResponse
+// with key "XSRFToken". The token corresponds to the user information found in
+// the request.
+func (it *Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) safehttp.Result {
+	tmplResp, ok := resp.(safehttp.TemplateResponse)
+	if !ok {
+		return safehttp.NotWritten()
+	}
+
+	tok, err := Token(r)
+	if err != nil {
+		// The token should have been added in the Before stage and if that is
+		// not the case, a server misconfiguration occured.
+		return w.WriteError(safehttp.StatusInternalServerError)
+	}
+
+	// TODO(maramihali@): Change the key when function names are exported by
+	// htmlinject
+	// TODO: what should happen if the XSRFToken key is not present in the
+	// tr.FuncMap?
+	tmplResp.FuncMap[htmlinject.XSRFTokensDefaultFuncName] = func() string { return tok }
+	return safehttp.NotWritten()
+}
+
+// OnError is a no-op, required to satisfy the safehttp.Interceptor interface.
+func (it *Interceptor) OnError(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) safehttp.Result {
+	return safehttp.NotWritten()
+}
diff --git a/safehttp/plugins/xsrf/xsrf.go b/safehttp/plugins/xsrf/xsrf.go
index 82e7271..37b233d 100644
--- a/safehttp/plugins/xsrf/xsrf.go
+++ b/safehttp/plugins/xsrf/xsrf.go
@@ -12,27 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// Package xsrf provides a safehttp.Interceptor that ensures Cross-Site Request
-// Forgery protection by verifying the incoming requests, rejecting those
-// requests that are suspected to be part of an attack.
+// Package xsrf contains helper functions for the safehttp.Interceptor that
+// provide protection against Cross-Site Request Forgery attacks.
 package xsrf
 
 import (
-	"context"
-	"crypto/rand"
-	"encoding/base64"
-	"errors"
-	"fmt"
-
 	"github.com/google/go-safeweb/safehttp"
-	"golang.org/x/net/xsrftoken"
-)
-
-const (
-	// TokenKey is the form key used when sending the token as part of POST
-	// request.
-	TokenKey    = "xsrf-token"
-	cookieIDKey = "xsrf-cookie"
 )
 
 var statePreservingMethods = map[string]bool{
@@ -41,128 +26,7 @@ var statePreservingMethods = map[string]bool{
 	safehttp.MethodOptions: true,
 }
 
-// Interceptor implements XSRF protection.
-type Interceptor struct {
-	// SecretAppKey uniquely identifies each registered service and should have
-	// high entropy as it is used for generating the XSRF token.
-	SecretAppKey string
-}
-
-var _ safehttp.Interceptor = &Interceptor{}
-
-type tokenCtxKey struct{}
-
-// Token extracts the XSRF token from the incoming request. If it is not
-// present, it returns a non-nil error.
-func Token(r *safehttp.IncomingRequest) (string, error) {
-	tok := r.Context().Value(tokenCtxKey{})
-	if tok == nil {
-		return "", errors.New("xsrf token not found")
-	}
-	return tok.(string), nil
-}
-
-func addCookieID(w *safehttp.ResponseWriter) (*safehttp.Cookie, error) {
-	buf := make([]byte, 20)
-	if _, err := rand.Read(buf); err != nil {
-		return nil, fmt.Errorf("crypto/rand.Read: %v", err)
-	}
-
-	c := safehttp.NewCookie(cookieIDKey, base64.StdEncoding.EncodeToString(buf))
-	c.SetSameSite(safehttp.SameSiteStrictMode)
-	if err := w.SetCookie(c); err != nil {
-		return nil, err
-	}
-	return c, nil
-}
-
-// Before should be executed before directing the safehttp.IncomingRequest to
-// the handler to ensure it is not part of a Cross-Site Request
-// Forgery attack.
-//
-// On first user visit through a state preserving request (GET, HEAD or
-// OPTIONS), a nonce-based cookie will be set in the response as a way to
-// distinguish between users and prevent pre-login XSRF attacks. The cookie will
-// be used in the token generation and verification algorithm and is expected to
-// be present in all subsequent incoming requests.
-//
-// For every authorized request, the interceptor will also generate a
-// cryptographically-safe XSRF token using the appKey, the cookie and the path
-// visited. This can be later extracted using Token and should be injected as a
-// hidden input field in HTML forms.
-//
-// In case of state changing requests (all except GET, HEAD and OPTIONS), the
-// interceptor checks for the presence of the XSRF token in the request body
-// (expected to have been injected) and validates it.
-func (it *Interceptor) Before(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, _ safehttp.InterceptorConfig) safehttp.Result {
-	needsValidation := !statePreservingMethods[r.Method()]
-	cookieID, err := r.Cookie(cookieIDKey)
-	if err != nil {
-		if needsValidation {
-			return w.WriteError(safehttp.StatusForbidden)
-		}
-		cookieID, err = addCookieID(w)
-		if err != nil {
-			// An error is returned when the plugin fails to Set the Set-Cookie
-			// header in the response writer as this is a server misconfiguration.
-			return w.WriteError(safehttp.StatusInternalServerError)
-		}
-	}
-
-	actionID := r.URL.Path()
-	if needsValidation {
-		f, err := r.PostForm()
-		if err != nil {
-			// We fallback to checking whether the form is multipart. Both types
-			// are valid in an incoming request as long as the XSRF token is
-			// present.
-			mf, err := r.MultipartForm(32 << 20)
-			if err != nil {
-				return w.WriteError(safehttp.StatusBadRequest)
-			}
-			f = &mf.Form
-		}
-
-		tok := f.String(TokenKey, "")
-		if f.Err() != nil || tok == "" {
-			return w.WriteError(safehttp.StatusUnauthorized)
-		}
-
-		if ok := xsrftoken.Valid(tok, it.SecretAppKey, cookieID.Value(), actionID); !ok {
-			return w.WriteError(safehttp.StatusForbidden)
-		}
-	}
-
-	tok := xsrftoken.Generate(it.SecretAppKey, cookieID.Value(), actionID)
-	r.SetContext(context.WithValue(r.Context(), tokenCtxKey{}, tok))
-	return safehttp.NotWritten()
-}
-
-// Commit adds the XSRF token corresponding to the safehttp.TemplateResponse
-// with key "XSRFToken". The token corresponds to the user information found in
-// the request.
-func (it *Interceptor) Commit(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) safehttp.Result {
-	tmplResp, ok := resp.(safehttp.TemplateResponse)
-	if !ok {
-		return safehttp.NotWritten()
-	}
-
-	tok, err := Token(r)
-	if err != nil {
-		// The token should have been added in the Before stage and if that is
-		// not the case, a server misconfiguration occured.
-		return w.WriteError(safehttp.StatusInternalServerError)
-	}
-
-	// TODO(maramihali@): Change the key when function names are exported by
-	// htmlinject
-	// TODO: what should happen if the XSRFToken key is not present in the
-	// tr.FuncMap?
-	tmplResp.FuncMap["XSRFToken"] = func() string { return tok }
-	return safehttp.NotWritten()
-}
-
-// OnError is a no-op, required to satisfy the safehttp.Interceptor interface.
-func (it *Interceptor) OnError(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest, resp safehttp.Response, _ safehttp.InterceptorConfig) safehttp.Result {
-	return safehttp.NotWritten()
+// StatePreserving checks if the provided request is state preserving.
+func StatePreserving(r *safehttp.IncomingRequest) bool {
+	return statePreservingMethods[r.Method()]
 }
diff --git a/safehttp/response.go b/safehttp/response.go
index 2c7719b..7ebb2d2 100644
--- a/safehttp/response.go
+++ b/safehttp/response.go
@@ -29,13 +29,6 @@ type JSONResponse struct {
 	Data interface{}
 }
 
-// WriteJSON creates a JSONResponse from the data object and calls the Write
-// function of the ResponseWriter, passing the response. The data object should
-// be valid JSON, otherwise an error will occur.
-func WriteJSON(w *ResponseWriter, data interface{}) Result {
-	return w.Write(JSONResponse{data})
-}
-
 // Template implements a template.
 type Template interface {
 	// Execute applies data to the template and then writes the result to
@@ -50,25 +43,11 @@ type Template interface {
 // TemplateResponse bundles a Template with its data and names to function
 // mappings to be passed together to the commit phase.
 type TemplateResponse struct {
-	Template Template
-	Data     interface{}
+	Template *Template
+	Data     *interface{}
 	FuncMap  map[string]interface{}
 }
 
-// ExecuteTemplate creates a TemplateResponse from the provided Template and its
-// data and calls the Write function of the ResponseWriter, passing the
-// response.
-func ExecuteTemplate(w *ResponseWriter, t Template, data interface{}) Result {
-	return w.Write(TemplateResponse{t, data, nil})
-}
-
-// ExecuteTemplateWithFuncs creates a TemplateResponse from the provided
-// Template, its data and the name to function mappings and calls the Write
-// function of the ResponseWriter, passing the response.
-func ExecuteTemplateWithFuncs(w *ResponseWriter, t Template, data interface{}, fm map[string]interface{}) Result {
-	return w.Write(TemplateResponse{t, data, fm})
-}
-
 // NoContentResponse is sent to the commit phase when it's initiated from
 // ResponseWriter.NoContent.
 type NoContentResponse struct{}
diff --git a/safehttp/response_writer.go b/safehttp/response_writer.go
index 6ac4a62..73324ce 100644
--- a/safehttp/response_writer.go
+++ b/safehttp/response_writer.go
@@ -66,10 +66,12 @@ func NotWritten() Result {
 	return Result{}
 }
 
-// Write dispatches the response to the Dispatcher. This will be written to the
-// underlying http.ResponseWriter if the Dispatcher decides it's safe to do so.
+// Write dispatches the response to the Dispatcher, setting the Content-Type and
+// response status if the provided response is a safe response. The
+// Dispatcher will then write the response to the underlying Response Writer.
 //
-// TODO: replace panics with proper error handling when writing the response fails.
+// TODO: replace panics with proper error handling when getting the response
+// Content-Type or writing the response fails.
 func (w *ResponseWriter) Write(resp Response) Result {
 	if w.written {
 		panic("ResponseWriter was already written to")
@@ -78,17 +80,71 @@ func (w *ResponseWriter) Write(resp Response) Result {
 	if w.written {
 		return Result{}
 	}
-
 	w.markWritten()
+	ct, err := w.d.ContentType(resp)
+	if err != nil {
+		panic(err)
+	}
+	w.rw.Header().Set("Content-Type", ct)
+	w.rw.WriteHeader(int(StatusOK))
+	if err := w.d.Write(w.rw, resp); err != nil {
+		panic(err)
+	}
+	return Result{}
+}
 
+// WriteJSON encapsulates data into a JSON response and dispatches it to the
+// Dispatcher, to be serialised and written to the ResponseWriter. It will also
+// set the Content-Type and response status will be set.
+//
+// TODO: replace panics with proper error handling when getting the response
+// Content-Type or writing the response fails.
+func (w *ResponseWriter) WriteJSON(data interface{}) Result {
+	if w.written {
+		panic("ResponseWriter was already written to")
+	}
+	w.handler.commitPhase(w, JSONResponse{Data: data})
+	if w.written {
+		return Result{}
+	}
+	w.markWritten()
+	resp := JSONResponse{Data: data}
 	ct, err := w.d.ContentType(resp)
 	if err != nil {
 		panic(err)
 	}
 	w.rw.Header().Set("Content-Type", ct)
 	w.rw.WriteHeader(int(StatusOK))
+	if err := w.d.WriteJSON(w.rw, resp); err != nil {
+		panic(err)
+	}
+	return Result{}
+}
 
-	if err := w.d.Write(w.rw, resp); err != nil {
+// WriteTemplate dispatches a parsed template and a data object to the
+// Dispatcher to be executed and written to the underlying Response Writer, in
+// case the template is a safe HTML template. If it is a safe HTML Template, the
+// Content-Type  and response status will also be set.
+//
+// TODO: replace panics with proper error handling when getting the response
+// Content-Type or writing the response fails.
+func (w *ResponseWriter) WriteTemplate(t Template, data interface{}) Result {
+	if w.written {
+		panic("ResponseWriter was already written to")
+	}
+	resp := TemplateResponse{Template: &t, Data: &data, FuncMap: map[string]interface{}{}}
+	w.handler.commitPhase(w, resp)
+	if w.written {
+		return Result{}
+	}
+	w.markWritten()
+	ct, err := w.d.ContentType(t)
+	if err != nil {
+		panic(err)
+	}
+	w.rw.Header().Set("Content-Type", ct)
+	w.rw.WriteHeader(int(StatusOK))
+	if err := w.d.ExecuteTemplate(w.rw, resp); err != nil {
 		panic(err)
 	}
 	return Result{}
@@ -163,21 +219,36 @@ func (w *ResponseWriter) SetCookie(c *Cookie) error {
 // The implementation of a custom Dispatcher should be thoroughly reviewed by
 // the security team to avoid introducing vulnerabilities.
 type Dispatcher interface {
+	// Write writes a Response to the http.ResponseWriter.
+	//
+	// It should return an error if the writing operation fails or if the
+	// provided Response should not be written to the http.ResponseWriter
+	Write(rw http.ResponseWriter, resp Response) error
+
+	// WriteJSON serialises and writes a JSON object to the http.ResponseWriter.
+	//
+	// This should return an error if the provided response is not a safe
+	// JSONResponse  or if writing the object to the http.ResponseWriter fails.
+	WriteJSON(rw http.ResponseWriter, resp JSONResponse) error
+
+	// ExecuteTemplate applies the response parsed template to its data object
+	// and writes the output to the http.ResponseWriter. If the response funcMap
+	// is non-nil, the Dispatcher will try to update the names to functions
+	// mappings of the template.
+	//
+	// This should return an error if the template contained by the
+	// TemplateResponse is not a safe template or if an error occurs executing
+	// or writing the template.
+	ExecuteTemplate(rw http.ResponseWriter, resp TemplateResponse) error
+
 	// Content-Type returns the Content-Type of the provided response if it is
-	// of a safe type, supported by the Dispatcher, and should return an error
+	// of a type supported by the Dispatcher and should return an error
 	// otherwise.
 	//
 	// Sending a response to the http.ResponseWriter without properly setting
 	// CT is error-prone and could introduce vulnerabilities. Therefore, this
-	// method should be used to set the Content-Type header before calling
-	// Dispatcher.Write. Writing should not proceed if ContentType returns an
-	// error.
+	// method should be used to set the Content-Type header before calling any
+	// of the writing methods in the Dispatcher. Writing should not proceed
+	// if ContentType returns an error.
 	ContentType(resp Response) (string, error)
-
-	// Write writes a Response to the underlying http.ResponseWriter.
-	//
-	// It should return an error if the writing operation fails or if the
-	// provided Response should not be written to the http.ResponseWriter
-	// because it's unsafe.
-	Write(rw http.ResponseWriter, resp Response) error
 }


Test patch:
diff --git a/safehttp/default_dispatcher_test.go b/safehttp/default_dispatcher_test.go
index ed4dda5..ed58b41 100644
--- a/safehttp/default_dispatcher_test.go
+++ b/safehttp/default_dispatcher_test.go
@@ -51,7 +51,11 @@ func TestDefaultDispatcherValidResponse(t *testing.T) {
 						Parse("<h1>{{ . }}</h1>")))
 				var data interface{}
 				data = "This is an actual heading, though."
-				return d.Write(w, safehttp.TemplateResponse{t, data, nil})
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+				}
+				return d.ExecuteTemplate(w, resp)
 			},
 			wantBody: "<h1>This is an actual heading, though.</h1>",
 		},
@@ -69,7 +73,13 @@ func TestDefaultDispatcherValidResponse(t *testing.T) {
 				fm := map[string]interface{}{
 					"Token": func() string { return "Token-secret" },
 				}
-				return d.Write(w, safehttp.TemplateResponse{t, data, fm})
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+					FuncMap:  fm,
+				}
+
+				return d.ExecuteTemplate(w, resp)
 			},
 			wantBody: `<form><input type="hidden" name="token" value="Token-secret">Content</form>`,
 		},
@@ -87,7 +97,13 @@ func TestDefaultDispatcherValidResponse(t *testing.T) {
 				fm := map[string]interface{}{
 					"Nonce": func() string { return "Nonce-secret" },
 				}
-				return d.Write(w, safehttp.TemplateResponse{t, data, fm})
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+					FuncMap:  fm,
+				}
+
+				return d.ExecuteTemplate(w, resp)
 			},
 			wantBody: `<script nonce="Nonce-secret" type="application/javascript">alert("script")</script><h1>Content</h1>`,
 		},
@@ -98,7 +114,7 @@ func TestDefaultDispatcherValidResponse(t *testing.T) {
 				data := struct {
 					Field string `json:"field"`
 				}{Field: "myField"}
-				return d.Write(w, safehttp.JSONResponse{data})
+				return d.WriteJSON(w, safehttp.JSONResponse{Data: data})
 			},
 			wantBody: ")]}',\n{\"field\":\"myField\"}\n",
 		},
@@ -144,7 +160,11 @@ func TestDefaultDispatcherInvalidResponse(t *testing.T) {
 						Parse("<h1>{{ . }}</h1>")))
 				var data interface{}
 				data = "This is an actual heading, though."
-				return d.Write(w, safehttp.TemplateResponse{t, data, nil})
+				resp := safehttp.TemplateResponse{
+					Template: &t,
+					Data:     &data,
+				}
+				return d.ExecuteTemplate(w, resp)
 			},
 			want: "",
 		},
@@ -152,7 +172,7 @@ func TestDefaultDispatcherInvalidResponse(t *testing.T) {
 			name: "Invalid JSON Response",
 			write: func(w http.ResponseWriter) error {
 				d := &safehttp.DefaultDispatcher{}
-				return d.Write(w, safehttp.JSONResponse{math.Inf(1)})
+				return d.WriteJSON(w, safehttp.JSONResponse{Data: math.Inf(1)})
 			},
 			want: ")]}',\n",
 		},
diff --git a/safehttp/plugins/xsrf/angular/xsrf_test.go b/safehttp/plugins/xsrf/angular/xsrf_test.go
new file mode 100644
index 0000000..efc7733
--- /dev/null
+++ b/safehttp/plugins/xsrf/angular/xsrf_test.go
@@ -0,0 +1,184 @@
+// Copyright 2020 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package xsrfangular
+
+import (
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-safeweb/safehttp"
+	"github.com/google/go-safeweb/safehttp/safehttptest"
+	"strings"
+	"testing"
+)
+
+const (
+	cookieName = "XSRF-TOKEN"
+	headerName = "X-XSRF-TOKEN"
+)
+
+func TestAddCookie(t *testing.T) {
+	tests := []struct {
+		name, cookie string
+		it           *Interceptor
+	}{
+		{
+			name:   "Default interceptor",
+			it:     Default(),
+			cookie: cookieName,
+		},
+		{
+			name: "Custom interceptor",
+			it: &Interceptor{
+				TokenCookieName: "FOO-TOKEN",
+				TokenHeaderName: "X-FOO-TOKEN",
+			},
+			cookie: "FOO-TOKEN",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+			rec := safehttptest.NewResponseRecorder()
+			test.it.Commit(rec.ResponseWriter, req, nil, nil)
+
+			if got, want := rec.Status(), safehttp.StatusOK; got != want {
+				t.Errorf("rec.Status(): got %v, want %v", got, want)
+			}
+
+			wantCookie := "Path=/; Max-Age=86400; Secure; SameSite=Strict"
+			got := map[string][]string(rec.Header())["Set-Cookie"][0]
+			if !strings.Contains(got, test.cookie) {
+				t.Errorf("Set-Cookie header: %s not present", test.cookie)
+			}
+			if !strings.Contains(got, wantCookie) {
+				t.Errorf("Set-Cookie header: got %q, want defaults %q", got, wantCookie)
+			}
+
+			if got, want := rec.Body(), ""; got != want {
+				t.Errorf("rec.Body(): got %q want %q", got, want)
+			}
+
+		})
+	}
+}
+
+func TestAddCookieFail(t *testing.T) {
+	req := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+	rec := safehttptest.NewResponseRecorder()
+	it := &Interceptor{}
+	it.Commit(rec.ResponseWriter, req, nil, nil)
+
+	if wantStatus := safehttp.StatusInternalServerError; rec.Status() != wantStatus {
+		t.Errorf("rec.Status(): got %v want %v", rec.Status(), wantStatus)
+	}
+
+	wantHeaders := map[string][]string{
+		"Content-Type":           {"text/plain; charset=utf-8"},
+		"X-Content-Type-Options": {"nosniff"},
+	}
+	if diff := cmp.Diff(wantHeaders, map[string][]string(rec.Header())); diff != "" {
+		t.Errorf("rec.Header(): mismatch (-want +got):\n%s", diff)
+	}
+
+	if wantBody, gotBody := "Internal Server Error\n", rec.Body(); wantBody != gotBody {
+		t.Errorf("response body: got %v, want %v", gotBody, wantBody)
+	}
+}
+
+func TestPostProtection(t *testing.T) {
+	tests := []struct {
+		name       string
+		req        *safehttp.IncomingRequest
+		wantStatus safehttp.StatusCode
+		wantHeader map[string][]string
+		wantBody   string
+	}{
+		{
+			name: "Same cookie and header",
+			req: func() *safehttp.IncomingRequest {
+				req := safehttptest.NewRequest(safehttp.MethodPost, "/", nil)
+				req.Header.Set("Cookie", cookieName+"="+"1234")
+				req.Header.Set(headerName, "1234")
+				return req
+			}(),
+			wantStatus: safehttp.StatusOK,
+			wantHeader: map[string][]string{},
+			wantBody:   "",
+		},
+		{
+			name: "Different cookie and header",
+			req: func() *safehttp.IncomingRequest {
+				req := safehttptest.NewRequest(safehttp.MethodPost, "/", nil)
+				req.Header.Set("Cookie", cookieName+"="+"5768")
+				req.Header.Set(headerName, "1234")
+				return req
+			}(),
+			wantStatus: safehttp.StatusUnauthorized,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Unauthorized\n",
+		},
+		{
+			name: "Missing header",
+			req: func() *safehttp.IncomingRequest {
+				req := safehttptest.NewRequest(safehttp.MethodPost, "/", nil)
+				req.Header.Set("Cookie", cookieName+"="+"1234")
+				return req
+			}(),
+			wantStatus: safehttp.StatusUnauthorized,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Unauthorized\n",
+		},
+		{
+			name: "Missing cookie",
+			req: func() *safehttp.IncomingRequest {
+				req := safehttptest.NewRequest(safehttp.MethodPost, "/", nil)
+				req.Header.Set(headerName, "1234")
+				return req
+			}(),
+			wantStatus: safehttp.StatusForbidden,
+			wantHeader: map[string][]string{
+				"Content-Type":           {"text/plain; charset=utf-8"},
+				"X-Content-Type-Options": {"nosniff"},
+			},
+			wantBody: "Forbidden\n",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			rec := safehttptest.NewResponseRecorder()
+			i := Default()
+			i.Before(rec.ResponseWriter, test.req, nil)
+
+			if got := rec.Status(); got != test.wantStatus {
+				t.Errorf("rec.Status(): got %v, want %v", got, test.wantStatus)
+			}
+
+			if diff := cmp.Diff(test.wantHeader, map[string][]string(rec.Header())); diff != "" {
+				t.Errorf("rec.Header() mismatch (-want +got):\n%s", diff)
+			}
+
+			if got := rec.Body(); got != test.wantBody {
+				t.Errorf("rec.Body(): got %q want %q", got, test.wantBody)
+			}
+		})
+	}
+}
diff --git a/safehttp/plugins/xsrf/xsrf_test.go b/safehttp/plugins/xsrf/html/xsrf_test.go
similarity index 99%
rename from safehttp/plugins/xsrf/xsrf_test.go
rename to safehttp/plugins/xsrf/html/xsrf_test.go
index c6d1d1c..dd14a5e 100644
--- a/safehttp/plugins/xsrf/xsrf_test.go
+++ b/safehttp/plugins/xsrf/html/xsrf_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package xsrf
+package xsrfhtml
 
 import (
 	"context"
diff --git a/safehttp/response_writer_test.go b/safehttp/response_writer_test.go
index cf13eae..bf8d159 100644
--- a/safehttp/response_writer_test.go
+++ b/safehttp/response_writer_test.go
@@ -15,7 +15,6 @@
 package safehttp_test
 
 import (
-	"math"
 	"strings"
 	"testing"
 	"text/template"
@@ -68,69 +67,52 @@ func TestResponseWriterWriteTwicePanic(t *testing.T) {
 			},
 		},
 		{
-			name: "Call NoContent twice",
+			name: "Call WriteJSON twice",
 			write: func(w *safehttp.ResponseWriter) {
-				w.NoContent()
-				w.NoContent()
+				obj := struct{ Field string }{Field: "myField"}
+				w.WriteJSON(obj)
+				w.WriteJSON(obj)
 			},
 		},
 		{
-			name: "Call WriteError twice",
+			name: "Call Write then WriteJSON",
 			write: func(w *safehttp.ResponseWriter) {
-				w.WriteError(safehttp.StatusInternalServerError)
-				w.WriteError(safehttp.StatusInternalServerError)
+				obj := struct{ Field string }{Field: "myField"}
+				w.Write(obj)
+				w.WriteJSON(obj)
 			},
 		},
 		{
-			name: "Call Redirect twice",
+			name: "Call WriteTemplate twice",
 			write: func(w *safehttp.ResponseWriter) {
-				ir := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
-				w.Redirect(ir, "/asdf", safehttp.StatusMovedPermanently)
-				w.Redirect(ir, "/asdf", safehttp.StatusMovedPermanently)
+				w.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+				w.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
 			},
 		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			w := safehttp.NewResponseWriter(safehttp.DefaultDispatcher{}, safehttptest.NewTestResponseWriter(&strings.Builder{}), nil)
-			defer func() {
-				if r := recover(); r != nil {
-					return
-				}
-				t.Errorf("tt.write(w) expected panic")
-			}()
-			tt.write(w)
-		})
-	}
-}
-
-func TestResponseWriterUnsafeResponse(t *testing.T) {
-	tests := []struct {
-		name  string
-		write func(w *safehttp.ResponseWriter)
-	}{
 		{
-			name: "Unsafe HTML Response",
+			name: "Call NoContent twice",
 			write: func(w *safehttp.ResponseWriter) {
-				w.Write("<h1>Hello World!</h1>")
+				w.NoContent()
+				w.NoContent()
 			},
 		},
 		{
-			name: "Invalid JSON Response",
+			name: "Call WriteError twice",
 			write: func(w *safehttp.ResponseWriter) {
-				safehttp.WriteJSON(w, math.Inf(1))
+				w.WriteError(safehttp.StatusInternalServerError)
+				w.WriteError(safehttp.StatusInternalServerError)
 			},
 		},
 		{
-			name: "Unsafe Template Response",
+			name: "Call Redirect twice",
 			write: func(w *safehttp.ResponseWriter) {
-				safehttp.ExecuteTemplate(w, template.
-					Must(template.New("name").
-						Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+				ir := safehttptest.NewRequest(safehttp.MethodGet, "/", nil)
+				w.Redirect(ir, "/asdf", safehttp.StatusMovedPermanently)
+				w.Redirect(ir, "/asdf", safehttp.StatusMovedPermanently)
 			},
 		},
 	}
+
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			w := safehttp.NewResponseWriter(safehttp.DefaultDispatcher{}, safehttptest.NewTestResponseWriter(&strings.Builder{}), nil)
diff --git a/tests/integration/csp/csp_test.go b/tests/integration/csp/csp_test.go
index fbe409a..827b0f8 100644
--- a/tests/integration/csp/csp_test.go
+++ b/tests/integration/csp/csp_test.go
@@ -44,7 +44,7 @@ func TestServeMuxInstallCSP(t *testing.T) {
 		nonce, err = csp.Nonce(r.Context())
 		t := template.Must(template.New("name").Funcs(fns).Parse(`<script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script><h1>{{.}}</h1>`))
 
-		return safehttp.ExecuteTemplateWithFuncs(w, t, "Content", fns)
+		return w.WriteTemplate(t, "Content")
 	})
 	mb.Handle("/bar", safehttp.MethodGet, handler)
 
diff --git a/tests/integration/mux/mux_test.go b/tests/integration/mux/mux_test.go
index 13eed03..f4c874b 100644
--- a/tests/integration/mux/mux_test.go
+++ b/tests/integration/mux/mux_test.go
@@ -48,7 +48,7 @@ func TestMuxDefaultDispatcher(t *testing.T) {
 		{
 			name: "Safe HTML Template Response",
 			handler: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
-				return safehttp.ExecuteTemplate(w, safetemplate.
+				return w.WriteTemplate(safetemplate.
 					Must(safetemplate.New("name").
 						Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
 			}),
@@ -63,7 +63,7 @@ func TestMuxDefaultDispatcher(t *testing.T) {
 				data := struct {
 					Field string `json:"field"`
 				}{Field: "myField"}
-				return safehttp.WriteJSON(w, data)
+				return w.WriteJSON(data)
 			}),
 			wantHeaders: map[string][]string{
 				"Content-Type": {"application/json; charset=utf-8"},
@@ -110,7 +110,7 @@ func TestMuxDefaultDispatcherUnsafeResponses(t *testing.T) {
 		{
 			name: "Unsafe Template Response",
 			handler: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
-				return safehttp.ExecuteTemplate(w, template.
+				return w.WriteTemplate(template.
 					Must(template.New("name").
 						Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
 			}),
@@ -118,7 +118,7 @@ func TestMuxDefaultDispatcherUnsafeResponses(t *testing.T) {
 		{
 			name: "Invalid JSON Response",
 			handler: safehttp.HandlerFunc(func(w *safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
-				return safehttp.WriteJSON(w, math.Inf(1))
+				return w.WriteJSON(math.Inf(1))
 			}),
 		},
 	}
diff --git a/tests/integration/safehtml/safehtml_test.go b/tests/integration/safehtml/safehtml_test.go
index 1a0f79b..a8303a3 100644
--- a/tests/integration/safehtml/safehtml_test.go
+++ b/tests/integration/safehtml/safehtml_test.go
@@ -49,7 +49,7 @@ func TestHandleRequestWrite(t *testing.T) {
 func TestHandleRequestWriteTemplate(t *testing.T) {
 	mb := &safehttp.ServeMuxConfig{}
 	mb.Handle("/", safehttp.MethodGet, safehttp.HandlerFunc(func(rw *safehttp.ResponseWriter, ir *safehttp.IncomingRequest) safehttp.Result {
-		return safehttp.ExecuteTemplate(rw, template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
+		return rw.WriteTemplate(template.Must(template.New("name").Parse("<h1>{{ . }}</h1>")), "This is an actual heading, though.")
 	}))
 
 	req := httptest.NewRequest(safehttp.MethodGet, "http://foo.com/", nil)
diff --git a/tests/integration/xsrf/xsrf_test.go b/tests/integration/xsrf/xsrf_test.go
index 5c60f27..2f8e79e 100644
--- a/tests/integration/xsrf/xsrf_test.go
+++ b/tests/integration/xsrf/xsrf_test.go
@@ -20,14 +20,14 @@ import (
 	"testing"
 
 	"github.com/google/go-safeweb/safehttp"
-	"github.com/google/go-safeweb/safehttp/plugins/xsrf"
+	"github.com/google/go-safeweb/safehttp/plugins/xsrf/html"
 	"github.com/google/go-safeweb/safehttp/safehttptest"
 	"github.com/google/safehtml/template"
 )
 
 func TestServeMuxInstallXSRF(t *testing.T) {
 	mb := &safehttp.ServeMuxConfig{}
-	it := xsrf.Interceptor{SecretAppKey: "testSecretAppKey"}
+	it := xsrfhtml.Interceptor{SecretAppKey: "testSecretAppKey"}
 	mb.Intercept(&it)
 
 	var token string
@@ -40,10 +40,10 @@ func TestServeMuxInstallXSRF(t *testing.T) {
 		// handler can retrieve the XSRF token (e.g. for when the XSRF plugin is
 		// installed, but auto-injection isn't yet supported and has to be done
 		// manually by the handler).
-		token, err = xsrf.Token(r)
+		token, err = xsrfhtml.Token(r)
 		t := template.Must(template.New("name").Funcs(fns).Parse(`<form><input type="hidden" name="token" value="{{XSRFToken}}">{{.}}</form>`))
 
-		return safehttp.ExecuteTemplateWithFuncs(w, t, "Content", fns)
+		return w.WriteTemplate(t, "Content")
 	})
 	mb.Handle("/bar", safehttp.MethodGet, handler)
 
@@ -55,7 +55,7 @@ func TestServeMuxInstallXSRF(t *testing.T) {
 	mb.Mux().ServeHTTP(rr, req)
 
 	if err != nil {
-		t.Fatalf("xsrf.Token: got error %v", err)
+		t.Fatalf("xsrfhtml.Token: got error %v", err)
 	}
 
 	if token == "" {