Help with NTLM Authentication and POST commands in 8.3

I need to make API calls to a system that will only allow authentication via NTLM in Ignition 8.3.
After being authenticated, I need to POST to two APIs as an authenticated user.
I’m writing a Project Library script that will be called via a Gateway Tag Change event.
The tag change will have access to parameter values that need to be sent in the POSTs.

I haven’t figured out how to set the headers or the body/params yet.

Here’s what I have so far:


""" 
This will call three APIs to step through the process of printing a Material Tag.
The material must have been previously accepted and must not be expired.
    1. login so the server can trust Search and Print requests
    2. query to get acceptance details
    3. update the JSON record with process info from the PLC and Ignition
    4. send the updated JSON to create quantity details and print a tag
    Note: Password changes are maintained in KeePass2 shared file
"""

# import libraries recommended by Ryan McLaughlin  

from org.apache.http.auth import AuthScope
from org.apache.http.auth import NTCredentials
from org.apache.http.client.methods import HttpGet
from org.apache.http.client.methods import HttpPost        # added because POST is required
from org.apache.http.impl.client import DefaultHttpClient
from org.apache.http.impl.client import BasicResponseHandler
from org.apache.http.entity import ContentType
from java.io import ByteArrayOutputStream

# initialize variables from tag values
tagPaths = [
    '[NG]Costpoint/LoginUser',
    '[NG]Costpoint/Search',
    '[NG]Costpoint/Print',
    '[NG]Costpoint/Database',
    '[NG]Costpoint/Domain',
    '[NG]Costpoint/Username',
    '[NG]Costpoint/Password'
]
tagValues = system.tag.readBlocking(tagPaths)

loginUrl = tagValues[0].value
searchUrl = tagValues[1].value
printUrl = tagValues[2].value
database = tagValues[3].value
domain = tagValues[4].value
username = tagValues[5].value
password = tagValues[6].value


def loginUser(moNumber, quantity, printer, labelCount):
    # Setup the client with NTLM Auth
    httpclient = DefaultHttpClient()
    
    # Define the credentials to use
    creds = NTCredentials(username, password, system.net.getHostName(), domain)
    print('creds={}'.format(creds))
    
    httpclient.getCredentialsProvider().setCredentials(AuthScope.ANY, creds)
    print('httpclient={}'.format(httpclient))
    
    # Define the host and URL to call
    httppost = HttpPost(loginUrl)
    print('httppost={}'.format(httppost))
    
    # Add headers
    header = {'Accept':'application/json'}
    try:
        httppost.addHeader(header)
    except:
        print('error trying to addHeader')
    
    # Execute the request
    response = httpclient.execute(httppost)
    print('response={}'.format(response))
    
    # Get response entity and MIME type
    entity = response.getEntity()
    print('entity={}'.format(entity))
    
    # Process the content
    contentType = ContentType.getOrDefault(response.getEntity()).getMimeType().lower()
    print('contentType={}'.format(contentType))
    
#    if 'json' in contentType:
#        return system.util.jsonDecode(BasicResponseHandler().handleResponse(response))
#    elif contentType in ["image/png", "image/jpeg"]:
#        baos = ByteArrayOutputStream()
#        entity.writeTo(baos)
#        return baos.toByteArray(), contentType
#    else:
#        return BasicResponseHandler().handleResponse(response), contentType
    
    httppost.releaseConnection()
    
    # step 2 search for the material tag
    httppost = HttpPost(searchUrl)
        
    searchParams = {'findBy':'MO','findText':moNumber,'findSerLot':'','database':database}
    try:
        httppost.setParams(searchParams)
    except:
        print('error trying to setParams')
    
    jsonResponse = httpclient.execute(httppost)
    print('jsonResponse={}'.format(jsonResponse))
    # step 3 update elements of the search results and print a material tag
    #jsonResponse['searchResults'][0]['quantity'] = quantity
    #jsonResponse['searchResults'][0]['labelCount'] = labelCount
    
    ###
    ###
    
    printPayload={'printer':printer,'userId':username,'userDisplayName':'Ignition','database':database,'labelRecords':jsonResponse}

This is on a parent project just printing via this script in the script console

moNumber = '33PM097734'
quantity = 19.4
printer = 'PRN060050'
labelCount = 1

AcceptTag.tagTest.loginUser(moNumber, quantity, printer, labelCount)

Printed output is:

creds=[principal: myDomain\myUsername][workstation: myWorkstation]
httpclient=org.apache.http.impl.client.DefaultHttpClient@4b841149
httppost=POST http://myURL/LoginUser HTTP/1.1
error trying to addHeader
response=HTTP/1.1 415 Unsupported Media Type [Transfer-Encoding: chunked, Server: Microsoft-IIS/10.0, Persistent-Auth: true, X-Powered-By: ASP.NET, Date: Wed, 26 Nov 2025 20:31:34 GMT] org.apache.http.conn.BasicManagedEntity@70e60d8c
entity=org.apache.http.conn.BasicManagedEntity@70e60d8c
contentType=text/plain
error trying to setParams
jsonResponse=HTTP/1.1 415 Unsupported Media Type [Transfer-Encoding: chunked, Server: Microsoft-IIS/10.0, X-Powered-By: ASP.NET, Date: Wed, 26 Nov 2025 20:31:34 GMT] org.apache.http.conn.BasicManagedEntity@59e15099

See AbstractHttpMessage (Apache HttpCore 4.4.13 API)

It doesn't accept a dictionary, it accepts either 2 string parameters or a Header object.

Be aware that gateway events will use the account that is running the service--you may need to change the service to run under a user with appropriate privileges.

The service account running the gateway is registered with the API service.
I have a working Python version that runs start-to-finish written in Visual Studio Code so I know the credentials are good.

The VSC version uses the from requests_ntlm import HttpNtlmAuth library.

Thanks @Kevin.Herron, the header problem is fixed.
Still get HTTP/1.1 415 Unsupported Media Type

Consider running wireshark while testing with the gateway and then testing with CPython. Compare.

Our IT department said wireshark is not justified for this issue.

The DEV team that wrote the APIs I’m trying to call said that NTLM is the only authentication method they will support and,

I think I’ll have to ask the customer (internal) to drop this requirement :weary_face:

As noted in other topics, this is supported in the Apache HTTP client jars that come with Ignition. Like this one:

Your code is missing something or doing something wrong and it isn't clear based on what you've shared.

Your IT department is simply wrong. Use a packet capture to compare to working requests so you can see the differences. I would suspect some aspect of how your gateway service account is set up, but the possibilities are endless. (Are you running your Python3 test code in a service with the same account as Ignition? Desktop and service environments are different.)

2 Likes

I have not tried running native Python on the server, only from my workstation so that is probably why VSCode Python succeeds vs. the service account that is running Ignition.

I agree; wireshark would be helpful and the IT dept. knows it would help but the project is too low of a priority for them / not worth their time so they won’t allow installing wireshark on the server.

I have another version of code based on what I found here:
Webservices NTLM Auth - General Discussion - Inductive Automation Forum

This version returns a PKIX certificate error.
The needed certificates are in the …\Certificates\Supplemental folder, downloaded from the web site hosting the API and confirmed by their developers.

def useApacheLibs(username, password):
    from org.apache.http.auth import AuthScope
    from org.apache.http.auth import NTCredentials
    from org.apache.http.client.methods import HttpGet
    from org.apache.http.client.methods import HttpPost
    from org.apache.http.impl.client import DefaultHttpClient
    from org.apache.http.impl.client import BasicResponseHandler
    from org.apache.http.entity import ContentType
    from java.io import ByteArrayOutputStream
    
    # Setup the client with NTLM Auth
    httpclient = DefaultHttpClient()
    print 'httpclient={}'.format(httpclient)
    # Define the credentials to use
    creds = NTCredentials(username, password, system.net.getHostName(), "myDomain")
    httpclient.getCredentialsProvider().setCredentials(AuthScope.ANY, creds)
    print 'creds={}'.format(creds)
    print 'httpclient={}'.format(httpclient)
    # Define the host and URL to call
    getAppInfoUrl = '{}/GetAppInfo'.format(baseUrl)
    print 'getAppInfoUrl={}'.format(getAppInfoUrl)
    
    httpget = HttpGet(getAppInfoUrl)
    print 'httpget={}'.format(httpget)
    
    # Execute the request
    response = httpclient.execute(httpget)
    print 'response={}'.format(response)
    return response

Printed info looks good up to the execute statement where it blows up with PKIX error.

Apache's HTTP client is not aware of our supplemental certificates pattern and so is building a chain of trust that doesn't include your added certs.

You'll have to figure out how to inject the certs from the stored file location into the Apache HTTP client instance.

Huh, that's a little surprising, because our supplement cert pattern is nothing special - we just import those certs into the JDK's root keystore.

Maybe if you use org.apache.http.impl.client.HttpClientBuilder and the setSSLContext method to recycle the default from the system (javax.net.ssl.SSLContext#getDefault) it'll "just work"?