Rails: Authenticate before routing
ruby-on-rails, ruby-on-rails-4
Solution
Using the built-in rails authentication you can't achieve what you want for the very reason @rich-peck explained. Rails first evaluate your routes, then passes the request for the controller.
You have two options. First, do exactly what the Stripe guys are doing. Delegate the authentication to the web server in front of your app server (nginx in their case).
$ curl -I https://api.stripe.com/this/route/doesnt/exist
HTTP/1.1 401 Unauthorized
Server: nginx
Date: Fri, 08 Aug 2014 21:21:49 GMT
Content-Type: application/json;charset=utf-8
Www-Authenticate: Basic realm="Stripe"
Cache-Control: no-cache, no-store
Stripe-Version: 2014-08-04
Strict-Transport-Security: max-age=31556926; includeSubDomains
Or, if you want/need to keep it in ruby-land, you can use a rack middleware which will authenticate before rails loads your routes. You can try https://github.com/iain/rack-token_auth
$ bin/rake middleware
...snip a bunch of middleware...
use Rack::TokenAuth
run Dummy::App::Application.routes
Update: Using HTTP Basic Authentication
In that case, you don't need another library at all, because rack has built-in support for http basic auth through the `Rack::Auth::Basic` middleware, e.g.
config.middleware.use Rack::Auth::Basic do |username, password|
username == "bob" && password == "secret"
end
Update: Can I apply the authentication to the `API` namespace?
It depends. Because the rack middleware runs before rails loads your routes, controllers, etc. it has no knowledge of your namespaces. Nonetheless, if your namespaced controllers are also namespaced at the URL level, e.g. all under `/api`, you can check `request.path` to whether apply the authentication.
You can create a new rack middlware to decorate `Rack::Auth::Basic` with this behaviour, e.g.
class MyAPIAuth < Rack::Auth::Basic
def call(env)
request = Rack::Request.new(env)
if request.path =~ /\A\/api\/?/
super
else
@app.call(env)
end
end
end
# change your configuration to use your middleware
config.middleware.use MyAPIAuth do |username, password|
username == "bob" && password == "secret"
end
Problem
The What. I'm trying to implement token based Basic authentication on my Rails API. It works fine for existing routes, but here's the catch: When an unauthenticated user visits a route that does NOT exist, it displays the `404 - Not found` page, and not the `401 - Unauthorized`. How do I get Rails to check authentication before validating the routes? Here's my `application_controller.rb`: ``` class Api::V1::ApiController < ApplicationController # Main controller inherited by all API controllers # For APIs, you may want to use :null_session instead. protect_from_forgery with: :null_session # Enforce site-wide authentication before_action :authenticate def authenticate authenticate_token || render_unauthorized end def authenticate_token # Can we find a user with the authentication token used? authenticate_with_http_basic do |u, p| # Only search active, private keys @api_key = ApiKey.active.find_by(api_key: u, is_secret: true) @user = @api_key.user if @api_key return @api_key end end def render_unauthorized # Displays the Unauthorized message render json: JSON.pretty_generate({ error: { type: "unauthorized", message: "This page cannot be accessed without a valid API key." } }), status: 401 end end ``` The Why Think of it this way: someone stops you at the office to ask for directions. Do you ask them to present some ID first or do you just show them the way? If it were a public office, I'd just show them the way .. but if we were in the restricted Special Projects division at the CIA, I don't care where you're going (even especially if you tell me you're looking for Office 404, which I know doesn't exist): I want to see some ID first. Edit: Using Basic Auth I originally mentioned Token based authentication, but it's actually Basic Auth (with "username" = "token").