ReqNtlm (req_ntlm v0.1.0)

Copy Markdown View Source

NTLM ("NTLMSSP") is a challenge/response authentication protocol used by Windows-based servers (IIS, Exchange, internal corporate services, ...). Unlike Basic or Bearer authentication, it is a connection-oriented three-way handshake:

  1. the client sends a NEGOTIATE_MESSAGE,
  2. the server replies 401 with a CHALLENGE_MESSAGE,
  3. the client resends the request with an AUTHENTICATE_MESSAGE computed from the challenge and the user's credentials (NTLMv2).

ReqNtlm implements this purely as a request step (sets the authorization header for the current leg) and a response step (reacts to a 401 NTLM challenge by resending the request). It does not ship its own HTTP adapter — whichever adapter the request already uses (Req.Finch by default, or anything else configured via Req.new(adapter: ...)) keeps being used.

This means NTLM's requirement that all legs of the handshake share the same TCP connection is only as good as that adapter's connection reuse for two requests issued back-to-back to the same host. For Req.Finch, that is generally the case for sequential (non-concurrent) requests against the same {scheme, host, port}, but it's not a hard guarantee. If you need stronger guarantees (e.g. under concurrent load), configure the adapter's connection pool accordingly for the affected host (see Req.Finch options).

Usage

Mix.install([
  {:req, "~> 0.5"},
  {:req_ntlm, "~> 0.1.0"}
])

req = Req.new() |> ReqNtlm.attach()

Req.get!(req,
  url: "http://intranet.example.com/private",
  ntlm: [username: "bob", password: "secret", domain: "CORP"]
)

The username may also be given in "DOMAIN\user" form, in which case :domain can be omitted:

Req.get!(req,
  url: "http://intranet.example.com/private",
  ntlm: [username: "CORP\bob", password: "secret"]
)

A {username, password} tuple is accepted as a shortcut too:

Req.get!(req, url: "...", ntlm: {"CORP\bob", "secret"})

Request Options

  • :ntlm - credentials used to perform the NTLM handshake. One of:

    • a {username, password} tuple,
    • a keyword list or map with :username, :password, and optionally :domain and :workstation (defaults to "").

    When this option is not set, ReqNtlm does not interfere with the request at all.

Limitations

  • Only NTLMv2 is implemented (there is no NTLMv1 downgrade).
  • As explained above, the handshake relies on the configured adapter reusing the same connection across the two requests it takes; it is not pinned to a single socket by ReqNtlm itself.
  • Message confidentiality/integrity (NTLM signing/sealing) and session key exchange are not implemented — only the authentication handshake itself, which is what's needed to get past a 401 challenge.

Summary

Functions

Attaches the plugin.

Reacts to a 401 NTLM challenge by resending the request.

Sets the authorization header for the current leg of the handshake.

Functions

attach(request, options \\ [])

Attaches the plugin.

See the module documentation for usage.

handle_ntlm_challenge(arg)

Reacts to a 401 NTLM challenge by resending the request.

This response step is automatically added by attach/2. It only acts once per request (to avoid loops): if the :ntlm option is set, no challenge has been recorded yet, and the response is a 401 carrying an NTLM <challenge> www-authenticate header, it records the challenge and reruns the request — which will then send the AUTHENTICATE_MESSAGE via put_ntlm_header/1 above. Any other response is returned unchanged.

put_ntlm_header(request)

Sets the authorization header for the current leg of the handshake.

This request step is automatically added by attach/2. When the :ntlm option is set, it sends a NEGOTIATE_MESSAGE unless a challenge has already been recorded by handle_ntlm_challenge/1, in which case it sends the AUTHENTICATE_MESSAGE computed from that challenge.