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
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.
I originally mentioned Token based authentication, but it's actually Basic Auth (with "username" = "token").
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
All you need to do is to append a catch-all route at the end of your routes.rb:
YourApp::Application.routes.draw do
# Standard routes
# ...
# Catch-all
get '*other', to: 'dummy#show'
end
Then have a dummy controller just render 404 - Not Found
in show action. Should anyone visit a non-existing route it will be handled by dummy, which will perform authentication first in a before filter and if it fails 401 - Unauthorized
is rendered.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With