In our last post, we reviewed signing up users via a
POST /users endpoint in our API. In this episode, we'll review how we authenticate users using an authentication token passed in the authorization header.
Note: This tutorial is designed for someone who has a beginning to intermediate knowledge of Ruby on Rails.
We use Warden to authenticate our endpoints. Warden lets you define custom strategies for restricting access to routes in your application. This is especially useful if you have different authentication rules for different endpoints.
In this post, we'll create a
GET /widgets endpoint that can only be accessed by our users. We'll require users to pass up their authentication token in an authorization header like so:
(In case you were wondering, the
Token token= convention comes from RFC-2617, which suggests passing both the auth scheme -- “Token” -- and the auth param --
token=<my_auth_token> as shown above.)
There are three components we'll need to implement:
(1) A constraint to wrap our routes:
(2) The Warden strategy that the constraint uses to enforce authentication.
(3) A method on our
User model that will find a user given an authentication token. Our strategy will use this method to validate that a user with a given auth token exists.
Tools We'll Use
Let's get started.
Step 1: Write a test
By the time we get to implementing an authentication system, we usually have at least one endpoint that requires authentication. So let's start with a
GET /widgets endpoint that we want to restrict access to. (Here’s the commit.)
Now let's add a test in our
widgets_requests_spec.rb that ensures that a 401 status is returned if an unauthenticated user tries to access our endpoint:
This test should fail.
Step 2: Add a constraint
Rails' constraints allow us to define rules for restricting access to particular routes in your app. A constraint class must simply implement a method called
matches? that returns true if access is allowed and false otherwise.
Let's create a new constraint called
AuthenticatedConstraint and use it to wrap our
GET /widgets route. Our constraint's
matches? method will rely on the Warden strategy we'll implement next, which we'll call
TokenAuthenticationStrategy to reflect the fact that it relies on an authentication token:
What's going on here? First, our constraint is grabbing the information that the Warden middleware inserts into the request's environment. If that is present, it then uses Warden to call the
authenticate! method that we'll define on our
TokenAuthenticationStrategy. This is a convention of Warden strategies: they must implement an
Then we're telling Warden about our strategy by calling
Warden::Strategies.add (which we could really do anywhere, but here seems as good a place as any). This stores our
TokenAuthenticationStrategy in a hash of strategies that Warden keeps track of.
Step 3: Add our strategy
Now let's add our Warden strategy:
Let's walk through this. Our class inherits from
Warden::Strategies::Base, which gives us a few things:
success!method that allows the request to proceed and sets the user in the request environment as
fail!method that halts the request and calls Warden's failure app. The failure app tells Warden what to do if a request fails. If we're using Monban to authenticate users using email and password, as we are, it sets up a default failure app for us that will return a 401 response. If not, you'll have to set this yourself in
authenticate!, Warden will first call our
valid? method. If it returns false, Warden won't attempt to authenticate.
authenticate! method pulls the token out of the request headers, looks for a user with that (unexpired) token, and calls Warden's
fail! methods depending on whether it finds such a user.
There's one other thing here that's very important for authenticating API requests (as opposed to web requests). Warden's
store? method determines whether a user should remain logged in across requests. If not overridden, this method returns `true`, which will allow user a user to pass a valid auth token once and then remain authenticated across subsequent requests. This is :no_good:.
Let's run our tests. They should be passing!
Step 4: Extract a `User#for_authentication` method
We could leave as is and be done. However, I like to extract the logic in
TokenAuthenticationStrategy#user to a class method on the
User model for easier testing. (commit)
See the demo app repo for the new method's tests.
Tune in next time for our third part in this series, in which we'll add a
POST /authentications endpoint to regenerate authentication tokens when a user's token expires.