Skip to content

Commit c1f4eb3

Browse files
francoposaCorey Daley
andauthored
issues/158/examples for working api with javascript frontend (#162)
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>
1 parent 73c96a5 commit c1f4eb3

File tree

10 files changed

+269
-0
lines changed

10 files changed

+269
-0
lines changed

examples/api-backends/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# API Backends
2+
3+
Examples in this directory are intended to provide basic working backend CSRF-protected APIs,
4+
compatible with the JavaScript frontend examples available in the
5+
[`examples/javascript-frontends`](../javascript-frontends).
6+
7+
In addition to CSRF protection, these backends provide the CORS configuration required for
8+
communicating the CSRF cookies and headers with JavaScript client code running in the browser.
9+
10+
See [`examples/javascript-frontends`](../javascript-frontends/README.md) for details on CORS and
11+
CSRF configuration compatibility requirements.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// +build ignore
2+
3+
module github.com/gorilla-mux/examples/api-backends/gorilla-mux
4+
5+
go 1.20
6+
7+
require (
8+
github.com/gorilla/csrf v1.7.1
9+
github.com/gorilla/handlers v1.5.1
10+
github.com/gorilla/mux v1.8.0
11+
)
12+
13+
require (
14+
github.com/felixge/httpsnoop v1.0.3 // indirect
15+
github.com/gorilla/securecookie v1.1.1 // indirect
16+
github.com/pkg/errors v0.9.1 // indirect
17+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2+
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
3+
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
4+
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
5+
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
6+
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
7+
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
8+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
9+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
10+
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
11+
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
12+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
13+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// +build ignore
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"log"
8+
"net/http"
9+
"os"
10+
"strings"
11+
"time"
12+
13+
"github.com/gorilla/csrf"
14+
"github.com/gorilla/handlers"
15+
"github.com/gorilla/mux"
16+
)
17+
18+
func main() {
19+
router := mux.NewRouter()
20+
21+
loggingMiddleware := func(h http.Handler) http.Handler {
22+
return handlers.LoggingHandler(os.Stdout, h)
23+
}
24+
router.Use(loggingMiddleware)
25+
26+
CSRFMiddleware := csrf.Protect(
27+
[]byte("place-your-32-byte-long-key-here"),
28+
csrf.Secure(false), // false in development only!
29+
csrf.RequestHeader("X-CSRF-Token"), // Must be in CORS Allowed and Exposed Headers
30+
)
31+
32+
APIRouter := router.PathPrefix("/api").Subrouter()
33+
APIRouter.Use(CSRFMiddleware)
34+
APIRouter.HandleFunc("", Get).Methods(http.MethodGet)
35+
APIRouter.HandleFunc("", Post).Methods(http.MethodPost)
36+
37+
CORSMiddleware := handlers.CORS(
38+
handlers.AllowCredentials(),
39+
handlers.AllowedOriginValidator(
40+
func(origin string) bool {
41+
return strings.HasPrefix(origin, "http://localhost")
42+
},
43+
),
44+
handlers.AllowedHeaders([]string{"X-CSRF-Token"}),
45+
handlers.ExposedHeaders([]string{"X-CSRF-Token"}),
46+
)
47+
48+
server := &http.Server{
49+
Handler: CORSMiddleware(router),
50+
Addr: "localhost:8080",
51+
ReadTimeout: 60 * time.Second,
52+
WriteTimeout: 60 * time.Second,
53+
}
54+
55+
fmt.Println("starting http server on localhost:8080")
56+
log.Panic(server.ListenAndServe())
57+
}
58+
59+
func Get(w http.ResponseWriter, r *http.Request) {
60+
w.Header().Add("X-CSRF-Token", csrf.Token(r))
61+
w.WriteHeader(http.StatusOK)
62+
}
63+
64+
func Post(w http.ResponseWriter, r *http.Request) {
65+
w.WriteHeader(http.StatusOK)
66+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# JavaScript Frontends
2+
3+
Examples in this directory are intended to provide basic working frontend JavaScript, compatible
4+
with the API backend examples available in the [`examples/api-backends`](../api-backends).
5+
6+
## CSRF and CORS compatibility
7+
8+
In order to be compatible with a CSRF-protected backend, frontend clients must:
9+
10+
1. Be served from a domain allowed by the backend's CORS Allowed Origins configuration.
11+
1. `http://localhost*` for the backend examples provided
12+
2. An example server to serve the HTML and JavaScript for the frontend examples from localhost is included in
13+
[`examples/javascript-frontends/example-frontend-server`](../javascript-frontends/example-frontend-server)
14+
3. Use the HTTP headers expected by the backend to send and receive CSRF Tokens.
15+
The backends configure this as the Gorilla `csrf.RequestHeader`,
16+
as well as the CORS Allowed Headers and Exposed Headers.
17+
1. `X-CSRF-Token` for the backend examples provided
18+
2. Note that some JavaScript HTTP clients automatically lowercase all received headers,
19+
so the values must be accessed with the key `"x-csrf-token"` in the frontend code.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/gorilla-mux/examples/javascript-frontends/example-frontend-server
2+
3+
go 1.20
4+
5+
require (
6+
github.com/gorilla/handlers v1.5.1
7+
github.com/gorilla/mux v1.8.0
8+
)
9+
10+
require github.com/felixge/httpsnoop v1.0.3 // indirect
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2+
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
3+
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
4+
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
5+
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
6+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
7+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// +build ignore
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"log"
8+
"net/http"
9+
"os"
10+
"time"
11+
12+
"github.com/gorilla/handlers"
13+
"github.com/gorilla/mux"
14+
)
15+
16+
func main() {
17+
router := mux.NewRouter()
18+
19+
loggingMiddleware := func(h http.Handler) http.Handler {
20+
return handlers.LoggingHandler(os.Stdout, h)
21+
}
22+
router.Use(loggingMiddleware)
23+
24+
wd, err := os.Getwd()
25+
if err != nil {
26+
log.Panic(err)
27+
}
28+
// change this directory to point at a different Javascript frontend to serve
29+
httpStaticAssetsDir := http.Dir(fmt.Sprintf("%s/../frontends/axios/", wd))
30+
31+
router.PathPrefix("/").Handler(http.FileServer(httpStaticAssetsDir))
32+
33+
server := &http.Server{
34+
Handler: router,
35+
Addr: "localhost:8081",
36+
ReadTimeout: 60 * time.Second,
37+
WriteTimeout: 60 * time.Second,
38+
}
39+
40+
fmt.Println("starting http server on localhost:8081")
41+
log.Panic(server.ListenAndServe())
42+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Gorilla CSRF</title>
6+
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
7+
</head>
8+
<body>
9+
<div>
10+
<h1>Gorilla CSRF: Axios JS Frontend</h1>
11+
<p>See Console and Network tabs of your browser's Developer Tools for further details</p>
12+
</div>
13+
14+
<div>
15+
<h2>Get Request:</h2>
16+
<h3>Full Response:</h3>
17+
<code id="get-request-full-response"></code>
18+
<h3>CSRF Token:</h3>
19+
<code id="get-response-csrf-token"></code>
20+
</div>
21+
22+
23+
<div>
24+
<h2>Post Request:</h2>
25+
<h3>Full Response:</h3>
26+
<p>
27+
Note that the <code>X-CSRF-Token</code> value is in the Axios <code>config.headers</code>;
28+
it is not a response header set by the server.
29+
</p>
30+
<code id="post-request-full-response"></code>
31+
</div>
32+
</body>
33+
<script src="index.js"></script>
34+
</html>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// make GET request to backend on page load in order to obtain
2+
// a CSRF Token and load it into the Axios instance's headers
3+
// https://github.com/axios/axios#creating-an-instance
4+
const initializeAxiosInstance = async (url) => {
5+
try {
6+
let resp = await axios.get(url, {withCredentials: true});
7+
console.log(resp);
8+
document.getElementById("get-request-full-response").innerHTML = JSON.stringify(resp);
9+
10+
let csrfToken = parseCSRFToken(resp);
11+
console.log(csrfToken);
12+
document.getElementById("get-response-csrf-token").innerHTML = csrfToken;
13+
14+
return axios.create({
15+
// withCredentials must be true to in order for the browser
16+
// to send cookies, which are necessary for CSRF verification
17+
withCredentials: true,
18+
headers: {"X-CSRF-Token": csrfToken}
19+
});
20+
} catch (err) {
21+
console.log(err);
22+
}
23+
};
24+
25+
const post = async (axiosInstance, url) => {
26+
try {
27+
let resp = await axiosInstance.post(url);
28+
console.log(resp);
29+
document.getElementById("post-request-full-response").innerHTML = JSON.stringify(resp);
30+
} catch (err) {
31+
console.log(err);
32+
}
33+
};
34+
35+
// general-purpose func to deal with clients like Axios,
36+
// which lowercase all headers received from the server response
37+
const parseCSRFToken = (resp) => {
38+
let csrfToken = resp.headers[csrfTokenHeader];
39+
if (!csrfToken) {
40+
csrfToken = resp.headers[csrfTokenHeader.toLowerCase()];
41+
}
42+
return csrfToken
43+
}
44+
45+
const url = "http://localhost:8080/api";
46+
const csrfTokenHeader = "X-CSRF-Token";
47+
initializeAxiosInstance(url)
48+
.then(axiosInstance => {
49+
post(axiosInstance, url);
50+
});

0 commit comments

Comments
 (0)