SamDecrock / node-http-ntlm

Node.js module to authenticate using HTTP NTLM
MIT License
192 stars 89 forks source link

React native implementation - node core modules #83

Closed KristineTrona closed 1 year ago

KristineTrona commented 5 years ago

Hey,

I am working on a React Native project that uses NTLM authentication in the backend. I tried to implement this library in my project, but it is not possible due to the node-core dependencies being required (like url, crypto) - any good work around for that?

Perhaps somebody else trying to implement NTLM authentication from React Native front-end?

Any help is much appreciated! Thanks!

wbelhomsi commented 5 years ago

@KristineTrona well this was just done and tested yesterday (and it works!), thanks to @maa105 who created a frontend ntlm auth for people using something like a cordova container

httpntlm-web-maa

Note:

  1. Make sure that your server sends 200 to OPTIONS request without the need of authentication
  2. Make sure your server has the following header Access-Control-Expose-Headers: location, www-authenticate

Hope this helps

KristineTrona commented 5 years ago

@wbelhomsi thank you for the update! I tried to implement this library, but still got a 401 in return and Unauthorized access due to invalid credentials. I think it could be because response from sendType1Message returns a 401 and closes the connection.

I see you mentioned that I should make sure the server sends 200 back to OPTIONS request - do you mean the sendTypeMessage1 request? As far as I know that is not possible in my use case and returning 200 to the first request would make the NTLM authentication ineffective.

wbelhomsi commented 5 years ago

@KristineTrona Ok first there was an update to the POST request that i just merged so please update the package as for the OPTIONS request, it is also known as preflight request and it is usually sent before a request

There might be a need to add more Response headers (hopefully the backend is yours to edit) in my case the headers were:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *INSERT HEADERS THAT YOU NEED* + Content-Type, Access, Authorization (check the attached image below for my settings)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS (or the methods you want to allow)
Access-Control-Allow-Origin: <the origin you want to allow>
Access-Control-Expose-Headers: location, www-authenticate + any headers you want to expose

this is an example of using it, it worked well for me:

 authenticatedCall(config) {
    return new Promise((resolve, reject) => {
      const options: any = _.assign({
        username: 'temp',
        password: 'temp123',
        domain: 'wb',
        workstation: '',
        ntlm: { strict: false }
      }, config);

      httpntlm[options.method](options, function(err, res) {
        if (err) {
          reject(err);
        } else {
          if(res.status === 200) {
            resolve(res.data);
          } else {
            reject(err);
          }
        }
      });
    });
  }

And then i used it like that:

authenticatedCall({
   method: 'get',
   url: url
})
.then((ret: any) => {
   console.log('ret', ret);
})

image image

KristineTrona commented 5 years ago

@wbelhomsi thank you for your response. Is this working for you in a react-native/mobile app or just in a browser environment?

I have tried your implementation in my react native project, and I get the following error:

  status: 401,
  statusText: 'Unauthorized',
  headers: {
    'content-type': 'text/html',
    server: 'Microsoft-IIS/8.5',
    'www-authenticate': 'Negotiate, NTLM',
    'x-powered-by': 'ASP.NET',
    date: 'Tue, 30 Jul 2019 12:10:38 GMT',
    'content-length': '1293'
  },
  config: {
    url: 'myBackendEndpoint',
    method: 'get',
    headers: {
      Accept: 'application/json, text/plain, */*',
      Connection: 'keep-alive',
      Authorization: 'NTLM ' +
        'TlRMTVNTUAADAAAAGAAYAFgAAAAYABgAcAAAAAAAAABIAAAAEAAQAEgAAAAAAAAAWAAAAAAAAACIAAAABYKIogUBKAoAAAAPRQBBAFMAQQBkAG0AaQBuABZ0AZLZoKjJAAAAAAAAAAAAAAAAAAAAAC8SwjOh6p8qv+V9jSVU56qrkiEY9Pa7WQ==',
      'User-Agent': 'axios/0.19.0'
    },
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    withCredentials: true,
    adapter: [Function: httpAdapter],
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    validateStatus: [Function: validateStatus],
    maxRedirects: 0,
    data: undefined

I also tried to implement in manually by creating the sendType1Message and sendType3message functions and the library to generate the appropriate credentials:

const options = {
    url: 'myBackendEndpoint'
    username: 'myUserName',
    password: 'secretPassword',
    workstation: '',
    domain: '',
    ntlm: { strict: false },
    method: 'get'
  };

  function sendType1Message(callback) {
    const type1msg = ntlm.createType1Message(options);

    const type1options = {
      url: options.url,
      method: 'get',
      headers: {
        Connection: 'keep-alive',
        Authorization: type1msg
      },
      timeout: options.timeout || 0,
      maxRedirects: 0
    };

    axios
      .request(type1options)
      .then(res => {
        callback(null, res);
      })
      .catch(err => {
        if (err.response) {
          if (err.response.status === 401) {
            callback(null, err.response);
          } else {
            callback(err.response);
          }
        } else {
          callback(err);
        }
      });
  }

  function sendType3Message(res, callback) {

    if (!res.headers['www-authenticate']) {
      if (options.ntlm && options.ntlm.strict) {
        return callback(new Error('www-authenticate not found on response of second request'));
      }
      if (res.status === 401) {
        console.warn(
          'If this 401 response is unexpected, make sure your server sets "Access-Control-Expose-Headers" to "location, www-authenticate"'
        );
      }
      return callback(null, res);
    }

    // parse type2 message from server:
    const type2msg = ntlm.parseType2Message(res.headers['www-authenticate']); 

    if (!type2msg) return; // if callback returned an error, the parse-function returns with null

    // create type3 message:
    const type3msg = ntlm.createType3Message(type2msg, options);

    // build type3 request:
    const type3options = {
      url: options.url,
      method: 'get',
      headers: {
        Connection: 'keep-alive',
        Authorization: type3msg
      },
      maxRedirects: 0,
      withCredentials: true
    };

    axios
      .request(type3options)
      .then(response => {
        console.log('THERE IS RESPONSE', response);
        callback(null, response);
      })
      .catch(err => {
        console.log('THERE IS ERROR :(', err.response);
        if (err.response) {
          if (err.response.status === 401) {
            callback(err.response);
          } else {
            callback(err.response);
          }
        } else {
          callback(err);
        }
      });
  }

  return new Promise((resolve, reject) => {
    sendType1Message((err, res) => {
      if (err) {
        reject(err);
      }
      setImmediate(() => {
        sendType3Message(res, (error, data) => {
          resolve(data);
        });
      });
    });
  });

Both approaches return a 401 in the end.

Preflight request return 204, which is ok, The backend is not mine to edit unfortunately, but I know the requests work well in a node environment and in postman.

I checked the headers you mentioned:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *

These should not be important I think, since I am making request from react native and not from broswer, and therefore do not see any CORS issues:

Access-Control-Allow-Headers: Content-Type, Access, Authorization 
Access-Control-Allow-Methods: GET

These should also be working fine since a I was able to implement the original library of Sam Decrock in a node environment, where it worked without any issues:

Access-Control-Expose-Headers: location, www-authenticate 

About this - I know that www-authenticate is exposed, but I don't think the location is. Is the location header trivial for the library to work? As far as I understand it is only important for redirecting, but there shouldn't be any redirects in this authentication flow.

Is there something else I should be aware of for making this work in react native ? I feel like I have tried everything I can think of, but maybe the issue is just something very small.

maa105 commented 5 years ago

@KristineTrona We are using it in a cordova app so it is indeed a browser environment, If you are using react-native I think the CORS problem wouldn't be an issue but I'm not sure.

Can you please verify if the error is in the type 1 request or the type 3 request? like can u do console logs to track the code progression and where exactly this is happening?

Another thing I noticed the domain is left empty can you try putting a domain in and check. The location header is only used for redirection so I think it is not essential

KristineTrona commented 5 years ago

@maa105 The error above is returned from type3 request. The type 1 request also returns a 401 error, but this is expected and intercepted with axios, which sends error.response to sendType3Message.

The domain is not important for the sandbox backend environment. I get authenticated in node env or postman if I leave the domain equal to an empty string.

I am starting to think it has something to do with how react-native handles API requests. Perhaps the connection is not really kept alive, because of the 401 error after sendType1message?

maa105 commented 5 years ago

@KristineTrona I see that may be the case. Just a final thought can u create a small API with a post endpoint and ntlm authentication so that u can control it fully and try adding all the headers we mentioned and do logging on the server to check what is happening in details. (check express-ntlm for an expressjs middleware that does ntlm authentication).

KristineTrona commented 5 years ago

@maa105 not sure if I set it up correctly in the sense that I have no domaincontroller, so all authentication messages are valid, but with Postman I get the response after NTLM authentication and with React native I get this error:

Unexpected NTLM message Type 3 in newconnection for URI http://localhost:4000/

I am guessing this means the connection gets closed after the sendTypeMessage1?

SamDecrock commented 1 year ago

Don't forget that most browsers (not sure about Cordova and the like) have NTLM authentication built-in. So there's no need to use my module. Closing this ticket as this module is not aimed at ReactNative.