16

I write and host web applications on Windows servers for intranet usage. My server stack uses Sinatra (which uses Rack), Thin, and (in some cases) Apache for reverse-proxying only.

I want to support Single Sign-on (using NTLM or Kerberos) within our ActiveDirectory-backed domain. I have seen that I can use mod_ntlm or mod_auth_kerb when I'm behind Apache to perform my NTLM authentication. I haven't tried this yet, but I assume it will work.

My question is about NTLM or Kerberos authentication when I'm not behind Apache, using only Thin and Sinatra. I've seen rack-ntlm, but the usage details there are exceedingly sparse.

Please provide known-working code under Sinatra or Rack that shows how to use NTLM or Kerberos on the server-side, authenticating with ActiveDirectory (presumably via net-ldap).

Edit: Emphasized the desired answers, as no answers so far come close to providing the explicit help this question is asking for. Users should be able to find this answer and have a working solution, not pointers to external libraries that they must figure out how to use.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 1
    Maybe this fork shows a bit more of it´s usage: https://github.com/dtsato/rack-ntlm and this one: https://github.com/steelman/rack-ntlm Documenation is pretty sparse on that topic – scable Apr 16 '11 at 17:08
  • Why do you use NTLM and not Kerberos? http://msdn.microsoft.com/en-us/library/aa378749%28VS.85%29.aspx – free_easy Apr 16 '11 at 20:03
  • @free_easy Thank you for the pointer, I was not aware of Kerberos. If it provides the same capability (users logged into their desktop are automatically and securely identified when browsing the web application with no need to type their name or password) then I will happily accept Kerberos-based answers as well. – Phrogz Apr 17 '11 at 01:33
  • 2
    from the client point of view it doesn't make a big difference. Web sso via NTLM is handled by a protocol called SPNEGO (which is supported by all major browsers) and SPNEGO can handle both, NTLM and Kerberos. – free_easy Apr 17 '11 at 19:16

6 Answers6

9

I wrote a Rack::Auth module that implements NTLM SSO. It's maybe a little rough but it works for me. It does all that challenge/response stuff that's required for NTLM and sets REMOTE_USER to whatever the browser submitted.

Here's the code.

To make this work, the browser must be set up to send NTLM stuff to the server. In my environment this only happened when the server address was in the list of trusted domains. For Firefox, the domain has to be added to the list assigned to the key network.automatic-ntlm-auth.trusted-uris that can be accessed via about:config.

  • Wonderful! I humbly suggest that you add a README to your project with a bit of documentation on what it takes to use this/customize it. Or perhaps break out the parts that a user will need to customize as constant values/procs. It's not much code at all, but it would still be nice to have pointers as to what I'll likely need to change versus what I shouldn't need to. – Phrogz Apr 09 '12 at 12:53
  • Much of it is boilerplate to adhere to the gem format. I hope to add some instructions some time this week. –  Apr 09 '12 at 14:17
  • 2
    I've just added a README with some code that demonstrates how the module is to be used with a Rails application. –  Apr 13 '12 at 06:46
7

While I don't have any code to share and don't have an AD server to test against, I'll post some general information that others might find helpful when using rack-ntlm (which would be the best route at this point).

First thing to understand is that NTLM never actually gives you the user password. You don't NEED to authenticate the user inside your app. NTLM has already done that. What rack-ntlm will give you is a domain + user that you can then work with.

rack-ntlm does some additional work with that information that may or may not be valuable to you. You provide it with an AD server, port and a set of credentials. It will the take that user object (for lack of a better word) and look them up in AD via an LDAP call.

The credentials that rack-ntlm is asking for in settings would be YOUR credentials (or optimally, application-specific credentials in the domain that have limited query access). With that query, you would get back the details of that user from AD (group membership, email addresses, whatever). You can use that to further populate your database with user details.

One thing to note is that if you're using any browser OTHER than IE (and in some cases, even with IE), your users will get an HTTP authentication dialog. Depending on if your site is on the "intranet" or not, IE will passthrough the NTLM credentials automatically. This is controlled on per-browser basis so you may not have any control. In firefox, there's an "about:config" setting that will let you populate trusted sites.

So if we're going back to rack-ntlm, the flow would look something like this:

  • browser -> sinatra app
  • (handwaving challenge/response work
    here)
  • rack-ntlm now looks up user in AD via LDAP
  • sinatra app now has user details from LDAP in some hash
  • sinatra app creates a base user
  • store username (no password
    because you don't HAVE it) with some basic set of abilities in local datastore
  • sinatra sets cookie to "logged-in" or whatever

If you wanted, you could map AD groups to application roles in some capacity so that, say, domain admins automatically were added to your admin role.

lusis
  • 660
  • 4
  • 10
  • 2
    Rack::NTLM does not actually authenticate users. When you get an HTTP authentication dialog, you can enter any valid AD user name and a blank password, and it still indicates that is the logged in user. Reading the code, I suspect that you could also alter the user ID in the NTLM request and bypass security. – LeBleu Jan 17 '13 at 20:25
2

There are people responding to this question on authentication/security who are giving completely false and misleading information, which is potentially very dangerous. NTLM is a two-phase process. You've got the client-to-web-server negotiation, which gets details from the client such as the purported username and a token which is encrypted with a hash of the users password. A lot of people think as long as you can talk NTLM with the client, somehow authentication has happened. I have no idea why people make this assumption, maybe because the NTLM hand-shake process is relatively convoluted.

If you stop after the first phase and don't perform the actual authentication, you're trusting the client/user, in which case you may as well not do any authentication and just put a message saying "Please don't use this web application if you're not allowed to".

The second phase is the actual authentication. The web server sends the details provided by the client (the encrypted token) to the domain controller. The domain controller knows the hash of the users password used to encrypt the token, and so performs the same encryption of the password hash. If it matches the value of the client, then we know the client used the correct password hash. The web server never sees the hashed password, it only sees a token that was encrypted with the hashed password as the encryption key.

Unfortunately, there are not many LDAP libraries that support the NETLOGON capabilities required to actually authenticate an NTLM token, probably because it's non-trivial proprietary crap. Samba (well actually winbind) is one of only a small handful of libraries that can do it. There isn't currently a Ruby library capable of NTLM authentication, though there's plenty of libraries that'll get you the username reported by the client, though the client can report any username it likes.

As a rule of thumb, if you're NTLM library isn't asking for details of your domain controller, then there's no way it's doing any kind of authentication. A lot of the developers of these simple libraries themselves have no idea what they're doing.

TomWardrop
  • 557
  • 5
  • 12
  • Has the situation RE: Ruby libraries that can authenticate an NTLM token with a domain controller changed? – Ben Mar 09 '15 at 10:30
1

I use OmniAuth to do authentication off of an ActiveDirectory LDAP interface. Documentation is pretty good and it hooks easily into Rack.

Rob Di Marco
  • 43,054
  • 9
  • 66
  • 56
  • I appreciate this answer, but it does not appear to me to answer my question of NTLM or Kerberos integration, handling the handshakes and authenticating the user name. – Phrogz Apr 17 '11 at 19:27
1

I successfully used the Apache Kerberos module that you mentioned (http://modauthkerb.sourceforge.net/) It then presents the same API as would basic auth, while providing all the goodies of Kerberos. You'll just have to use a plain Rack::Auth::Basic, and that's it.

For plain Rack auth, you could probably use https://github.com/djberg96/rack-auth-kerberos, but I haven't personally tried it. The code looks straight forward, though.

Obviously in both cases you'll have to introduce your server to AD.

Roman
  • 13,100
  • 2
  • 47
  • 63
0

I have this working without the rack and NTLM solutions.

For authentication see my answer here: is there a way to read a clients windows login name using ruby on rails

Authorisation can then be done through the net-ldap gem by checking membership of security groups.

This runs only once when the server/service starts, only downside to this is you need to restart the service when the members in the group change. You could of course keep the authorised users in a database table.

Here my code.

In the Sinatra app

require 'net-ldap'

HOST     =   "XXXXXX"
PORT     =   389
LDAP = Net::LDAP.new(:host => HOST, :port => PORT)

# get account info somewhere safe
LDAP.auth(CONFIG.admin_user, CONFIG.admin_password)

if LDAP.bind
  log "ldap logged in"
else
  log "ldap login failed"
  abort
end

# CONFIG.permitted_users is the name of the apps security group
$members = get_members CONFIG.permitted_users

and in the helper file

def get_ldap_username cn
  treebase = "ou=xxxxxx,ou=xxxxxx,ou=xxxxxxx,ou=xxxxxx,dc=xxx,dc=xx"
  filter = Net::LDAP::Filter.eq("cn", cn)
  LDAP.search(:filter => filter, :base => treebase) do |item| 
    return item.sAMAccountName.first
  end
end

def get_members name, members = []
  treebase = "ou=xxxxxxx,ou=xxxxxxx,ou=xxxxxxx,ou=xxxxxx,dc=xxx,dc=xx"
  filter = Net::LDAP::Filter.eq("cn", name)
  LDAP.search(:filter => filter, :base => treebase) do |item| 
    item.each do |attribute, values|
      if attribute == :member
        values.each do |value|
          cn = value[/CN=([^,]+),/,1]

          # my groups all begin with a letter/number sequence
          # recurse this method if member is a group itself
          if cn[0..2].downcase == "xxx" # xxx something else of course
            get_members cn, members
          else
            members << get_ldap_username(cn)
          end

        end
      end
     end
  end
  members # an array of permitted usernames
end

before do
  # authentication code 
  # see https://stackoverflow.com/questions/5506932/is-there-a-way-to-read-a-clients-windows-login-name-using-ruby-on-rails/48407500#48407500

  # authorisation
  unless $members.include? @username
    halt "No access"
  end
end
peter
  • 41,770
  • 5
  • 64
  • 108