Skip to content

Instantly share code, notes, and snippets.

@lusis
Last active April 11, 2018 13:58
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lusis/6005442 to your computer and use it in GitHub Desktop.
Save lusis/6005442 to your computer and use it in GitHub Desktop.

Warning: Like the shitty OSS person I am, I forgot to update this. This particular iteration is exploitable in a really basic way that I missed. I will update this with the updated version hopefully soon

Reasoning

While authing against our Google Apps domain has worked pretty well up until now, we really needed a way to auth against out Github organization. Not everyone who is accessing some of our protected development content has an email account in our Google Apps domain. They do, however, have access to our github org.

Sadly it seems that apache and nginx modules for doing oauth are lacking.

I was hoping to avoid the whole lua approach (and mod_authnz_external was a no go from the start). However I realized that Brian Akins (@bakins) had done some fancy omnibus work that got me 90% of the way there.

From there it was a matter of patching up the omnibus repo to bring it to current versions as well as adding in a few additional components.

Requirements

Software

You'll either need to build your own nginx packages from here:

or you can grab mine from here:

These are omnibus builds of nginx + openresty created by @bakins. They have pretty much everything you need to get started to do some fancy lua application related stuff right in nginx.

GitHub application

Go to your github account and add a new application under your github org. (https://github.com/organizations/ORGNAME/settings/applications/)

  • Application Name is arbitrary.
  • Homepage URL is the url to the site you're protecting
  • Callback URL is http[s]://mysite.com/_callback

Make note of the ID and Secret you're given. You'll need those.

Configuration

If you just want a simple test, it's pretty straightforward.

  • install the package
  • edit /opt/nginx/etc/nginx.conf with the attached conf file
  • edit /opt/nginx/etc/access.lua with the attach lua script making the appropriate changes noted below
local oauth = {
    app_id = "MY_GITHUB_APP_ID",
    app_secret = "MY_GITHUB_APP_SECRET",
    orgs_whitelist = {["MY_GITHUB_ORG"]=true},

Note that org names are case-sensitive.

Start nginx (this will start in the foreground)

cd /opt/nginx/
sudo LD_PRELOAD=/opt/nginx/lib/libmmap_lowmem.so nginx -c /opt/nginx/etc/nginx.conf -p /opt/nginx/etc/

In another window, you might want to tail -f /opt/nginx/log/*

Load up the site in your browser. You should get redirected to github to authorize the application. After you auth, you'll get redirected BACK to your site (and will likely get a 404 since we don't actually have any content to serve).

This is just a POC really. You'll want to likely tweek the access.lua appropriately and maybe even restrict access to a given repository.

References

I got most of the inspiration (okay all of it) from a shitload of other people. Here are the big ones in no specific order

I'm actually pretty excited about the openresty stuff but really the ability to extend nginx generically with lua is pretty awesome too.

-- handles all the authentication, don't touch me
-- skip favicon
local block = ""
if ngx.var.uri == "/favicon.ico" then return ngx.location.capture(ngx.var.uri) end
ngx.log(ngx.INFO, block, "################################################################################")
-- import requirements
local cjson = require("cjson")
local https = require("ssl.https")
local url = require("socket.url")
local ltn12 = require("ltn12")
local function pt(t)
s = ""
for k,v in pairs(t) do s=s.."("..k..","..v.."), " end
ngx.log(ngx.INFO, block, s)
end
-- TODO: make this an oauth lib
-- note that org names are case-sensitive
local oauth = {
app_id = "MY_GITHUB_APP_ID",
app_secret = "MY_GITHUB_APP_SECRET",
orgs_whitelist = {["MY_GITHUB_ORG"]=true},
scope = "repo,user,user:email",
authorize_base_url = "https://github.com/login/oauth/authorize",
access_token_url = "https://github.com/login/oauth/access_token",
user_orgs_url = "https://api.github.com/user/orgs",
}
oauth.authorize_url = oauth.authorize_base_url.."?client_id="..oauth.app_id.."&scope="..oauth.scope
function oauth.request(url_string, method)
local result_table = {}
local url_table = {
url = url.build(url.parse(url_string, {port = 443})),
method = method,
sink = ltn12.sink.table(result_table),
headers = {
["accept"] = "application/json"
}
}
local body, code, headers, status_line = https.request(url_table)
local json_body = ""
for i, value in ipairs(result_table) do json_body = json_body .. value end
ngx.log(ngx.INFO, block, "body::", json_body)
return {body=cjson.decode(json_body), status=code, headers=headers}
end
function oauth.get(url_string)
return oauth.request(url_string, "GET")
end
function oauth.get_access_token(code)
local params = {
access_token_url=oauth.access_token_url,
client_id=oauth.app_id,
client_secret=oauth.app_secret,
code=code,
redirect_uri=oauth.redirect_uri,
}
local url_string = oauth.access_token_url.."?"..ngx.encode_args(params)
return oauth.get(url_string)
end
function oauth.verify_user(access_token)
local params = {access_token=access_token}
local url_string = oauth.user_orgs_url.."?"..ngx.encode_args(params)
local response = oauth.get(url_string)
local body = response.body
if body.error then
return {status=401, message=body.error}
end
for i, org in ipairs(body) do
ngx.log(ngx.INFO, block, "testing", org.login)
if oauth.orgs_whitelist[org.login] then
ngx.log(ngx.INFO, block, org.login, " is in orgs_whitelist")
return {status=200, body={access_token=access_token, org=org, access_level=9001}}
end
end
return {status=403, message='not authorized for any orgs'}
end
--- end oauth lib
local args = ngx.req.get_uri_args()
local cookie_jar = {}
local function set_cookies(cookie_jar)
local vals={}
for k,v in pairs(cookie_jar) do table.insert(vals,v) end
ngx.header["Set-Cookie"] = vals
end
local function del_cookie(c, cookie_jar)
cookie_jar[c] = c.."=deleted; path=/; Expires=Thu, 01-Jan-1970 00:00:01 GMT"
set_cookies(cookie_jar)
end
local function set_cookie(c, v, cookie_jar, age)
age = age or 3000
cookie_jar[c] = c.."="..v.."; path=/;Max-Age="..age
set_cookies(cookie_jar)
end
-- extract previous token from cookie if it is there
local access_token = ngx.var.cookie_NGAccessToken or nil
local authorized = ngx.var.cookie_NGAuthorized or nil
local redirect_back = ngx.var.cookie_NGRedirectBack or ngx.var.uri
redirect_back = (string.match(redirect_back, "/_callback%??.*")) and "/" or redirect_back
ngx.log(ngx.INFO, block, "redirect_back0=", redirect_back)
if access_token == "" then access_token = nil end
if authorized ~= "true" then authorized = nil end
if access_token then set_cookie('NGAccessToken', access_token, cookie_jar) end
if authorized then set_cookie('NGAuthorized', authorized, cookie_jar) end
if redirect_back then set_cookie('NGRedirectBack', redirect_back, cookie_jar, 120) end
-- We have nothing, do it all
if authorized ~= "true" or not access_token then
block = "[A]"
ngx.log(ngx.INFO, block, 'authorized=', authorized)
ngx.log(ngx.INFO, block, 'access_token=', access_token)
-- first lets check for a code where we retrieve
-- credentials from the api
if not access_token or args.code then
if args.code then
response = oauth.get_access_token(args.code)
-- kill all invalid responses immediately
if response.status ~= 200 or response.body.error then
ngx.status = response.status
ngx.header["Content-Type"] = "application/json"
response.body.auth_wall = "something went wrong with the OAuth process"
ngx.say(cjson.encode(response.body))
ngx.exit(ngx.HTTP_OK)
end
-- decode the token
access_token = response.body.access_token
end
-- both the cookie and proxy_pass token retrieval failed
if not access_token then
ngx.log(ngx.INFO, block, 'no access_token')
-- Redirect to the /oauth endpoint, request access to ALL scopes
set_cookies(cookie_jar)
return ngx.redirect(oauth.authorize_url)
end
end
end
if authorized ~= "true" then
block = "[B]"
ngx.log(ngx.INFO, block, 'authorized=', authorized)
ngx.log(ngx.INFO, block, 'access_token=', access_token)
-- ensure we have a user with the proper access app-level
local verify_user_response = oauth.verify_user(access_token)
if verify_user_response.status ~= 200 then
-- delete their bad token
del_cookie('NGAccessToken', cookie_jar)
-- Redirect 403 forbidden back to the oauth endpoint, as their stored token was somehow bad
if verify_user_response.status == 403 then
set_cookies(cookie_jar)
return ngx.redirect(oauth.authorize_url)
end
-- Disallow access
ngx.status = verify_user_response.status
ngx.say('{"status": 503, "message": "Error accessing oauth.api for credentials"}')
set_cookies(cookie_jar)
return ngx.exit(ngx.HTTP_OK)
end
-- Ensure we have the minimum for access_level to this resource
if verify_user_response.body.access_level < 255 then
-- Expire their stored token
del_cookie('NGAccessToken', cookie_jar)
del_cookie('NGAuthorized', cookie_jar)
-- Disallow access
ngx.status = ngx.HTTP_UNAUTHORIZED
ngx.say('{"status": 403, "message": "USER_ID "'..access_token..'" has no access to this resource"}')
return ngx.exit(ngx.HTTP_OK)
end
-- Store the access_token within a cookie
set_cookie('NGAccessToken', access_token, cookie_jar)
set_cookie('NGAuthorized', "true", cookie_jar)
end
-- should be authorized by now
-- Support redirection back to your request if necessary
ngx.log(ngx.INFO, block, "redirect_back1=", redirect_back)
local redirect_back = ngx.var.cookie_NGRedirectBack
ngx.log(ngx.INFO, block, "redirect_back2=", redirect_back)
if redirect_back then
ngx.log(ngx.INFO, block, "redirect_back3=", redirect_back)
del_cookie('NGRedirectBack', cookie_jar)
return ngx.redirect(redirect_back)
end
ngx.log(ngx.INFO, block, "--------------------------------------------------------------------------------")
-- 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)
worker_processes 1;
daemon off;
error_log /opt/nginx/log/error.log info;
events {
worker_connections 1024;
}
lua_code_cache off;
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
lua_need_request_body on;
access_by_lua_file "/opt/nginx/etc/access.lua";
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
@bakins
Copy link

bakins commented Jul 16, 2013

Your oauth request will block the nginx child process. You can use a location and ngx.location.capture to avoid this. Something like:

in nginx.conf

location /oauth {
    proxy_set_header Accept application/json;
    proxy_pass $arg_oath_url;
}

then in your code:

function oauth.request(url_string, method)
    local res = ngx.location.capture("/oauth," { method = method, args = { oauth_url = url_string })

    return {body=cjson.decode(res.body), status=res.code, headers=res.header}
end

Untested and done from memory, but presented as an idea.

@josegonzalez
Copy link

I would do as @bakins says, for the exact reason he mentions. You don't want to block #nodejs

@michaelmaring
Copy link

@bakins im getting an error with that function

2 failed to load lua inlined code: /opt/nginx/etc/access.lua:59: ')' expected near '{', client: 10.0.0.142, server: localhost, request: "GET / HTTP/1.1", host: "***"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment