-- import requirements
local cjson = require "cjson"
-- Ubuntu broke the install. Puts the source in /usr/share/lua/5.1/https.lua,
-- but since the source defines itself as the module "ssl.https", after we
-- load the source, we need to grab the actual thing. Building from source
-- wasn't practical.
require "https"
local https = require "ssl.https"
local ltn12 = require("ltn12")
-- setup some app-level vars
local client_id = ngx.var.ngo_client_id
local client_secret = ngx.var.ngo_client_secret
local domain = ngx.var.ngo_domain
local cb_scheme = ngx.var.ngo_callback_scheme or ngx.var.scheme
local cb_server_name = ngx.var.ngo_callback_host or ngx.var.server_name
local cb_uri = ngx.var.ngo_callback_uri or "/_oauth"
local cb_url = cb_scheme.."://"..cb_server_name..cb_uri
local signout_uri = ngx.var.ngo_signout_uri or "/_signout"
local debug = ngx.var.ngo_debug
local whitelist = ngx.var.ngo_orgs or "reverb,wordnik"
local secure_cookies = ngx.var.ngo_secure_cookies
local uri_args = ngx.req.get_uri_args()
local ngx_headers = ngx.req.get_headers()
function get_basic_auth()
local header = ngx_headers['Authorization']
if header == nil or header:find(" ") == nil then
return
end
local divider = header:find(' ')
local auth_type = header:sub(0, divider-1)
if auth_type ~= 'Basic' and auth_type ~= 'token' then
return
elseif auth_type ~= 'Basic' then
local auth = ngx.decode_base64(header:sub(divider+1))
if auth == nil or auth:find(':') == nil then
return
end
local divider2 = auth:find(':')
if divider2 == nil or divider2 == "" then
return auth
else
return auth:sub(0, divider2 - 1)
end
else
local auth = header:sub(divider + 1)
if auth == nil then
return
end
return auth
end
return
end
function set_cookie(access_token, userinfo)
local cookie_tail = ";version=1;path=/;HttpOnly"
if secure_cookies then
cookie_tail = cookie_tail..";secure"
end
local name = userinfo["name"]
if name and name ~= "" then
name = ngx.escape_uri(name)
end
local email = userinfo["email"]
if email and email ~= "" then
email = ngx.escape_uri(email)
end
local picture = userinfo["avatar_url"]
if picture and picture ~= "" then
picture = ngx.escape_uri(name)
end
ngx.header["Set-Cookie"] = {
"AccessToken="..access_token..cookie_tail,
"Name="..name..cookie_tail,
"Email="..email..cookie_tail,
"Picture="..picture..cookie_tail
}
end
function delete_cookie()
ngx.header["Set-Cookie"] = {
"AccessToken=deleted;version=1;path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
"Name=;version=1;path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
"Email=;version=1;path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT",
"Picture=;version=1;path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
}
end
function validate_token( access_token )
if debug then
ngx.log(ngx.ERR, "DEBUG: validate_token "..(access_token or "NO TOKEN!!!!"))
end
if not access_token then
unauthenticated()
end
-- body
local userinfo = validate_user(access_token)
if not userinfo or not validate_org_membership(access_token, userinfo) then
unauthenticated()
end
return userinfo
end
function unauthenticated()
-- body
-- ngx.header.content_type = 'text/plain'
-- ngx.header.www_authenticate = 'Basic realm=""'
ngx.status = ngx.HTTP_UNAUTHORIZED
delete_cookie()
ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
function validate_org_membership(access_token, userinfo)
local send_headers = {
Authorization = "token "..access_token,
}
local orgs_table = {}
local res3, code3, headers3, status3 = https.request({
url = "https://api.github.com/user/orgs",
method = "GET",
headers = send_headers,
sink = ltn12.sink.table(orgs_table)
})
if code3~=200 then
ngx.log(ngx.ERR, "received "..code2.." from https://api.github.com/user/orgs")
return false
end
if debug then
ngx.log(ngx.ERR, "DEBUG: orgs response "..res3..code3..status3..table.concat(orgs_table))
end
local orgs = cjson.decode( table.concat(orgs_table))
local is_member = false
for i, org in pairs(orgs) do
if string.find(whitelist, org.login) then
is_member = true
end
end
return is_member
end
function validate_user(access_token)
local send_headers = {
Authorization = "token "..access_token,
}
local result_table = {}
local res2, code2, headers2, status2 = https.request({
url = "https://api.github.com/user",
method = "GET",
headers = send_headers,
sink = ltn12.sink.table(result_table),
})
if code2~=200 then
ngx.log(ngx.ERR, "received "..code2.." from https://api.github.com/user")
return nil -- ngx.exit(ngx.HTTP_UNAUTHORIZED)
end
if debug then
ngx.log(ngx.ERR, "DEBUG: userinfo response "..res2.." "..code2.." "..status2.." "..table.concat(result_table))
end
return cjson.decode( table.concat(result_table) )
end
function fetch_access_token(auth_code)
local init_send_headers = {
Accept = "application/json"
}
-- TODO: Switch to NBIO sockets
-- If I get around to working luasec, this says how to pass a function which
-- can generate a socket, needed for NBIO using nginx cosocket
-- http://lua-users.org/lists/lua-l/2009-02/msg00251.html
local bd = "code="..ngx.escape_uri(auth_code).."&client_id="..client_id.."&client_secret="..client_secret.."&redirect_uri="..ngx.escape_uri(cb_url)
local res, code, headers, status = https.request(
"https://github.com/login/oauth/access_token",
bd
)
if debug then
ngx.log(ngx.ERR, "DEBUG: token response "..res..code..status)
end
ngx.log(ngx.ERR, "received "..code..res.." from https://github.com/login/oauth/access_token")
if code~=200 then
ngx.log(ngx.ERR, "received "..code.." from https://github.com/login/oauth/access_token")
return nil
end
-- use version 1 cookies so we don't have to encode. MSIE-old beware
local json = ngx.decode_args(res, 0)
return json.access_token
end
function fetch_auth_code()
-- If no access token and this isn't the callback URI, redirect to oauth
if ngx.var.uri ~= cb_uri then
-- Redirect to the /oauth endpoint, request access to ALL scopes
return ngx.redirect("https://github.com/login/oauth/authorize?client_id="..client_id.."&scope=user:email,read:org&response_type=code&redirect_uri="..ngx.escape_uri(cb_url).."&state="..ngx.escape_uri(ngx.var.uri).."&login_hint="..ngx.escape_uri(domain))
end
-- Fetch the authorization code from the parameters
local auth_code = uri_args["code"]
local auth_error = uri_args["error"]
if auth_error then
ngx.log(ngx.ERR, "received "..auth_error.." from https://github.com/login/oauth/authorize")
unauthenticated()
end
if debug then
ngx.log(ngx.ERR, "DEBUG: fetching token for auth code "..auth_code)
end
return auth_code
end
-- See https://developers.google.com/accounts/docs/OAuth2WebServer
if ngx.var.uri == signout_uri then
delete_cookie()
return ngx.redirect(ngx.var.scheme.."://"..ngx.var.server_name)
end
if ngx.var.uri == "/favicon.ico" then
return
end
-- start the actual authentication process
local access_token = get_basic_auth()
if debug then
ngx.log(ngx.ERR, "basic auth access_token: "..(access_token or "NO TOKEN"))
end
if not access_token then
access_token = uri_args["access_token"]
if debug then
ngx.log(ngx.ERR, "query string access_token: "..(access_token or "NO TOKEN"))
end
if not access_token then
access_token = ngx.var.cookie_AccessToken
if debug then
ngx.log(ngx.ERR, "cookie access_token: "..(access_token or "NO TOKEN"))
end
end
end
if access_token then
if debug then
ngx.log(ngx.ERR, "got access_token: "..(access_token or "NO TOKEN"))
end
-- validate_token(access_token)
if debug then
ngx.log(ngx.ERR, "DEBUG: authorized "..access_token)
end
return
else
local auth_code = fetch_auth_code()
if debug then
ngx.log(ngx.ERR, "DEBUG: fetched auth_code "..(auth_code or "NO CODE"))
end
access_token = fetch_access_token(auth_code)
if debug then
ngx.log(ngx.ERR, "DEBUG: resolved access_token "..(auth_code or "NO TOKEN"))
end
local userinfo = validate_token(access_token)
set_cookie(access_token, userinfo)
-- Redirect
if debug then
ngx.log(ngx.ERR, "DEBUG: authorized "..userinfo["email"]..", redirecting to "..uri_args["state"])
end
ngx.redirect(uri_args["state"])
end