I am building a web app in Golang, using mux as a router and Negroni for managing middleware for the API.
I have the following middleware:
- Authentication middleware which checks that the client has a valid session and returns 401 if not
- Middleware to allow CORS requests which is based on an edited version of https://github.com/rs/cors/
I want to apply the middleware to specific routes as appropriate, i.e using the CORS middleware on all requests and checking auth only on the protected routes.
What is actually happens for me when I try to make API requests using the frontend is that the CORS middleware seems only to get called for an initial OPTIONS request for each API route and is not subsequently used.
For example:
- UI does a POST to /api/login with a JSON object containing the credentials
- In the browser console we see the initial preflight OPTIONS request getting a 200 OK with all the headers we set using handlePreflight in the corsMiddleware, so far so good.
- Then none of the subsequent POST requests hit the corsMiddleware. We know the middleware is never called as we never see it being called in the server log. We get the error "No 'Access-Control-Allow-Origin' header is present on the requested resource" in Chrome.
- The weird thing is that the function handleLogin is actually called, we can see the correct credentials and it appears that the server is correctly storing a session for them.
We also see strange things on some of the other routes - i.e when making Postman GET requests to /api/entities we are able to get the list of entities (which should be protected) when not logged in. It is as if the authentication middleware is not being called or is not behaving as expected.
I am fairly new to some of these concepts so perhaps I have misunderstood something. Any help would be appreciated.
My code is as follows (main.go):
router := mux.NewRouter()
router.HandleFunc("/", ServeUI).Methods("GET")
apiRouter := router.PathPrefix("/api").Subrouter()
authRouter := apiRouter.PathPrefix("/auth").Subrouter()
authRouter.HandleFunc("/login", HandleLogin).Methods("POST")
authRouter.HandleFunc("/logout", HandleLogout).Methods("POST")
entitiesRouter := apiRouter.PathPrefix("/entities").Subrouter()
entitiesRouter.HandleFunc("/", GetEntities).Methods("GET")
commonAPIMiddleware := negroni.New(corsMiddleware.NewCorsMiddleware())
router.PathPrefix("/api/auth").Handler(commonAPIMiddleware.With(
negroni.Wrap(authRouter),
))
router.PathPrefix("/api/entities").Handler(commonAPIMiddleware.With(
auth.NewAPIAuthMiddleware(),
negroni.Wrap(entitiesRouter),
))
n := negroni.New(negronilogrus.NewMiddleware())
n.UseHandler(router)
n.Run(":8009")
The code for corsMiddleware is as follows:
// CorsMiddleware allows CORS request for api routes
type CorsMiddleware struct {
}
// Negroni compatible interface
func (m *CorsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
log.Info("CORS MIDDLEWARE CALLED")
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
log.Info("ServeHTTP: Preflight request")
handlePreflight(w, r)
// Preflight requests are standalone and should stop the chain as some other
// middleware may not handle OPTIONS requests correctly. One typical example
// is authentication middleware ; OPTIONS requests won't carry authentication
// headers (see #1)
w.WriteHeader(http.StatusOK)
} else {
log.Info("ServeHTTP: Actual request")
handleActualRequest(w, r)
next(w, r)
}
}
// handlePreflight handles pre-flight CORS requests
func handlePreflight(w http.ResponseWriter, r *http.Request) {
headers := w.Header()
origin := r.Header.Get("Origin")
if r.Method != http.MethodOptions {
log.Info(" Preflight aborted: %s!=OPTIONS", r.Method)
return
}
// Always set Vary headers
// see https://github.com/rs/cors/issues/10,
// https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001
headers.Add("Vary", "Origin")
headers.Add("Vary", "Access-Control-Request-Method")
headers.Add("Vary", "Access-Control-Request-Headers")
if origin == "" {
log.Info(" Preflight aborted: empty origin")
return
}
headers.Set("Access-Control-Allow-Origin", origin)
// Spec says: Since the list of methods can be unbounded, simply returning the method indicated
// by Access-Control-Request-Method (if supported) can be enough
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
headers.Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, X-Requested-With")
headers.Set("Access-Control-Allow-Credentials", "true")
headers.Set("Access-Control-Max-Age", strconv.Itoa(1000))
log.Info(" Preflight response headers: %v", headers)
}
// handleActualRequest handles simple cross-origin requests, actual request or redirects
func handleActualRequest(w http.ResponseWriter, r *http.Request) {
log.Info("CORS HANDLING ACTUAL REQUEST")
headers := w.Header()
origin := r.Header.Get("Origin")
if r.Method == http.MethodOptions {
log.Info(" Actual request no headers added: method == %s", r.Method)
return
}
// Always set Vary, see https://github.com/rs/cors/issues/10
headers.Add("Vary", "Origin")
if origin == "" {
log.Info(" Actual request no headers added: missing origin")
return
}
headers.Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, X-Requested-With")
headers.Set("Access-Control-Allow-Credentials", "true")
headers.Set("Access-Control-Allow-Origin", origin)
if true {
headers.Set("Access-Control-Allow-Credentials", "true")
}
}