I started out with testing an issue which I was facing on my setup. My setup included an API server written in Golang and a HTML client code which is to be run on browser. This backend server hosted HTTP and Websocket API endpoints:
/api/v1/login: To authenticate the user and get a login token. This login token is sent to client as part of HTTP Cookies in response headers/api/v1/auth/showdata: All subsequent requests will have above cookie set and hence server can authenticate such requests based on associated cookie/ws/api/v1/auth/streamdata: Websocket API endpoint is used to stream data. Since this is also an authenticated request, browser should set cookie for every request to Websocket APITo achieve above cookie based authentication, I planned to test it out locally with minimal code before applying my changes on production code. So two main things to be achieved once Cookie is sent as part of Login response:
I performed my testing on Chrome browser (Version 90) and below steps are only tested on the mentioned browser. But before we look at the cross-origin requests, we must understand CORS restriction. Here's a nice article to understand this.
python -m SimpleHTTPServer 8800Hence we deal with two different origins:
http://localhost:8800 : Python Webserverhttp://localhost:1323: Golang Backend ServerWhen we open URL #1, it makes requests to URL #2. This is cross origin requests and it is blocked by browsers for security reasons
Golang API server (test_server.go):
package mainimport ( "net/http" "time" jwt "github.com/dgrijalva/jwt-go" "github.com/labstack/echo" "github.com/labstack/echo/middleware")func login(c echo.Context) error { claims := jwt.StandardClaims{ IssuedAt: time.Now().Unix(), // 1 day expiration for now ExpiresAt: time.Now().AddDate(0, 0, 1).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS512, &claims) // Set ID just for local testing jwtToken, err := token.SignedString([]byte("testkey")) if err != nil { return err } httpCookie := http.Cookie{ Name: "token", Value: jwtToken, Expires: time.Unix(claims.ExpiresAt, 0), } c.SetCookie(&httpCookie) return c.JSON(http.StatusOK, "Logged in successfully")}func show_data(c echo.Context) error { jwtToken := "" for _, httpCookie := range c.Request().Cookies() { if httpCookie.Name == "token" { jwtToken = httpCookie.Value break } } if jwtToken == "" { return c.JSON(http.StatusUnauthorized, "No token found") } token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) { return []byte("testkey"), nil }) if !token.Valid || err != nil { return c.JSON(http.StatusUnauthorized, "token is invalid") } sampleData := []string{ "test1", "test2", } return c.JSON(http.StatusOK, sampleData)}func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.POST("/api/v1/login", login) e.POST("/api/v1/auth/showdata", show_data) e.Logger.Fatal(e.Start(":1323"))}Started from terminal using the following command:
❯ go run test_server.go____ __/ __/___/ / ___/ _// __/ _ \/ _ \/___/\__/_//_/\___/ v3.3.6High performance, minimalist Go web frameworkhttps://echo.labstack.com____________________________________O/_______O\⇨ http server started on [::]:1323
HTML client (test_client.html):
<html> <head> <script type = "text/javascript"> function Login() { const Http = new XMLHttpRequest(); const url='http://localhost:1323/api/v1/login'; Http.open("POST", url, true); Http.send(); Http.onreadystatechange = (e) => { console.log("Login", Http.responseText) } } function ShowData() { const Http = new XMLHttpRequest(); const url='http://localhost:1323/api/v1/auth/showdata'; Http.open("POST", url, true); Http.send(); Http.onreadystatechange = (e) => { console.log("Data", Http.responseText) } } </script> </head> <body> <div id = "login"> <a href = "javascript:Login()">Login</a> </div> <div id = "showdata"> <a href = "javascript:ShowData()">ShowData</a> </div> </body></html>Now because we are dealing with different origins, requests to backend server from browser via webserver will not go through. Here's browser console log on perfoming the following actions:
http://localhost:1323/test_client.htmlLogin textAccess to XMLHttpRequest at 'http://localhost:1323/api/v1/login' from origin 'http://localhost:8800' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains the invalid value ''.
To allow access to such cross origins, we use the following CORS policy in our API server code:
e.Use(middleware.Logger()) e.Use(middleware.Recover())+ corsConfig := middleware.DefaultCORSConfig+ corsConfig.AllowOrigins = []string{"http://localhost:8800"}+ e.Use(middleware.CORSWithConfig(corsConfig))+ e.POST("/api/v1/login", login) e.POST("/api/v1/auth/showdata", show_data) e.Logger.Fatal(e.Start(":1323"))Now when we open http://localhost:8800/test_client.html and click Login text, it will go through
But then when we click ShowData text, we see following error in Browser's console:
test_client.html:22 POST http://localhost:1323/api/v1/auth/showdata 401 (Unauthorized)
This is because we didn't set XMLHttpRequest.withCredentials it to true. This is required for browser to store cookie or any other credentials like authorization headers or TLS client certificates for cross-site requests
Hence we'll need the following changes:
In HTML client code, set withCredentials to true:
const Http = new XMLHttpRequest(); const url='http://localhost:1323/api/v1/login'; Http.open("POST", url, true);+ Http.withCredentials = true; Http.send(); Http.onreadystatechange = (e) => { const Http = new XMLHttpRequest(); const url='http://localhost:1323/api/v1/auth/showdata'; Http.open("POST", url, true);+ Http.withCredentials = true; Http.send();In API server code, update CORS policy:
corsConfig := middleware.DefaultCORSConfig corsConfig.AllowOrigins = []string{"http://localhost:8800"}+ corsConfig.AllowCredentials = true e.Use(middleware.CORSWithConfig(corsConfig)) e.POST("/api/v1/login", login)This should now fix the cookie issue.