I have since come up with another solution for this. This is still not the same as nginx doing the NTLM (which will be nice if the nginx team ever implements this). But, for now, what I'm doing works for us.
I've written some lua code that uses an encrypted cookie. The encrypted cookie contains the user's id, the time he authenticated and the ip address from which he authenticated. I'm attaching this stuff here for reference. It's not polished, but perhaps you can use it to develop your own similar scheme.
Basically, how it works is:
- If the cookie is NOT available or if it's expired or invalid, nginx makes a service call (pre-auth) to a backend IIS application passing the client's IP address and then redirects the client to an IIS web application where I have "Windows Authentication" turned on. The back-end IIS application's pre-auth service generates a GUID and stores an entry in the db for that guid and a flag indicating this GUID is about to be authenticated.
- The browser is redirected by nginx to the authenticator app passing the GUID.
- The IIS app authenticates the user via windows authentication and updates the db record for that GUID and client IP address with the user id and the time authenticated.
- The IIS app redirects the client back to the original request.
- nginx lua code intercepts this call and makes a back-door service call to the IIS app again (post-auth) and asks for the user id and time authenticated. This information is set in an encrypted cookie and is sent to the browser. The request is allowed to pass through and the REMOTE_USER is sent along.
- subsequent requests by the browser pass the cookie and the nginx lua code sees the valid cookie and proxies the request directly (without needing to authenticate again of course) by passing the REMOTE_USER request header.
access.lua:
local enc = require("enc");
local strings = require("strings");
local dkjson = require("dkjson");
function beginAuth()
local headers = ngx.req.get_headers();
local contentTypeOriginal = headers["Content-Type"];
print( contentTypeOriginal );
ngx.req.set_header( "Content-Type", "application/json" );
local method = ngx.req.get_method();
local body = "";
if method == "POST" then
local requestedWith = headers["X-Requested-With"];
if requestedWith ~= nil and requestedWith == "XMLHttpRequest" then
print( "bailing, won't allow post during re-authentication." );
ngx.exit(ngx.HTTP_GONE); -- for now, we are NOT supporting a post for re-authentication. user must do a get first. cookies can't be set on these ajax calls when redirecting, so for now we can't support it.
ngx.say("Reload the page.");
return;
else
print( "Attempting to handle POST for request uri: " .. ngx.var.uri );
end
ngx.req.read_body();
local bodyData = ngx.req.get_body_data();
if bodyData ~= nil then
body = bodyData;
end
end
local json = dkjson.encode( { c = contentTypeOriginal, m = method, d = body } );
local origData = enc.base64encode( json );
local res = ngx.location.capture( "/preauth", { method = ngx.HTTP_POST, body = "{'clientIp':'" .. ngx.var.remote_addr .. "','originalUrl':'" .. ngx.var.FrontEndProtocol .. ngx.var.host .. ngx.var.uri .. "','originalData':'" .. origData .. "'}" } );
if contentTypeOriginal ~= nil then
ngx.req.set_header( "Content-Type", contentTypeOriginal );
else
ngx.req.clear_header( "Content-Type" );
end
if res.status == 200 then
ngx.header["Access-Control-Allow-Origin"] = "*";
ngx.header["Set-Cookie"] = "pca=guid:" .. enc.encrypt( res.body ) .. "; path=/"
ngx.redirect( ngx.var.authurl .. "auth/" .. res.body );
else
ngx.exit(res.status);
end
end
function completeAuth( cookie )
local guid = enc.decrypt( string.sub( cookie, 6 ) );
local contentTypeOriginal = ngx.header["Content-Type"];
ngx.req.set_header( "Content-Type", "application/json" );
local res = ngx.location.capture( "/postauth", { method = ngx.HTTP_POST, body = "{'clientIp':'" .. ngx.var.remote_addr .. "','guid':'" .. guid .. "'}" } );
if contentTypeOriginal ~= nil then
ngx.req.set_header( "Content-Type", contentTypeOriginal );
else
ngx.req.clear_header( "Content-Type" );
end
if res.status == 200 then
local resJson = res.body;
-- print( "here a1" );
-- print( resJson );
local resTbl = dkjson.decode( resJson );
if resTbl.StatusCode == 0 then
resTbl = resTbl.Result;
local time = os.time();
local sessionData = dkjson.encode( { u = resTbl.user, t = time, o = time } );
ngx.header["Set-Cookie"] = "pca=" .. enc.encrypt( sessionData ) .. "; path=/"
ngx.req.set_header( "REMOTE_USER", resTbl.user );
if resTbl.originalData ~= nil and resTbl.originalData ~= "" then
local tblJson = enc.base64decode( resTbl.originalData );
local tbl = dkjson.decode( tblJson );
if tbl.m ~= nil and tbl.m == "POST" then
ngx.req.set_method( ngx.HTTP_POST );
ngx.req.set_header( "Content-Type", tbl.c );
ngx.req.read_body();
ngx.req.set_body_data( tbl.d );
end
end
else
ngx.log( ngx.ERR, "error parsing json " .. resJson );
ngx.exit(500);
end
else
print( "error completing auth." );
ngx.header["Set-Cookie"] = "pca=; path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; token=deleted;"
print( res.status );
ngx.exit(res.status);
end
end
local cookie = ngx.var.cookie_pca;
print( cookie );
if cookie == nil then
beginAuth();
elseif strings.starts( cookie, "guid:" ) then
completeAuth( cookie );
else
-- GOOD TO GO...
local json = enc.decrypt( cookie );
local d = dkjson.decode( json );
local now = os.time();
local diff = now - d.t;
local diffOriginal = 0;
if d.o ~= nil then
diffOriginal = now - d.o;
end
if diff > 3600 or diffOriginal > 43200 then
beginAuth();
elseif diff > 300 then
print( "regenerating new cookie after " .. tostring( diff ) .. " seconds." );
local sessionData = dkjson.encode( { u = d.u, t = now, o = d.t } );
ngx.header["Set-Cookie"] = "pca=" .. enc.encrypt( sessionData ) .. "; path=/"
end
ngx.req.set_header( "REMOTE_USER", d.u );
end
strings.lua:
local private = {};
local public = {};
strings = public;
function public.starts(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
function public.ends(String,End)
return End=='' or string.sub(String,-string.len(End))==End
end
return strings;
enc.lua:
-- for base64, try something like: http://lua-users.org/wiki/BaseSixtyFour
local private = {};
local public = {};
enc = public;
local aeslua = require("aeslua");
private.key = "f8d7shfkdjfhhggf";
function public.encrypt( s )
return base64.base64encode( aeslua.encrypt( private.key, s ) );
end
function public.decrypt( s )
return aeslua.decrypt( private.key, base64.base64decode( s ) );
end
return enc;
sample nginx conf:
upstream dev {
ip_hash;
server app.server.local:8080;
}
set $authurl http://auth.server.local:8082/root/;
set $FrontEndProtocol https://;
location / {
proxy_pass http://dev/;
proxy_set_header Host $host;
proxy_redirect default;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Real-IP $remote_addr;
proxy_buffers 128 8k;
access_by_lua_file conf/lua/app/dev/access.lua;
}