Ruby on Rails and NTLM Authentication
I had an interesting task at work. I needed to integrate NTLM Authentication into a Ruby on Rails application. Now on the face of it this sounded like a nightmare. Immediately thoughts of running Rails on IIS go through my mind. Bleh. Well, it turns out not to be that bad. But pretty damn close.
But add to the circumstance that this Rails app won’t run on Windows. It will run on RedHat Enterprise Linux through IBM WebSphere. Now the task becomes very interesting. Now this is a JRuby on Rails application and there won’t be a Windows box in sight. You have 16hrs … go!
So doing research I found out just what exactly NTLM is and how it works. It’s not magic at all really. It’s a simple handshake between server and browser using some HTTP headers to send back and forth.
Here are the steps. This is a very brief overview with no details. I will follow this with some code to provide a rough example. In the following steps “client” means your browser and “server” means the Rails application (specifically a before_filter in application controller).
- Client requests home page. GET / HTTP/1.1
- Server receives request and inspects headers for “Authorization” key.
- Server finds no request header with the key “Authorization” and builds a response with the header “WWW-Authenticate” with a value of “NTLM”. It returns this with a status of 401.
- Client receives this response and because of the 401 status and the “WWW-Authenticate” header value of “NTLM” it prepares a “Authorization” header value of “NTLM XXXXXXXXXXX” where “XXXXXXXXX” is a Base 64 encoded bit structure of information for the server. It then requests the home page again with this added header. This is the Type 1 message.
- Server receives this second request and finds the “Authorization” header and parses it’s value. It then prepares a “WWW-Authenticate” header with a value “NTLM XXXXXXX” where “XXXXXX” is a Base 64 encoded challenge. It returns this to the client with a 401 status. This is the Type 2 message.
- Client receives this response, recognizing the 401 and the NTLM challenge and it prompts the user with a login dialog. NOTE: Internet Explorer and properly configured FireFox browsers will not prompt the user and simply pass along the information due to NTLM integration.
- Client requests home page again, now with the “Authorization” header set with a “NTLM XXXXXXXXXXXX” value where “XXXXXXXX” is a Base 64 encoded bit structure populated with information from user dialog.
- Server receives this third request, recognizes the challenge response, parses the bit structure and finds the username, password, remote host and domain information of the user. This is the Type 3 message.
- With all of this information the Server can then authentication and authorize the user to the resource originally requested.
This all happens behind the scenes invisible to the user. All 9 steps look like 1 web request to the user. This is because the browser and server utilize Keep-Alive statuses of the Connection. Also recognize that in step 8 & 9 the user is NOT AUTHENTICATED. I repeat – this is not a complete authentication example, just an overview of the steps involved.
Notice however that in all the steps there is nothing that the Rails application can’t do itself. Meaning there isn’t a need for the server to do anything to the response/request cycle. Rails can manage all of this, notwithstanding the actual authenticating to a Windows domain. Here is some psuedo code to demonstrate.
class ApplicationController < ActionController::Base
before_filter :authenticate
def authenticate
if request.headers["Authorization"].blank?
headers["WWW-Authenticate"] = "NTLM"
render :inline => "NTLM Auth !!", :status => 401, :layout => false
else
# grab the request.headers["Authorization"] value
# and parse it. maybe create a class to do this and expose
# the message type property of the value
ntlm = NtlmAuthentication.new(request.headers["Authorization"])
if ntlm.message_type == 1
headers["WWW-Authenticate"] = message # message is the base 64 encoded challenge
render :inline => "NTLM Auth Challenge !!", :status => 401, :layout => false
elsif ntlm.message_type == 3
# now within the ntlm message you have the user/domain/remotehost/password
# with which you can authenticate/authorize ntlm.user_name, ntlm.password
end
end
end
end
There are a ton of things missing from the example above but it hits the major steps. In particular the actual parsing of the Base 64 header value is not demonstrated. It is a bit structure that is quite gnarly (at least to me). I ended up using Java to encode/decode it and it worked out pretty nicely. This parsing step could be a post all by itself. And I think it will be!
My solution worked. But after implementing it to WebSphere there was a funny behavior. If IHS was used in front of the app server, IE would fail to finish the NTLM handshake. If IHS was not used and the request goes straight to the app server – all is well. Still debugging this one.