Created
March 18, 2016 23:35
-
-
Save casualjim/821e5350557492b92c0d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment