In the last two days, I’ve had to solve a rather interesting problem. I have an nginx instance proxying various servers, and I need to be able to add an authentication layer that will authenticate people with an external source (such as a web app) and allow them to pass through the proxy if they have an account on the authentication source (the web app, in this example).
Exploring the requirements
I considered various solutions for this, and I will list a few alternatives:
- A simple Python/Flask module that would do the actual proxying and authentication.
- An nginx module that would authenticate using subrequests (nginx can now do that).
- Using nginx’s Lua module to write some authentication code.
It became clear early on that adding another request to the whole system wouldn’t work very well, because of the added latency (it would be annoying to do this on every single request for every file on a page). This means that the subrequest module is out. The Python/Flask solution also feels like it’s bypassing a lot of nginx, so it’s also out. This leaves Lua, which nginx supports natively.
Since I don’t want to authenticate with the external server on every request, I decided to generate tokens which people can store and just present to the server so they are let through. However, since the Lua module doesn’t have any way (that I have found) to keep state, we can’t actually store these tokens anywhere. How do you verify that the user is who they say they are when you have no memory?
Solving the problem
Cryptographic signing to the rescue! We can give our users signed cookies with a username and expiration date, and very easily verify that they actually are who they say they are, while easily being able to expire the tokens as well.
With nginx, all we need to do is specify the access_by_lua_file /our/file.lua
directive in the location config, and that location will be protected with our script. Now, to write the actual checking code:
-- Some variable declarations.
local cookie = ngx.var.cookie_MyToken
local hmac = ""
local timestamp = ""
-- Check that the cookie exists.
if cookie ~= nil and cookie:find(":") ~= nil then
-- If there's a cookie, split off the HMAC signature
-- and timestamp.
local divider = cookie:find(":")
hmac = cookie:sub(divider+1)
timestamp = cookie:sub(0, divider-1)
-- Verify that the signature is valid.
if hmac_sha1("some very secret string", timestamp) == hmac and tonumber(timestamp) >= os.time() then
return
end
end
-- Internally rewrite the URL so that we serve
-- /auth/ if there's no valid token.
ngx.exec("/auth/")
The code above should be pretty straightforward. The signing happens by taking some plaintext (in this case, a timestamp, but it can be anything you want), and HMACing (using this very nice Lua HMAC library) it with a secret key, producing a signature which only we can generate, and which the user cannot tamper with without invalidating it.
When the user tries to load a resource, we check that the signature in the cookie is valid, and let them pass if it is. Otherwise, we redirect them to a token issuing server, which will authenticate and give them a signed token if not.
What I overlooked is that Lua does string interning, which makes string comparisons just pointer comparisons, and thus constant-time.
The token issuing server
Now that we have some amazing token-checking code written, all we need to do is write a server to actually issue these tokens. I could have written this in Python with Flask, but I wanted to give Go a shot because I’m a language hipster and Go seems cool. It would probably take a bit less time to do it in Python, but I enjoy it.
This server will pop up an HTTP basic auth form, check the credentials you enter and, if they are correct, it will give give you a signed token which is good for one (1) hour of proxy access. That way, you only have to authenticate with the external service once, and subsequent authentication checks are done at the nginx layer and are pretty fast.
The request handler
Writing a handler that would pop up a basic auth box wasn’t very hard, but Go isn’t terribly well documented, so I had to hunt around a bit. It was pretty simple, in the end, and here it is, HTTP basic authentication in Go:
func handler(w http.ResponseWriter, r *http.Request) {
if username := checkAuth(r); username == "" {
w.Header().Set("WWW-Authenticate", `Basic realm="The kingdom of Stavros"`)
w.WriteHeader(401)
w.Write([]byte("401 Unauthorized\n"))
} else {
fmt.Printf("Authenticated user %v.\n", username)
token := getToken()
setTokenCookie(w, token)
fmt.Fprintf(w, "<html><head><script>location.reload()</script></head></html>")
}
}
Setting the token and cookie
Once we’ve authenticated the user, we need to set a cookie with their token. We just do the same thing we did in Lua, above, only much more easily because Go includes actual crypto packages in the standard library. The code for this is equally straightforward, if underdocumented:
func getToken() string {
expiration := int(time.Now().Unix()) + 3600
mac := hmac.New(sha1.New, []byte("some very secret string"))
mac.Write([]byte(fmt.Sprintf("%v", expiration)))
expectedMAC := fmt.Sprintf("%x", mac.Sum(nil))
return fmt.Sprintf("%v:%s", expiration, expectedMAC)
}
func setTokenCookie(w http.ResponseWriter, token string) {
rawCookie := fmt.Sprintf("MyToken=%s", token)
expire := time.Now().Add(time.Hour)
cookie := http.Cookie{"MyToken",
token,
"/",
".example.com",
expire,
expire.Format(time.UnixDate),
3600,
false,
true,
rawCookie,
[]string{rawCookie}}
http.SetCookie(w, &cookie)
}
Tying it all together
To finish our big bundle of amazingness, we just need a function that checks the authentication provided by the user, and we’re done! Here’s what I lifted off some library, it currently only checks for a specific username/password combination, so integrating with third-party services is left as an exercise for the reader:
func checkAuth(r *http.Request) string {
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 || s[0] != "Basic" {
return ""
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
return ""
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return ""
}
if pair[0] != "username" || pair[1] != "password" {
return ""
}
return pair[0]
}
Conclusion
I have taken quite a liking to nginx’s Lua module. It allows you to perform simple operations right in the web server’s request/response cycle, and it makes a lot of sense for some things like authentication checks to proxied web servers. This sort of thing would have been very hard to do without a programmable web server, as we would pretty much have to write our own HTTP proxy.
The code above is pretty short and rather elegant, so I’m very happy with it overall. I’m not sure how much time it adds to the response, but, given that authentication is necessary, I think it will be worth it (and fast enough not to be a problem anyway).
Another good thing about it is that you can enable this with just a single directive in an nginx location
block, so there’s no long configuration to keep track of. I find it a very elegant solution overall, and am very glad to know nginx lets me do something like this if I need it in the future.
If you have any recommendations or feedback, please leave a comment (especially if I’ve screwed something up).