13

Anyone knows if is possible to do reverse proxy with Windows authentication that uses NTLM? I cant find any example on this. What should be the values of more_set_headers field?

location / {
            proxy_http_version      1.1;
            proxy_pass_request_headers on;
            proxy_set_header        Host            $host;
            proxy_set_header        X-Real-IP       $remote_addr;
            proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;


            more_set_input_headers  'Authorization: $http_authorization';

            proxy_set_header  Accept-Encoding  "";

            proxy_pass              http://host/;
            proxy_redirect          default;
            #This is what worked for me, but you need the headers-more mod
            more_set_headers        -s 401 'WWW-Authenticate: Basic realm="host.local"';
}

If I access the host directly the authentication succeed if I access with the reverse proxy the authentication fail every time.

matheus
  • 131
  • 1
  • 1
  • 5
  • for those who are new to nginx like me the more_set_input_headers and more_set_headers config lines might make your nginx crash at first (showing Aborted in HTTP logs) this is because you need to install headers-more-nginx-module, more info here https://github.com/openresty/headers-more-nginx-module#installation – baywet May 02 '16 at 15:50

5 Answers5

14

To enable NTLM pass-through with Nginx -

upstream http_backend {
    server 2.3.4.5:80;
    keepalive 16;
}
server {
    ...
    location / {
       proxy_pass http://http_backend/;
       proxy_http_version 1.1;
       proxy_set_header Connection "";
    ...
    }
 }

-- Ramon

poke
  • 369,085
  • 72
  • 557
  • 602
Fizz
  • 181
  • 1
  • 3
  • I have a very similar [question](http://serverfault.com/questions/754351/what-is-a-correct-ways-to-allow-login-to-an-iis-site-through-a-reverse-proxy) to this, if you have time, and can answer it, there's currently a bounty on it. – trueCamelType Feb 16 '16 at 16:57
  • 1
    @Fizz You mean we don't need commercial version of nginx for NTLM support? – DaNeSh May 27 '16 at 22:00
  • The same is not working for me. Why? Using tomcat in backend. – Naveen Kumar Nov 02 '16 at 13:17
  • @EdgarChavolla it works with express-ntlm, only if you have a session. otherwise, users will log in as each other. – DaNeSh Nov 16 '16 at 22:21
  • 3
    This seemed to be working, but in fact it doesn't. In my case it led to user sessions mixed up (!!!), i.e. a user may suddenly log in as another user. See this reply http://mailman.nginx.org/pipermail/nginx/2016-February/049889.html – Maksim Tyutmanov Sep 18 '19 at 12:44
4

As far as I know, this is currently not possible with nginx. I investigated this in depth myself just a little while ago. The basic problem is that NTLM authentication will require the same socket be used on the subsequent request, but the proxy doesn't do that. Until the nginx development team provides some kind of support for this behavior, the way I handled this was by resorting to authenticate in the reverse proxy itself. I am currently doing this using apache 2.2, mod_proxy, mod_auth_sspi (not perfect, but works). Good luck! Sorry nginx, I love you, but we could really use some help for this common use case.

  • Tony, so if I got it right you did the following: Users connect to Apache server, then Apache authenticate user itlsef with NTLM. After that Apache start a reverse proxy connection with some server and send it back to User? – matheus Feb 07 '14 at 15:40
  • Basically, yes. The client authenticates to apache running mod_auth_sspi. apache proxies the request to some server while injecting the user id into a request header. this is one mode of operation of siteminder for example. just be sure to prevent direct access to your backend servers. this is one way of doing this. better approaches are possible. redirect to auth server for example and use an oauth2 style token mechanism. – Tony Schwartz Apr 09 '14 at 17:06
4

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:

  1. 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.
  2. The browser is redirected by nginx to the authenticator app passing the GUID.
  3. 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.
  4. The IIS app redirects the client back to the original request.
  5. 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.
  6. 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;
}
  • Hi Tony. I would love to see how you actually implemeneted this. Would you consider putting a post together to take somebody through it, or help me out in any way? I currently have two apache frone end web servers that authenticate all users via NTML and it's horrendously slow. I apprecaite people will say I can tune Apache (blah , blah) but my experience with nginx is that it's much faster and I would just prefer to use it. I have IIS boxes available to use, so if I could copy your setup and use an IIS box for authentiation and rely on nginx to do the webserver work that would be fantastic. – BeardedGeek Oct 06 '14 at 16:16
  • BeardedGeek, I'm just now reading this post. If I have some time soon, I will try to put together a step by step or even just bundle it up. – Tony Schwartz Nov 13 '14 at 19:08
  • @BeardedGeek, instead of authenticating every single request, what I do is authenticate only the access to the application, so from that point on the application's session cookie is used. That really speeds up the whole thing. – Manel Clos Aug 11 '15 at 08:39
1

Ok, we wrote lua code for nginx/openresty, which solves ntlm reverse-proxy issue with some solvable limitations and without need of commercial nginx version

broomrider
  • 614
  • 1
  • 7
  • 16
1

According to nginx documentation:

Allows proxying requests with NTLM Authentication. The upstream connection is bound to the client connection once the client sends a request with the “Authorization” header field value starting with “Negotiate” or “NTLM”. Further client requests will be proxied through the same upstream connection, keeping the authentication context.

upstream http_backend {
    server 127.0.0.1:8080;

    ntlm;
}

but the ntlm; option is available only with a commercial subscription (Nginx Plus)

You can use this module for non-production environments.

gabihodoroaga/nginx-ntlm-module

The module is not complete but it was enough for me to solve my issues. There is also a blog post about this module at hodo.dev.