Let’s suppose that we have a monolith that needs to do lot of things, like process data, read and write to the DB, spawn an HTTP server for an API and also authenticate it’s incoming request.
There are scenarios where it would be nice to decouple some responsibilities. This could be because we have a team that can put all it’s effort into maintain that part of the system, or maybe there is a need to migrate the back end logic from one framework to another, etc.
In this case, this article proposes an example on how to make an HTTP server that stands in front the main server, authorize the incoming requests and then it forwards them to the back end or returns a response with a 401 status code.
Making our target server
In our target we have only two main blogs: our main
function and a welcome
handler that will be used for all of the incoming request. The first one just makes a new Mux for an HTTP server and then starts listening in port 8080.
The handler is also simple:
First, it checks if the incoming request comes from certain port (like a white list). If not, it gives a 401 response and return.
Then, if we have have an exported environment variable named SECRET, it checks if the incoming request has any header with that secret, and if it matches the one we have exported. If not, it respond with another 401 status code and return.
If everything is OK, it adds 3 headers and returns a body that says “hi from target server”.
Making our proxy server
We want something simple that could be extensible, so we will be having a main function that will instantiate our reverse proxy, the main server and it’s handlers:
In our reverse proxy we call a function that will return a function with the following signature: func (*http.Request)
, this will be our director function.
The Director in the httputil.ReverseProxy
struct is just a function that can access the request and make modifications like adding headers, change the URL, host, cookies depending on our forwarding logic.
For example, in director/director.go
we have this:
Basically, we have a function that access two environment variables and then returns a function that modifies the request to froward to the new host.
In our case, the HOST variable will be the host of our target server. This means that any incoming request served by the reverse proxy will be mapped to the target server.
For example, in our case http://localhost:8081/hi
will be passed to http://localhost:8080/hi
.
Now lets see what is happening in our router package.
In line 25 of our main.go file we had r := rt.NewRouter(&rp)
. So there is a function that receives a pointer to the reverse proxy and then it returns a router from the github.com/julienschmidt/httprouter package.
If we go to our router/router.go file we have the following:
There we instantiate a router and assign some handlers for each desired method.
At first view it appears that we check for an authorization header in the most common HTTP verbs and we just forwards the OPTIONS
verb.
Although it appears simple, this design allow us to plug and play different auth strategies depending on the structure and path of the URL, making it extensible and easy to maintain.
Now is time for the strategies.
Our strategies must be of the type httprouter.Handler
because is what our router uses. This is just a function with the following signature:
func (http.ResponseWriter, *http.Request, httprouter.Params)
In our example, the FwdOptionsReq
function is the most simple case.
As we can see, each strategy is a a function that receives the reverse proxy and then returns a function that can access the response writer, the request, the params, etc. In this case, the returned handler just serve the HTTP request using the reverse proxy and then pass the response from the target to the client.
Is interesting to notice that we are using high order functions but we could be using other things like structs that had a pointer to the reverse proxy and a methods which represents the handlers. This is just one implementation.
Let’s see now how the strategy/checkAuthHeader.go
file looks like:
In this case, we first load two environment variables so we can use it in the handler. Then, in the returned function, we check if there is any secret. If there is one, we enrich the headers with it.
Then we get the “Auth” header and compare it with the value we got from the “AUTH” environment variable.
If both do not match, we use the provided ResponseWriter and make an early response. Only if everything is OK we continue and forward the request to the target server.
A tiny demo
In order to follow this demo you can get the code cloning this github repo:
Golang Auth proxy example
This is just an example on how to use the httputil.ReverseProxy
type provided in the golang standard library.
-
Open a terminal in the root folder and run
go run target/main.go
. This will start the target server. It will only respond to request fromlocalhost:8081
-
Open another terminal in the root folder and run the following commands:
# first export some env variables
export SCHEMA="http"
export HOST="localhost:8080"
export Auth="123456"
# then run proxy server
run go proxy/main.go
This will start our auth proxy in localhost:8081
and will redirect traffic to http://localhost:8080
only if there is an header like "Auth: 123456"
-
Open another terminal and start making some requests
-
Have fun
First let’s run the target server opening a terminal and running:
go run target/main.go
# outputs: Starting target server
Then we need to open another terminal and let’s export some variables:
export SCHEME="http"
export HOST="localhost:8080"
export AUTH="123456"
And then let’s just start the reverse proxy running the following command:
go run proxy/main.go
# outputs:
#
# SCHEME http
# HOST localhost:8080
# Auth 123456
# Starting proxy server
# Routes Registered
Now let’s try to CURL our target server:
curl -X GET -i http://localhost:8081
# outputs:
#
# HTTP/1.1 403 Forbidden
# Date: Mon, 10 May 2021 17:16:57 GMT
# Content-Length: 12
# Content-Type: text/plain; charset=utf-8
#
# Auth needed
But, if we provide that header:
curl -X GET -H "Auth: 123456" -i http://localhost:8081
# outputs:
#
# HTTP/1.1 200 OK
# Content-Length: 23
# Content-Type: text/plain; charset=utf-8
# Date: Mon, 10 May 2021 17:17:56 GMT
# X-Host: localhost:8082
# X-Method: GET
# X-Url: /
#
# hi from target server!
Thanks for reading. This was just a little example on how to use the provided reverse proxy provided the standard Go library.
If you have some improvements or corrections, please feel free to make a comment (or a pull request to the repo).
If you liked this post, please share it :)
The code for this example can be found here: https://github.com/LautaroJayat/go_auth_proxy_example
Top comments (0)