We built the OAUTH2 authentication and authorization layer using Nginx's LUA middleware. If you have this intention, read the documentation below to automate and gain revenue.
SeatGeek has developed over the past few years, and we have accumulated a lot of different management interfaces for various tasks. We typically create new modules for new presentation requirements, such as our own blogs, charts, and more. We also regularly develop internal tools to handle such issues as deployment, visualization, and event handling. In dealing with these transactions, we have used several different interfaces to authenticate:
- Github/google Oauth
- We seatgeek the internal user system
- Basic Certification
- Hard coded Login
Obviously, the actual application is very nonstandard. Multiple authentication systems make it difficult to abstract a variety of databases for access levels and common permissions.
Single System Certification
We have also done some research on how to set up solutions to our problems. This prompted the advent of Odin, which worked well in verifying the users of Google Apps. Unfortunately it needs to use Apache, and we've been married to Nginx and have it as the front end of our backend application.
Luckily, I read MIXLR's blog and cited their LUA application in Nginx:
- Modify Response Headers
- overriding internal requests
- Selectively deny host access based on IP
The last one looks very interesting. It opens a hell of a package-management trip.
Build a nginx that supports LUA
The Lua for Nginx is not included in the core of nginx, and we often build nginx for OS X for development testing, for Linux builds for deployment.
custom Nginx for OS X
For OS X systems, I recommend using homebrew for package management. It has a very good reason for the very few modules that the initial Nginx installation package has enabled:
The key is that Nginx has so many options, and it must be crazy to add them all to the initial package, and if we just put some of them in it will force us to join them, which will drive us crazy.
-Charlie Sharpsteen, @sharpie
So we need to build it ourselves. Reasonable construction of nginx can facilitate our further expansion later. Fortunately, using homebrew for package management is very convenient and quick.
We need a working space first:
Copy Code code as follows:
After that, we need to find the initial installation Information pack. You can get it in any of the following ways:
- Locate the Homebrew_prefix directory, usually under/usr/local, where the nginx.rb file is found
- Obtain HTTPS://RAW.GITHUB.COM/MXCL/HOMEBREW/MASTER/LIBRARY/FORMULA/NGINX.RB from the following address
- Use the following command brew cat Nginx > NGINX.RB
At this point, if we execute the brew install./NGINX.RB command, it will install nginx based on the information in it. Now that we want to fully customize Nginx, we'll rename the packets so that when we update through the Brew Update command, we will not overwrite our customizations:
Copy Code code as follows:
MV Nginx.rb NGINX-CUSTOM.RB
Cat Nginx-custom.rb | Sed ' s/class nginx/class nginxcustom/' >> tmp
RM nginx-custom.rb
MV TMP NGINX-CUSTOM.RB
We can now add the modules we need to the installation package and start compiling. It's very simple, we simply pass all the modules we need to the Brew install command in the form of the following code:
Copy Code code as follows:
# collects arguments from ARGV
def collect_modules Regex=nil
Argv.select {|arg| arg.match (regex)!= nil}.collect {|arg| arg.gsub (regex, ')}
End
# get Nginx modules so are not compiled in by default specified in ARGV
def Nginx_modules; Collect_modules (/^--include-module-/); End
# get Nginx modules this are available on GitHub specified in ARGV
def Add_from_github; Collect_modules (/^--add-github-module=/); End
# get Nginx modules from Mdounin's HG repository specified in ARGV
def Add_from_mdounin; Collect_modules (/^--add-mdounin-module=/); End
# Retrieve a repository from GitHub
def fetch_from_github Name
Name, repository = name.split ('/')
Raise "You must specify a repository name for GitHub modules" if Repository.nil?
Puts "-adding #{repository} from GitHub ..."
' Git clone-q git://github.com/#{name}/#{repository} modules/#{name}/#{repository} '
Path = dir.pwd + '/modules/' + name + '/' + repository
End
# Retrieve A tar of a package from Mdounin
def fetch_from_mdounin Name
Name, hash = Name.split (' # ')
Raise "You must specify a commits SHA for Mdounin modules" if Hash.nil?
Puts "-adding #{name} from Mdounin ..."
' Mkdir-p modules/mdounin && CD $_; Curl-s-O http://mdounin.ru/hg/#{name}/archive/#{hash}.tar.gz; TAR-ZXF #{hash}.tar.gz '
Path = dir.pwd + '/modules/mdounin/' + name + '-' + hash
End
The above auxiliary module allows us to specify the desired module and retrieve the address of the module. Now we need to modify the nginx-custom.rb file so that it contains the names of the modules and retrieve them in the package, near line 58:
Copy Code code as follows:
Nginx_modules.each {|name| args << "--with-#{name}"; puts "-Adding #{name} module"}
Add_from_github.each {|name| args << "--add-module=#{fetch_from_github (Name)}"}
Add_from_mdounin.each {|name| args << "--add-module=#{fetch_from_mdounin (Name)}"}
Now we can compile our nginx with a new customization:
Copy Code code as follows:
Brew install./NGINX-CUSTOM.RB \
--add-github-module=agentzh/chunkin-nginx-module \
--include-module-http_gzip_static_module \
--add-mdounin-module=ngx_http_auth_request_module#a29d74804ff1
You can easily find the above information package in Seatgeek/homebrew-formulae.
Custom Nginx for Debian
We typically deploy to Debian distributions-usually on Ubuntu as our product servers. If so, it would be very simple to run dpkg-i Nginx-custom install our custom package. This step is so simple that you do it as soon as you run it.
Some notes when searching for custom Debian/ubuntu packages:
- You can obtain the Debian installation package by Apt-get source Package_name.
- The Debian installer package is controlled by a rules file and you need to sed-fu to manipulate it.
- You can update the Deb package dependencies by editing the control file. Note that some of the meta dependencies are specified here (meta-dependency) You don't want to delete it, but it's easy to tell.
- The new release must be indicated in the changelog, otherwise the package may not be upgraded because it may have been installed. You need to use +tag_name in your form to indicate which changes you have made to your baseline. I'll add a number-starting from 0-to indicate the release number of the package.
- Most of the changes can be changed automatically in some way, but there seems to be no simple command-line tool to create a custom release package. This is also where we are interested, if you know anything, please give us some links, tools or methods.
While running this great process, I built a small batch script that automates the main steps of the process, and you can find it on the gist on GitHub.
It took only 90 nginx packages to build time before I realized that the process could be scripted.
All OAuth
Now you can test and deploy the LUA scripts embedded in Nginx, let's start with LUA programming.
The Nginx-lua module provides some accessibility and variables to access the vast majority of nginx functions, and it is clear that we can force the OAuth authentication to be opened through the instructions provided by the module in Access_by_lua.
When you use the *_by_lua_file directive, you must overload the Nginx to make it work.
I created a simple OAuth2 provider class for SeatGeek with Nodejs. This section is simple and you can easily get a response version of what you are in the common language.
Next, our OAuth API uses JSON to process tokens (token), access levels (access level), and re-authenticate responses (re-authentication responses). So we need to install the Lua-cjson module.
Copy Code code as follows:
# Install Lua-cjson
if [!-D lua-cjson-2.1.0]; Then
Tar zxf lua-cjson-2.1.0.tar.gz
Fi
CD lua-cjson-2.1.0
Sed ' s/i686/x86_64/'/usr/share/lua/5.1/luarocks/config.lua >/usr/share/lua/5.1/luarocks/config.lua-tmp
Rm/usr/share/lua/5.1/luarocks/config.lua
Mv/usr/share/lua/5.1/luarocks/config.lua-tmp/usr/share/lua/5.1/luarocks/config.lua
Luarocks make
My OAuth provider class uses query-string to send authenticated error messages, and we also need to support them in our LUA scripts:
Copy Code code as follows:
Local args = Ngx.req.get_uri_args ()
If args.error and args.error = "Access_denied" Then
Ngx.status = Ngx. Http_unauthorized
Ngx.say ("{\" status\ ": 401, \" message\ ": \" "). Args.error_description ... " \"}")
Return Ngx.exit (NGX. HTTP_OK)
End
Now that we've solved the basic error condition, we're going to set up cookies for the access token. In my case, the cookie expires before the access token expires, so I can use cookies to refresh the access token.
Copy Code code as follows:
Local Access_token = Ngx.var.cookie_SGAccessToken
If Access_token Then
ngx.header["Set-cookie"] = "sgaccesstoken=". Access_token.. "; path=/; max-age=3000 "
End
Now we have resolved the API for the error response and stored the Access_token for subsequent access. We now need to ensure that the OAuth certification process starts correctly. Below, we want to:
- If no access_token has been or will be stored, open OAuth authentication
- If you have OAuth access code in the arguments of query string, use the OAuth API to retrieve the user's Access_token
- Request denied using illegal access code user
Reading Nginx-lua functions and variables can solve some of the problems and may also tell you various ways to access specific request/response information.
At this point, we need to get a token from our API interface. Nginx-lua provides a ngx.location.capture method that supports the initiation of an internal request to the Redis and receives a response. This means that we cannot directly invoke similar http://seatgeek.com/ncaa-football-tickets, but we can use Proxy_pass to wrap this external link into an internal request.
We usually agree to put an _ (underscore) in front of such an internal request to prevent external direct access.
Copy Code code as follows:
--The first step, get the token from the API
If not access_token or Args.code then
If Args.code Then
--Internal-oauth:1337/access_token
Local res = ngx.location.capture ("/_access_token?client_id=" ...) app_id ... " &client_secret= ". App_secret ... " &code= ". Args.code)
--Terminate all illegal requests
If Res.status ~= Then
Ngx.status = Res.status
Ngx.say (Res.body)
Ngx.exit (NGX. HTTP_OK)
End
--Decoding token
Local Text = Res.body
Local JSON = Cjson.decode (text)
Access_token = Json.access_token
End
--Cookie and Proxy_pass token request failed
If not Access_token then
--track user access for transparent redirection
ngx.header["Set-cookie"] = "sgredirectback=". Nginx_uri.. "; path=/; Max-age=120 "
--Redirect to/oauth, get permissions
Return Ngx.redirect ("internal-oauth:1337/oauth?client_id="). app_id ... " &scope=all ")
End
End
At this point in the Lua script, there should already be a usable access_token. We can use it to get the user information required by any request. In this article, returning 401 means no permissions, 403 means token expires, and authorization information is packaged in simple numbers as a JSON response.
Copy Code code as follows:
--Ensure users have access to Web applications
--Internal-oauth:1337/accessible
Local res = ngx.location.capture ("/_user", {args = {Access_token = Access_token}})
If Res.status ~= Then
--Delete the corrupted token
ngx.header["Set-cookie"] = "sgaccesstoken=deleted; path=/; Expires=thu, 01-jan-1970 00:00:01 GMT "
-If token is damaged, redirect 403 Forbidden to OAuth
if Res.status = = 403 Then
Return Ngx.redirect ("https://seatgeek.com/oauth?client_id="). app_id ... " &scope=all ")
End
--No permissions
Ngx.status = Res.status
Ngx.say (' {' status ': 503, ' message ': ' Error accessing api/me for Credentials '} ')
Return Ngx.exit (NGX. HTTP_OK)
End
Now that we have verified that the user is really authenticated and has a certain level of access, we can check their access levels to see if there is a conflict with the level of access that we have defined for any current endpoint. I personally removed the sgaccesstoken in this step so that the user has the ability to log in with a different user, but it's not up to you to decide.
Copy Code code as follows:
Local JSON = Cjson.decode (res.body)
--Ensure we have the minimum for access_level to this resource
If Json.access_level < 255 then
--Expire their stored token
ngx.header["Set-cookie"] = "sgaccesstoken=deleted; path=/; Expires=thu, 01-jan-1970 00:00:01 GMT "
--Disallow access
Ngx.status = Ngx. Http_unauthorized
Ngx.say ("{\" status\ ": 403, \" message\ ": \" user_id "). json.user_id. "Has no access to this resource\"} "
Return Ngx.exit (NGX. HTTP_OK)
End
--Store The Access_token within a cookie
ngx.header["Set-cookie"] = "sgaccesstoken=". Access_token.. "; path=/; max-age=3000 "
--Support redirection back to your request if necessary
Local Redirect_back = Ngx.var.cookie_SGRedirectBack
If Redirect_back Then
ngx.header["Set-cookie"] = "sgredirectback=deleted; path=/; Expires=thu, 01-jan-1970 00:00:01 GMT "
Return Ngx.redirect (Redirect_back)
End
Now we just need to pass some request header information to inform our current application who logged in. You can reuse Remote_user, and if you have a need, you can use this to replace Basic authentication, and everything else is a fair game.
Copy Code code as follows:
---Set Some headers for use within the protected endpoint
Ngx.req.set_header ("X-user-access-level", Json.access_level)
Ngx.req.set_header ("X-user-email", Json.email)
I can now access these HTTP headers in my application like any other site, instead of using hundreds of lines of code and a lot of time to implement authentication again.
Nginx and Lua, in the tree structure.
At this point, we should have a LUA script that can be used to block/deny access. We can put this script in a file on disk and use the Access_by_lua_file configuration to apply it to our Nginx site. In SeatGeek, we use Chief to templating output profiles, although you can use Puppet,fabric, or any other tool you like.
Here are the simplest nginx sites you can use to make everything work. You may also want to check the next Access.lua-here-it is a compiled file of the Lua script above.
Copy Code code as follows:
# The app we are proxying to
Upstream Production-app {
Server localhost:8080;
}
# The internal OAuth provider
Upstream Internal-oauth {
Server localhost:1337;
}
server {
Listen 80;
server_name private.example.com;
Root/apps;
CharSet Utf-8;
# This'll run for everything but subrequests
Access_by_lua_file "/etc/nginx/access.lua";
# Used in a subrequest
Location/_access_token {Proxy_pass Http://internal-oauth/oauth/access_token;}
Location/_user {Proxy_pass http://internal-oauth/user;}
Location/{
Proxy_set_header X-real-ip $remote _addr;
Proxy_set_header x-forwarded-for $proxy _add_x_forwarded_for;
Proxy_set_header Host $http _host;
Proxy_redirect off;
Proxy_max_temp_file_size 0;
if (!-f $request _filename) {
Proxy_pass Http://production-app;
Break
}
}
}
Further thinking
Although this setting works better, I want to point out some of the drawbacks:
- The code above is a simplification of our Access_by_lua script. We also handle the request to save post submissions, JS to add to the page update session automatically processing token updates, etc., you may not need these features, and in fact, I do not think I need them until we begin our system test in the internal system.
- We have some nodes that can be basically authenticated through a certain background task. These are modified, and the data is retrieved from an external store, such as S3. Note that this is not always possible, so the use may not be the answer you want.
- Oauth2 is just the standard I choose. In theory, you can use the Facebook authorization to achieve similar results. You can also use this method to limit speed, or to store different access levels in the database, such as in your LUA script for easy operation and retrieval. If you are really bored, you can re implement basic certifications in Lua, which only needs you.
- There is no test control system and so on. Testers will be afraid when they realize that this will be a time of integration testing. You can rerun the mock up for global injection of variables as well as execute scripts, but it is not the ideal setting.
- You also need to modify the application to identify your new access headers. The internal tools will be the simplest, but you may need to make some concessions for the vendor software.