Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issues/158/examples for working api with javascript frontend #162

Merged
merged 10 commits into from
Aug 17, 2023
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.
15 changes: 15 additions & 0 deletions examples/api-backends/gorilla-mux/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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=
64 changes: 64 additions & 0 deletions examples/api-backends/gorilla-mux/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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"),
coreydaley marked this conversation as resolved.
Show resolved Hide resolved
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=
40 changes: 40 additions & 0 deletions examples/javascript-frontends/example-frontend-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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);
});