I have since gotten another solution for this. It's still not the same as nginx doing NTLM (which would be nice if the nginx command ever implements this). But at the moment, what I'm doing is working for us.
I wrote lua code that uses an encrypted cookie. The encrypted cookie contains the user ID, the time of its authentication, and the IP address from which it was authenticated. I am attaching this material here for reference. It is not polished, but perhaps you can use it to develop your own similar scheme.
Basically how it works:
- If the cookie is NOT accessible or if it has expired or is invalid, nginx makes a pre-auth call to the IIS backend application that transmits the client IP address and then redirects the client to the IIS web application where I have "Windows Authentication " The pre-auth internal IIS application service generates a GUID and saves a db entry for this guid and a flag indicating that this GUID should be authenticated.
- The browser redirects nginx to the authenticator application that passes the GUID.
- The IIS application authenticates the user through Windows authentication and updates the db entry for this GUID and client IP address with the user ID and the authentication time.
- The IIS application redirects the client back to the original request.
- The nginx lua code intercepts this call and again calls the support service call to the IIS application (post-authorization) and requests the user ID and the authentication time. This information is set in an encrypted cookie and sent to the browser. The request is allowed to pass, and REMOTE_USER is sent together.
- subsequent browser requests send a cookie, and the nginx lua code sees a valid cookie and proxies the request directly (without the need for re-authentication), passing the request header REMOTE_USER.
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 - dt; local diffOriginal = 0; if do ~= nil then diffOriginal = now - do; 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 = du, t = now, o = dt } ); ngx.header["Set-Cookie"] = "pca=" .. enc.encrypt( sessionData ) .. "; path=/" end ngx.req.set_header( "REMOTE_USER", du ); 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;
nginx conf sample:
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; }