Simple lightweight NTLM in PHP

Many months ago I made a PHP script that could read NTLM authentication information from your browser. What’s NTLM? Basically, if you’re using Microsoft Windows, your browser can automatically send your windows login information to a website (if you agree to it). This means that without needing to enter additional username or passwords, you can be authenticated at the website you’re visiting. This is quite convenient especially for company intranets. NTLM should work with all major browsers (Internet Explorer, Firefox and Opera).

The PHP code I wrote is simple and can be inserted into the top of any PHP script. The key output is $user $domain $workstation, which is the information advertised by the user. Be warned though, the script does NOT authenticate the user and merely assumes that the user is who they say they are. This is akin to a user entering only a username with no password required. I plan to add password/hash verification possibly in conjuction with samba in the future.

A limitation is that the PHP script relies on apache_request_headers() which is only available if you run PHP as a apache module. (Update 2010, newer code doesn’t have this issue)

[php]
<?php

// loune 25/3/2006, updated 22/08/2009
// For more information see:
// http://siphon9.net/loune/2007/10/simple-lightweight-ntlm-in-php/
//
// This script is obsolete, you should see
// http://siphon9.net/loune/2009/09/ntlm-authentication-in-php-now-with-ntlmv2-hash-checking/
//

// NTLM specs http://davenport.sourceforge.net/ntlm.html

$headers = apache_request_headers();

if (!isset($headers[‘Authorization’])){
header(‘HTTP/1.1 401 Unauthorized’);
header(‘WWW-Authenticate: NTLM’);
exit;
}

$auth = $headers[‘Authorization’];

if (substr($auth,0,5) == ‘NTLM ‘) {
$msg = base64_decode(substr($auth, 5));
if (substr($msg, 0, 8) != "NTLMSSP\x00")
die(‘error header not recognised’);

if ($msg[8] == "\x01") {
$msg2 = "NTLMSSP\x00\x02\x00\x00\x00".
"\x00\x00\x00\x00". // target name len/alloc
"\x00\x00\x00\x00". // target name offset
"\x01\x02\x81\x00". // flags
"\x00\x00\x00\x00\x00\x00\x00\x00". // challenge
"\x00\x00\x00\x00\x00\x00\x00\x00". // context
"\x00\x00\x00\x00\x00\x00\x00\x00"; // target info len/alloc/offset

header(‘HTTP/1.1 401 Unauthorized’);
header(‘WWW-Authenticate: NTLM ‘.trim(base64_encode($msg2)));
exit;
}
else if ($msg[8] == "\x03") {
function get_msg_str($msg, $start, $unicode = true) {
$len = (ord($msg[$start+1]) * 256) + ord($msg[$start]);
$off = (ord($msg[$start+5]) * 256) + ord($msg[$start+4]);
if ($unicode)
return str_replace("\0", ”, substr($msg, $off, $len));
else
return substr($msg, $off, $len);
}
$user = get_msg_str($msg, 36);
$domain = get_msg_str($msg, 28);
$workstation = get_msg_str($msg, 44);

print "You are $user from $domain/$workstation";
}
}

?>
[/php]

If you try the script in Firefox (on windows), you will notice that you get prompted for a username and password when encountering an NTLM challenge. This is because sending your windows credentials to any unscrupulous website poses a real security risk. To make it automatically use your windows credentials for sites you trust, you can add the website to a whitelist.

The whitelist is located at Firefox’s about:config (type that into the address bar), which allows the editing of all of the browser’s preferences. Find the preference entry network.automatic-ntlm-auth.trusted-uris, double click on it and type the hostname of the site (ie http://www.abc.com) that you want in your whitelist. Multiple entries are seperated by commas. After doing that, Firefox should send your windows creds automatically.

Update 20/09/2009. The above script is outdated, anyone wishing to use NTLM should see the new post: Part 2 – Now with hash checking

63 thoughts on “Simple lightweight NTLM in PHP

  1. Pingback: simple plan » Simple lightweight NTLM in PHP

  2. Kruno

    is this working on IE6 and IE7? When looking tcp dump i see IE abandoning request just after sending x03 message, and before receiving 200 OK response.
    In firefox it is working fine. any taughts?

  3. Loune Post author

    Andrew – the point of NTLM is that you don’t need to enter the username/password. It reads it from the current user on windows. Otherwise, you can possibly send a 401 HTTP status code to clear the authenticated session.

    Kruno – It works fine for IE6 for me. Have you checked that the site you’re trying it with is trusted or on local intranet zone? Also I’ve heard that the whole transaction needs to be on one connection so another thing to check is to see if persistent connections are enabled.

  4. Matt

    This is great and works fine – I can take the username and cross reference users in a MySQL database to determine what to display in my php form, but when I submit the form, it doesn’t appear to be passing the elements in the REQUEST headers. Is that a function of the NTLM piece? It works fine without it included.

  5. David

    I wanted to use this for our Intranet, not for authentication but to personalise the site for our users. So the hash check would sorta be over kill. This lightweight version works perfect… and here’s the but…

    BUT, all forms on the intranet that are set to method=”post” stop working and the $_POST array is totally empty. This is not the case for form set as method=”get”.

    As soon as I disable the ntlm authorisation all the forms are working again. Odd thing is that some of these forms are located in pages within a different directory even i.e. ntlm auth happens on index.php and subsequently a form located in /admin/docmanager.php stops working.

    I am at wits end on this one and have scoured Google from end to end for a solution…. Any thoughts?

    P.S. This even happens on a fresh install of LAMP.

  6. Loune Post author

    This is probably due to the 3-way handshake (negotiate, challenge, response) the ntlm script performs. A post would be eaten up by the negotiate step. Ideally you should only ntlm authenticate the first time in a browser session and after that, you set a cookie and don’t ntlm authenticate until the next session ie:

    $user = $_COOKIE[‘username’];
    if (empty($user)) {
    // the ntlm.php stuff
    // straight after print “You are $user…, add:
    setcookie(‘username’, $user);
    }

    This would work provided that the user has cookies enabled and that your first request in that session isn’t a post.

  7. David

    Nope, still not working.

    Well…. it *sorta* works. Once the cookie has been set and you close and reopen the browser then the post forms work. I am guessing this is due to the authentication being skipped as $username is not empty.

    Really, REALLY baffling! —- Ohhh look! A grey hair… YAY!

  8. Loune Post author

    I’ve just quickly mocked up an example and it works fine. There was a mistake in my original snippet in that $username variable should really just be named $user as per my ntlm code. I’ve fixed that now.

    My testing was in firefox 3.0 and IE8. Could it be some quirk or setting with your browser regarding cookies? I can post my example up if you want.

  9. David

    Yeah that’s be nice. I wasn’t using a cookie until suggested it (there wasn’t much of interest that I could put in the cookie before this authentication snippet) so I doubt it is a problem with a cookie.

    Strange thing is it *does* work for Firefox, not a single problem. The problems occur mainly in IE7 & IE8 (though occasionally IE8 works but without any changes, deletions or anything stops working).

    Now as much as I would LOVE to roll out FF3 within the company the directors would have an absolute heart attack as it can’t be simply managed from a central location…. Hey hang on, now there is an opportunity for a KICKASS addon and an server app to manage it, might boost adoption, but I digress…

  10. Ira

    David:

    I had the same problem with it causing the post data to be empty, and I spent all day trying to figure out a solution. We solved the problem by simply adding that same code(posted by Loune) into every php file that is doing the “$variable = $_POST[‘name’].”

  11. AEvans

    I understand this is an old post but this script is exactly what I was looking for as it is lightweight and I can call it very easily.

    However I was getting the same problem as above, after calling the script and storing the username in $_SESSION any script that used $_POST didnt work as $_POST was empty. Does this mean I need to call the script EVERY time I want to use $_POST?

  12. Dave

    I notice that line 25 has a source reference to an image (looks inadvertent.) Whats the lenth of the string you are trying to compare?

    Also I am using this code instead of the password hash check since I just want a simple piece of code to identify who is logged into the computer. However in IE8 it looks like its prompting me for a username and password, especially when not on a domain. Is there any setting changes I need to do in IE8? Thanks!

  13. Loune Post author

    @Save Thanks for pointing out the error. WordPress was trying to make a smiley. It should now be fixed. Regarding the IE8 problem, I think in IE8, you need to add your PHP website as a trusted site. You can do this be going to Internet Options > Security tab, click on the Trusted sites icon, then the sites button. In the popup, add your website domain.

  14. Chris

    Hi Loune

    I’m trying this script on an intranet and keep getting the NT Challenge prompt when using a ‘protected’ directory and no user information when using an unprotected directory.

    I am using the latest install of WAMP and have mod_auth_sspi installed working on a single directory – the protected directory.

    Any assistance greatefully received!

  15. Loune Post author

    @Chris, what do you mean by protected directory? And which browser are you using? This script is for finding out the username of the current logged on windows user. Is that what you want?

  16. Chris

    Hi Loune.

    Many thanks for your response.

    I am using your script in two places one of them is in a directory which has the mod_auth_sspi pointed at it (.\protected\whoami.php) and the other is in the root.

    I am using IE6

    When accessing the script via IE6 in the root http://localhost/whoami.php i get this error ( Undefined index: REMOTE_USER in C:\wamp\www\whoami.php on line 8
    ) and when I access the script http://localhost/protected/whoami.php I get my user name, domain name and machine name as expected.

    How ever when I access the script http://10.99.99.99/whoami.php i still get this error ( Undefined index: REMOTE_USER in C:\wamp\www\whoami.php on line 8
    ) and when I access the script http://10.99.99.99/protected/whoami.php I get an NT Challenge prompt, which, if I fill in the user name part only (with say, the letter ‘h’) gives the user name as ‘h’ and the correct domain name etc.

    What I wish to do is extract the windows user name, domain etc (with out the user having to fill in an NT Challenge prompt).

    Is this some thing to do with the way the Apache web sever is set up as I have done this type of thing numerous times using windows IIS-5 with no NT Challange prompt.

    Cheers.

    Chris

  17. Loune Post author

    @Chris unfortunately I’ve had no experience with mod_auth_sspi. The script I made above is for sites which just want to get NTLM authentication information with PHP. If you’re using mod_auth_sspi, then the above script is not neccessary. From what you’ve written, it sounds like a configuration issue with mod_auth_sspi. It’s probably best to post at the mod_auth_sspi mailing list and see if the devs there can help you.

  18. Danny

    This is a fantastic script, works like a treat but I’m experiencing the same issues as others. I’ve authenticated the user on intranet home page load and then stored what I need in a session. However as others have reported it stops forms from processing.

    I have checked the post array and it’s empty when using this. Is there any answer of how to fix this?

  19. Nuke

    Hi guys, after wasting half a day debugging my code,; the issue seems to be with ie. Ntlm authentication is required on every page that is posting data across. Which means that if one page has been authenticated using this script all subsequent pages need to run this script in order post any information across; which is why all posts on ie are returning empty and if you try the same on firefox would work fine.

    Haven’t found a solution yet, but will not POST on ie unless all pages have this script;

    Let me know if anyones found a soulution for this.

  20. ThreeM

    There are a few of you with the issue of needing to send this data via a form or other post methods. To do this with other applications I have always found it easiest to either use the PHP that generates the page to pass up the variables – $user $domain $workstation in this case – as either JavaScript variables when it writes the script or if all the scripts are includes then hiding it in a hidden form field in the html. If you need it for multiple forms/scripts you can also just enclose it in a echo “$user”; then to retrieve it just use a getElementById(‘#data_user’).innerHTML to return what is in the div element and pass it with the rest of the data in your AJAX (JavaScript w/o reload) call, POST (header), or GET (URL) request.

  21. Hassan

    Hi,
    When I use this code for an intranet site, it works for most of the cases (auto retrieves the username), but for some user (possibly due to version of IE & OS) it asks for a username and pass.

    Is there any way to check when this code asks for the username & pass. I tried this,

    if(strlen(get_msg_str($msg, 36))==0)

    but of no work.

    Please help, my life is gonna hell.

  22. patrick van lier

    Hi, I’m trying to make it work on PHP on IIS on an 2003 server. When I use IE9 on win7 it gives backup a status 401 (checked responseheaders in developertools and it gives a “www-authenticate NTML blablalba” response so it seems like IE doesn’t do it’s trick) and just stops. Site is in intranet zone.

    IE: 9
    OS: win 7 64bit
    IIS: 6.0 on server 2003 (anonymous auth enabled)
    domain: intranet..local
    site: backupstatus.server.intranet..local

    When I use firefox it prompts for credentials and when I fill in correct ones it displays username and password.

  23. Theis

    To all who has the problem with the empty $_POST variable in the Internet Explorer (IE), you need to disable the NTLMPreAuth (DisableNTLMPreAuth) in the registry on each clientcomputer.


    [HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings]
    “DisableNTLMPreAuth”=dword:00000001

    See http://support.microsoft.com/kb/251404 or google ;-)

  24. funky

    hey,

    thank you so much for this script. i’m currently trying to understand exactly what it is doing, but a quick test looks very promising!

  25. Pingback: php, retrieve the password of the current user : Unlimitedtricks Programming Blog, Tutorials, jQuery, Ajax, PHP, MySQL and Demos

  26. Pingback: php, retrieve the password of the current user closed | Code and Programming

  27. Guille Filipich

    My two cents. Got it ported to Java.

    I used this from an app inside the DMZ to identify (not authenticate) users from within the corporate network. Our app in the DMZ cannot see the Active Directory.

    User has to login again in the app, but he is forced to use the same username as the windows one.

    now the code:

    package servlets;

    import java.io.IOException;
    import java.util.Enumeration;

    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;

    import org.apache.commons.codec.binary.Base64;

    public class SsoTest extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet {

    private static final long serialVersionUID = 1L;

    public void doGet(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {

    //System.out.println(“\n\n————————————- START REQUEST ——————————-“);

    Enumeration names = req.getHeaderNames();

    boolean authHeaderSet = false;
    String authString = “”;

    while (names.hasMoreElements()) {
    String name = (String) names.nextElement();
    Enumeration values = req.getHeaders(name); // support multiple values
    if (values != null) {
    while (values.hasMoreElements()) {
    String value = (String) values.nextElement();
    if (name.toLowerCase().equals(“authorization”)) {
    authHeaderSet = true;
    authString = value;
    }
    }
    }
    }

    if (!authHeaderSet) {
    // Not authorized
    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    res.setHeader(“WWW-Authenticate”, “NTLM”);

    } else {
    byte[] msg = Base64.decodeBase64(authString.substring(5));
    if (msg[8] == 0x01) {
    // Send challenge
    byte[] msg2 = { 0x4E, 0x54, 0x4C, 0x4D, 0x53, 0x53, 0x50, // NTLMSSP
    0x00, 0x02, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, // target name len/alloc
    0x00, 0x00, 0x00, 0x00, // target name offset
    0x01, 0x02, (byte) 0x81, 0x00, // flags
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // challenge
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // context
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // target
    // info
    // len/alloc/offset

    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    res.setHeader(“WWW-Authenticate”, “NTLM ” + (Base64.encodeBase64String(msg2)).trim());

    } else if (msg[8] == 0x03) {
    // Challenge accepted
    setVars(msg);

    } else {
    // Not authorized
    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

    }
    }
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doGet(request, response);
    }

    private void setVars(byte[] msg) {
    String user = getMsgString(msg, 36);
    System.out.println(“USER:”+user);

    String domain = getMsgString(msg, 28);
    System.out.println(“domain:”+domain);

    String workstation = getMsgString(msg, 44);
    System.out.println(“workstation:”+workstation);

    }

    private String getMsgString(byte [] msg, int start) {
    boolean unicode = true;
    int len = ((int)msg[start+1] * 256 ) + (int)msg[start];
    int off = ((int)msg[start+5] * 256 ) + (int)msg[start+4];

    String value = new String (msg);
    value = value.substring(off,off+len);
    if (unicode) {
    value = value.replace((char)0x00, (char)0x20);
    value = value.replaceAll(” “, “”);
    }
    return value;
    }

    }

  28. struppi

    If you are encountering problems with Internet Explorer not working but Firefox ist working check the HTTPD “KeepAlive” Setting which has to be “On”. This is default but on our CentOS it was Off.

  29. Pingback: Symfony2: automatically logging in users from their Windows session | BlogoSfera

  30. Pingback: How do I use Microsoft AD and php single sign on web app? | Ask Programming & Technology

  31. Mahesh

    this code working for me in windows wamp server. but when i uploaded the code into Linux server it gives authentication pop up box. i want to get windows username only with out asking authentication in Linux..do you guys have any idea?

  32. Pingback: Can a PHP intranet share Windows logins? | Yray Answers

  33. Iulia

    The script works great except one problem: if I don’t configure Mozilla, a window is prompted asking for the user credentials. How can I avoid the window? If the browser is not configured, I want the user to be redirected to a page where he can authenticate manually, and not by using te pop-up window.

    Can you help me? Thank you in advance.

  34. Iulia

    Another problem: if I type in the prompted window the wrong credentials, it still authenticates the user. How can I fix this?

Leave a Reply

Your email address will not be published. Required fields are marked *