einfallstoll / express-ntlm

An express middleware to have basic NTLM-authentication in node.js.
BSD 2-Clause "Simplified" License
89 stars 26 forks source link

Reverse proxy support? #95

Open tomer953 opened 1 year ago

tomer953 commented 1 year ago

Hi,

The new version 2.7.0 states:

"Support usage behind a proxy if the application can distinguish its user"

We desperately need a solution to work behind reverse proxy (backend served with apache/traefik) and currently the ui is access the api directly without the proxy, which adds a lot of complexity to the infrastructure settings.

what do you mean by: "if the application can distinguish its user" ? should we pass something from the UI?

previously when we used Ntlm with reverse proxy, when we had two users make request at the same time, they got mixed responses (user1 get user2 response)

I hope we can solve this with the new version!

einfallstoll commented 1 year ago

Have a look at #93 and @jsfi might be able to answer this

tomer953 commented 1 year ago

Oh nice, missed that post.

Currently our UI (angular) is making all the http requests with withCredentials: true

I try to understand if some handshake or sending uid from/to the Ui is needed. or the default options you added to the PR "just works"

jsfi commented 1 year ago

Hello, happy to help you with any questions.

The defaults of the new options probably won't help you because they are the same code that was running before.

To authenticate a user, the middleware creates a proxy to communicate with the NTLM server. The important thing is that the proxy needs to be cached because there are multiple requests going back and forth but the connection to the NTLM server must stay open during the whole process.

By default the middleware links the proxy with the connection it has with the user. But this link is not working anymore when you add a reverse proxy because now the user does not connect to your express application directly and multiple user will share the same connection.

What my changes allow is that you can overwrite the default behavior. Instead of using the connection, which is not unique to the one user anymore after adding the reverse proxy, you can return an alternative identifier from getProxyId.

When the authentication is successful the user data will be cached. Again the default is caching the data on the connection, so you have to implement your own caching with addCachedUserData and getCachedUserData too.

I have created a demo on CodeSandbox which shows an example implementation that uses express-session: https://codesandbox.io/s/jsfi-express-ntlm-55mqkk

tomer953 commented 1 year ago

thanks for your example!

I wonder if my reverse proxy (Traefik) can generate some uuid per request, and use this header as identifier. I look into it. I tried to use the x-forwared-for ip but it alsobehind a proxy\nat and not unique. I'll try my best and update if succeseed.

b.t.w Do you mind PR the @types/express-ntlm as well ?

jsfi commented 1 year ago

b.t.w Do you mind PR the @types/express-ntlm as well ?

Good idea. I don't use them myself but I will look into it.

tomer953 commented 1 year ago

Ok,

I added some configuration to the Traefik

tracing:
  jaeger:
    samplingParam: 0
    traceContextHeaderName: "X-Request-ID"

this adds a unique id to each request, and I thought maybe reading this unique header will help. however, this DID NOT solve my problem, Since NTLM requires a persistent connection to maintain the handshake and Traefik is generating a new X-Request-ID for each request, breaking the stateful link required by the package.

So I thought maybe your solution will work, I added the solution just like you, with sessions

// init session middleware
app.use(
  session({
    secret: "some-secret-string",
    resave: false,
    saveUninitialized: true,
  })
);

// init ntlm middleware
app.use(
  ntlm({
    domaincontroller: process.env.DC,
    getProxyId: (req) => req.session.id,
    getCachedUserData: (req) => req.session.ntlm,
    addCachedUserData: (req, res, ntlm) => {
      req.session.ntlm = ntlm;
    },
  })
);

// access the ntlm user data
app.get("/api/ping", (req, res) =>
  res.send({ ntlm: req.ntlm, headers: req.headers })
);

However for the First call it gets Internal Server Error and later it works after debug the ntlm it shows:

[express-ntlm] No Authorization header present
[express-ntlm] Initiating connection to Active Directory server XX (domain undefined) using base DN "null"
[express-ntlm] Unexpected NTLM message Type 3 in new connection for URI http://localhost:3000/api/ping
jsfi commented 1 year ago

Yes, the proxy ID needs to be stable per user. A solution that provides a new ID for each request is not enough.

However for the First call it gets Internal Server Error and later it works after debug the ntlm it shows:

Can you specify some more what you mean by First call?

Please also test what happens if you add some logging to getProxyId, e.g.

getProxyId: (req) => {
  console.log('session', req.session.id);
  return req.session.id;
}

The session id should be stable per user.

tomer953 commented 1 year ago

npm run dev (to start local server) then open browser at http:\\localhost:3000\api\ping and get Internal Server Error Logs:

App is listening...
[express-ntlm] No Authorization header present
session XiyK...........SGu
[express-ntlm] Initiating connection to Active Directory server XX (domain undefined) using base DN "null"
session foXtM7.....4us
[express-ntlm] Unexpected NTLM message Type 3 in new connection for URI http://localhost:3000/api/ping

Different session key! why is that?

after refresh:

[express-ntlm] No Authorization header present
session foXtM79SPu9...GU
[express-ntlm] Initiating connection to Active Directory server XX (domain undefined) using base DN "null"
session foXtM79SPu9...GU
session foXtM79SPu9...GU

then NTLM works. something is broken when init the first request (also after deploy to the reverse proxy, and ask some users here to open the url, they see the error for the first time they make the call)

jsfi commented 1 year ago

The first request (NTLM type 1) has a different session id than the second request (NTLM type 3). So the middleware cannot find the cached proxy and gives you the "Unexpected NTLM message Type 3" error.

I am not able to reproduce this. In my application it is pretty unusual for the first request to be against a secured route but even if I test in a new private browser window directly against a protected URL the session ID that I see is stable. I am also behind a proxy (Caddy in my case).

Not sure how I can help you any further other than telling you that it's working for me. If you can provide a CodeSandbox or a repository that I can debug, I will take a look.

tomer953 commented 1 year ago

Thanks for the support my friend.

I made a sample app that reproduces the bug, but it kinda the same like yours, so no new info there. https://github.com/tomer953/express-ntlm-session/blob/main/index.js

Sometimes it fails:
image

Logs:

Restarting '.'
Listening on port 8080
[express-ntlm] No Authorization header present
session 8ufN6kYxZCz6NyfEBXom6pG1gm3FXYwu
[express-ntlm] No domaincontroller was specified, all Authentication messages are valid.
session HVDFiRRHxVFbYTZEEc9KKV0-cXjsH-Xc
[express-ntlm] Unexpected NTLM message Type 3 in new connection for URI http://localhost:8080/api/ping

and sometimes not:
image

[express-ntlm] No Authorization header present
session HVDFiRRHxVFbYTZEEc9KKV0-cXjsH-Xc
[express-ntlm] No domaincontroller was specified, all Authentication messages are valid.
session HVDFiRRHxVFbYTZEEc9KKV0-cXjsH-Xc
session HVDFiRRHxVFbYTZEEc9KKV0-cXjsH-Xc

In my production, I do use domaincontroller to verify the user against AD server, however the bug remain even if I omit it...

I tried it in two different networks in my Org (internal and external networks) and the error is occurring also in my dev environment (local pc, no proxy)