Skip to content

Commit

Permalink
issues/158/examples for working api with javascript frontend (#162)
Browse files Browse the repository at this point in the history
Fixes #158, which is essentially that
1. none of the examples in the README for working with a JavaScript
frontend will work without proper CORS config on the backend
2. there is no example at all for using the HTTP header instead of
getting the CSRF token from the hidden form field

**Summary of Changes**

I have merged/copied over these simplified examples from my own
repository of working examples.

I was not sure how the maintainers may want to reference these examples
in the main README. Copying them over to the README verbatim would be
putting a lot of code into the README, but without changing the current
README, the content there differs significantly from the examples.

---------

Co-authored-by: Corey Daley <cdaley@redhat.com>
  • Loading branch information
francoposa and coreydaley authored Aug 17, 2023
1 parent 73c96a5 commit c1f4eb3
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 0 deletions.
11 changes: 11 additions & 0 deletions examples/api-backends/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# API Backends

Examples in this directory are intended to provide basic working backend CSRF-protected APIs,
compatible with the JavaScript frontend examples available in the
[`examples/javascript-frontends`](../javascript-frontends).

In addition to CSRF protection, these backends provide the CORS configuration required for
communicating the CSRF cookies and headers with JavaScript client code running in the browser.

See [`examples/javascript-frontends`](../javascript-frontends/README.md) for details on CORS and
CSRF configuration compatibility requirements.
17 changes: 17 additions & 0 deletions examples/api-backends/gorilla-mux/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// +build ignore

module github.com/gorilla-mux/examples/api-backends/gorilla-mux

go 1.20

require (
github.com/gorilla/csrf v1.7.1
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
)

require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
)
13 changes: 13 additions & 0 deletions examples/api-backends/gorilla-mux/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
66 changes: 66 additions & 0 deletions examples/api-backends/gorilla-mux/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// +build ignore

package main

import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"

"github.com/gorilla/csrf"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)

func main() {
router := mux.NewRouter()

loggingMiddleware := func(h http.Handler) http.Handler {
return handlers.LoggingHandler(os.Stdout, h)
}
router.Use(loggingMiddleware)

CSRFMiddleware := csrf.Protect(
[]byte("place-your-32-byte-long-key-here"),
csrf.Secure(false), // false in development only!
csrf.RequestHeader("X-CSRF-Token"), // Must be in CORS Allowed and Exposed Headers
)

APIRouter := router.PathPrefix("/api").Subrouter()
APIRouter.Use(CSRFMiddleware)
APIRouter.HandleFunc("", Get).Methods(http.MethodGet)
APIRouter.HandleFunc("", Post).Methods(http.MethodPost)

CORSMiddleware := handlers.CORS(
handlers.AllowCredentials(),
handlers.AllowedOriginValidator(
func(origin string) bool {
return strings.HasPrefix(origin, "http://localhost")
},
),
handlers.AllowedHeaders([]string{"X-CSRF-Token"}),
handlers.ExposedHeaders([]string{"X-CSRF-Token"}),
)

server := &http.Server{
Handler: CORSMiddleware(router),
Addr: "localhost:8080",
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
}

fmt.Println("starting http server on localhost:8080")
log.Panic(server.ListenAndServe())
}

func Get(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-CSRF-Token", csrf.Token(r))
w.WriteHeader(http.StatusOK)
}

func Post(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
19 changes: 19 additions & 0 deletions examples/javascript-frontends/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# JavaScript Frontends

Examples in this directory are intended to provide basic working frontend JavaScript, compatible
with the API backend examples available in the [`examples/api-backends`](../api-backends).

## CSRF and CORS compatibility

In order to be compatible with a CSRF-protected backend, frontend clients must:

1. Be served from a domain allowed by the backend's CORS Allowed Origins configuration.
1. `http://localhost*` for the backend examples provided
2. An example server to serve the HTML and JavaScript for the frontend examples from localhost is included in
[`examples/javascript-frontends/example-frontend-server`](../javascript-frontends/example-frontend-server)
3. Use the HTTP headers expected by the backend to send and receive CSRF Tokens.
The backends configure this as the Gorilla `csrf.RequestHeader`,
as well as the CORS Allowed Headers and Exposed Headers.
1. `X-CSRF-Token` for the backend examples provided
2. Note that some JavaScript HTTP clients automatically lowercase all received headers,
so the values must be accessed with the key `"x-csrf-token"` in the frontend code.
10 changes: 10 additions & 0 deletions examples/javascript-frontends/example-frontend-server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/gorilla-mux/examples/javascript-frontends/example-frontend-server

go 1.20

require (
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
)

require github.com/felixge/httpsnoop v1.0.3 // indirect
7 changes: 7 additions & 0 deletions examples/javascript-frontends/example-frontend-server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
42 changes: 42 additions & 0 deletions examples/javascript-frontends/example-frontend-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// +build ignore

package main

import (
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)

func main() {
router := mux.NewRouter()

loggingMiddleware := func(h http.Handler) http.Handler {
return handlers.LoggingHandler(os.Stdout, h)
}
router.Use(loggingMiddleware)

wd, err := os.Getwd()
if err != nil {
log.Panic(err)
}
// change this directory to point at a different Javascript frontend to serve
httpStaticAssetsDir := http.Dir(fmt.Sprintf("%s/../frontends/axios/", wd))

router.PathPrefix("/").Handler(http.FileServer(httpStaticAssetsDir))

server := &http.Server{
Handler: router,
Addr: "localhost:8081",
ReadTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
}

fmt.Println("starting http server on localhost:8081")
log.Panic(server.ListenAndServe())
}
34 changes: 34 additions & 0 deletions examples/javascript-frontends/frontends/axios/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Gorilla CSRF</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<div>
<h1>Gorilla CSRF: Axios JS Frontend</h1>
<p>See Console and Network tabs of your browser's Developer Tools for further details</p>
</div>

<div>
<h2>Get Request:</h2>
<h3>Full Response:</h3>
<code id="get-request-full-response"></code>
<h3>CSRF Token:</h3>
<code id="get-response-csrf-token"></code>
</div>


<div>
<h2>Post Request:</h2>
<h3>Full Response:</h3>
<p>
Note that the <code>X-CSRF-Token</code> value is in the Axios <code>config.headers</code>;
it is not a response header set by the server.
</p>
<code id="post-request-full-response"></code>
</div>
</body>
<script src="index.js"></script>
</html>
50 changes: 50 additions & 0 deletions examples/javascript-frontends/frontends/axios/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// make GET request to backend on page load in order to obtain
// a CSRF Token and load it into the Axios instance's headers
// https://github.com/axios/axios#creating-an-instance
const initializeAxiosInstance = async (url) => {
try {
let resp = await axios.get(url, {withCredentials: true});
console.log(resp);
document.getElementById("get-request-full-response").innerHTML = JSON.stringify(resp);

let csrfToken = parseCSRFToken(resp);
console.log(csrfToken);
document.getElementById("get-response-csrf-token").innerHTML = csrfToken;

return axios.create({
// withCredentials must be true to in order for the browser
// to send cookies, which are necessary for CSRF verification
withCredentials: true,
headers: {"X-CSRF-Token": csrfToken}
});
} catch (err) {
console.log(err);
}
};

const post = async (axiosInstance, url) => {
try {
let resp = await axiosInstance.post(url);
console.log(resp);
document.getElementById("post-request-full-response").innerHTML = JSON.stringify(resp);
} catch (err) {
console.log(err);
}
};

// general-purpose func to deal with clients like Axios,
// which lowercase all headers received from the server response
const parseCSRFToken = (resp) => {
let csrfToken = resp.headers[csrfTokenHeader];
if (!csrfToken) {
csrfToken = resp.headers[csrfTokenHeader.toLowerCase()];
}
return csrfToken
}

const url = "http://localhost:8080/api";
const csrfTokenHeader = "X-CSRF-Token";
initializeAxiosInstance(url)
.then(axiosInstance => {
post(axiosInstance, url);
});

0 comments on commit c1f4eb3

Please sign in to comment.