1

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")
    }
} 
  • Whatever web app is sending the requests, inspect it with browser devtools and look in the devtools console to see what error the browser is reporting about the preflight failing. Because if your middleware is only receiving the CORS preflight OPTIONS request, that would seem to indicate that the response the browser is receiving is not what the browser needs in order to decide the preflight has been successful. So the browser stops after the preflight fails, and never moves on to trying the POST. – sideshowbarker Sep 23 '18 at 21:51
  • So it appears like the browser actually is making the POST request, however the middleware is not being triggered and the request is making it as far as the handleLogin method as I can see the output of this in the server logs (it appears the auth request is succeeding and we are getting a 200 OK response). The CORS middleware is not running so we are still getting the "No 'Access-Control-Allow-Origin' header is present on the requested resource" error in the devtools console (which seems very weird to me). – lightningbolt6 Sep 24 '18 at 21:01
  • Just to add, the middleware does appear to be adding all of the correct things to the OPTIONS request. – lightningbolt6 Sep 24 '18 at 21:03
  • I have managed to get this working - rewrote my own middleware library without using Negroni - I can see and have full control over what's going on and can make sure everything is being called at the right times. – lightningbolt6 Sep 25 '18 at 21:40
  • Cool — very glad to hear you got it working. And yeah sometimes rolling your own is the better way to go. As you describe, it has the advantage that you clearly know what it’s doing (and can troubleshoot it, etc., more easily). – sideshowbarker Sep 25 '18 at 22:34

0 Answers0