Last week, I wrote a post listing 9 ways to use rails metal. This is an explanation of the first way to use Rails Metal: Check Authentication.
We’re setting up this Rails Metal to handle two scenarios: requiring authentication, and logging the user in. First, it verifies that requests to any path beginning with /admin have a user logged in by checking that a valid user id is stored in the session. Second, accepts an HTTP POST to /authenticate with a username, password, and target path for redirect upon
Before we get too deep into things, let me share the code. You can check out a working version of the application on GitHub.
Here’s the Metal.
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)
class Authentication
def self.call(env)
session = env["rack.session"]
request = Rack::Request.new(env)
params = req.params
if env["PATH_INFO"] =~ /^\/admin.*/ && (session["user_id"].nil? || (session["user_id"] && User.connection.select_all("SELECT * FROM users WHERE id = #{session["user_id"]}").empty?))
[302, {"Content-Type" => "text/html", "Location" => "/login?target_path=#{env["REQUEST_URI"]}"}, ["You must be logged in to view this page. You are being redirected."]]
elsif env["REQUEST_METHOD"] == "POST" && env["PATH_INFO"] =~ /^\/authenticate/
users = User.connection.select_all("SELECT * FROM users WHERE username='#{params["user"]["username"]}' AND password='#{params["user"]["password"]}'")
unless users.empty?
session["user_id"] = users.first["id"]
[302, {"Content-Type" => "text/html", "Location" => "#{params["target_path"] || "/"}"}, ["You have been logged in. You are being redirected."]]
else
[302, {"Content-Type" => "text/html", "Location" => "/login"}, ["Authentication failed. You are being redirected."]]
end
else
[404, {"Content-Type" => "text/html"}, ["Not Found"]]
end
end
end
Bases for Understanding the Code
There are a few methods and structures here that are important to note in how this all works.
First, the self.call(env) is the entry point for the Rails Metal application. env refers to the Rack environment.
Next, Rack::Request is a class that provides a convenient interface to the Rack environment. In this case, we’re using it to extract the GET and POST parameters from the request. You can see the full Rack::Request API here.
env['rack.session'] returns the session as a hash. In this case, keys are accessed as strings, not symbols. So, in our case, session[:user_id] won’t work, but session["user_id"] will work.
Finally, we do direct SQL calls through the ActiveRecord object rather than using the find method because it’s faster and we only need a few fields like the id of the user that’s found.
Overview of the Code
Checking Authentication
Our authentication code creates a session variable called user_id and places the current user’s id into it. So, to make sure someone is logged in, we need to make sure that a valid user id is in the session.
To do this we check if the path begins with /admin. env["PATH_INFO"] contains the path without any GET parameters. So, we do a regular expression match and them move on to checking if there’s a valid user id in the session.
There are two scenarios that we need to check. There is no user id stored in the session and the user id stored is not the id of a valid user.
If both conditions are met, we redirect the user to /login and pass along the path the user was trying to access as a GET parameter called “target_path.”
Logging the user in
If a valid username and password are passed to the path /authenticate, we need to store the user’s id in the session and redirect them to where they were trying to go. To check the username and password, we query the database. If no matches are found in the database, then we send them to the login page.
Overall the code is pretty simple. Feel free to use it in any of your applications. I’ll post the benchmarks when I get a chance, but several people were looking for this content, so I’m putting it out now.









{ 4 comments… read them below or add one }
Thanks for showing us an example of where and how Rails Metal can be used.
Keep in mind that the code in this article is vulnerable to SQL injection. To fix this, would AR::Base#find be best, despite its overhead?
Thanks for the heads up. This was just quick and dirty demo code, I should have disclaimed that so people can take the proper security precautions when using it. I would also recommend hashing and salting the password and making sure that you’re encrypting the connection where possible to minimize vulnerabilities.
Hi Chuck,
Thanks for sharing this.
You are doing : “if env["PATH_INFO"] =~ /^\/admin.*/” for checking urls, but is there a way to already access the url controller mapping ( aka routes ) at this point in the rake stack ?
I’m asking cause I’m authenticating on a controller basis and I would like use Rails metal for checking authentications.
Thanks,
Flo
Nice, but what about sql injection?
{ 3 trackbacks }