This tutorial is out of date and no longer maintained.
Note: Part two of a three-part series.
Build a RESTful JSON API With Rails 5 - Part One Build a RESTful JSON API With Rails 5 - Part Two Build a RESTful JSON API With Rails 5 - Part Three
In part one of this tutorial, we managed to generate an API-only Rails application, set up a testing framework, and use TDD to implement the todo API.
In this part of the tutorial, we’ll implement token-based authentication with JWT (JSON Web Tokens). In this implementation, we’ll proceed with our approach of using TDD principles to add the authentication features.
Our API should be able to support user accounts with each user having the ability to manage their own resources. We’ll adapt Hristo’s approach with notable alterations.
First, generate a user model.
If you’re wondering why we have password_digest
field instead of a normal password
field, hang tight, we’ll go over this soon enough. :)
Let’s define the user model spec.
Users should be able to manage their own todo lists. Thus, the user model should have a one-to-many relationship with the todo model. We also want to make sure that on every user account creation we have all the required credentials.
Let’s add a user factory. This will be used by our test suite to create test users.
Run the tests and…
User specs are failing as expected. Let’s fix that by implementing the user model.
Our user model defines a 1:m relationship with the todo model also adds field validations.
Note that user model calls the method has_secure_password
, this adds methods to authenticate against a bcrypt
password. It’s this mechanism that requires us to have a password_digest
attribute. Thus, we need to have the bcrypt
gem as a dependency.
Install the gem and run the tests.
All green!
Model’s all set up to save the users. We’re going to wire up the rest of the authentication system by adding the following service classes:
jwt
tokensWe’re going to implement token-based authentication. We’ll make use of the jwt
gem to manage JSON web tokens (JWT). Let’s add this to the Gemfile and install it.
Our class will live in the lib
directory since it’s not domain specific; if we were to move it to a different application it should work with minimal configuration. There’s a small caveat, though…
As of Rails 5, autoloading is disabled in production because of thread safety.
This is a huge concern for us since lib
is part of auto-load paths. To counter this change, we’ll add our lib
in app
since all code in-app is auto-loaded in development and eager-loaded in production.
Note: Here’s a long discussion on the above.
Great, let’s do this! Add the lib directory and the JWT class file.
Define jwt singleton.
This singleton wraps JWT
to provide token encoding and decoding methods. The encoding method will be responsible for creating tokens based on a payload (user id) and expiration period. Since every Rails application has a unique secret key, we’ll use that as our secret to sign tokens.
The decode method, on the other hand, accepts a token and attempts to decode it using the same secret used in the encoding. In the event decoding fails, be it due to expiration or validation, JWT
will raise respective exceptions which will be caught and handled by the Exception Handler
module.
We’ve defined custom Standard Error
sub-classes to help handle exceptions raised. By defining error classes as sub-classes of standard error, we’re able to rescue_from
them once raised.
This class will be responsible for authorizing all API requests making sure that all requests have a valid token and user payload.
Since this is an authentication service class, it’ll live in app/auth
.
Let’s define its specifications
The AuthorizeApiRequest
service should have an entry method call
that returns a valid user object when the request is valid and raises an error when invalid.
Note that we also have a couple of test helper methods;
token_generator
- generate test tokenexpired_token_generator
- generate expired tokenWe’ll define these helpers in spec/support
.
We also have additional test helpers to generate headers. In order to make use of these helper methods, we have to include the module in rails helper
. While we’re here let’s also include RequestSpecHelper
to all types (not just requests); remove type: :request
. This way, we’ll be able to reuse our handy json
helper.
At this point, if you attempt to run the tests, You should get a load error. You guessed it, this is because we haven’t defined the class. Let’s do just that!
The AuthorizeApiRequest
service gets the token from the authorization headers, attempts to decode it to return a valid user object. We also have a singleton Message
to house all our messages; this an easier way to manage our application messages. We’ll define it in app/lib
since it’s non-domain-specific.
Run the auth specs and everything should be green.
This class will be responsible for authenticating users via email and password.
Since this is also an authentication service class, it’ll live in app/auth
.
Let’s define its specifications.
The AuthenticateUser
service also has an entry point #call
. It should return a token when user credentials are valid and raise an error when they’re not. Running the auth specs and they should fail with a load error. Let’s go ahead and implement the class.
The AuthenticateUser
service accepts a user email and password, checks if they are valid, and then creates a token with the user id as the payload.
This controller will be responsible for orchestrating the authentication process making use of the auth service we have just created.
First thing’s first. Tests!
The authentication controller should expose an /auth/login
endpoint that accepts user credentials and returns a JSON response with the result.
Notice how slim the authentication controller is, we have our service architecture to thank for that. Instead, we make use of the authentication controller to piece everything together… to control authentication. We also need to add routing for authentication action.
In order to have users authenticate in the first place, we need to have them signup first. This will be handled by the users’ controller.
User signup spec.
The user controller should expose a /signup
endpoint that accepts user information and returns a JSON response with the result.
Add the signup route.
And then implement the controller.
The users’ controller attempts to create a user and returns a JSON response with the result. We use Active Record’s create!
method so that in the event there’s an error, an exception will be raised and handled in the exception handler.
One more thing, we’ve wired up the user authentication bit but our API is still open; it does not authorize requests with a token.
To fix this, we have to make sure that on every request (except authentication) our API checks for a valid token. To achieve this, we’ll implement a callback in the application controller that authenticates every request. Since all controllers inherit from the application controller, it will be propagated to all controllers.
Cool, now that we have the tests, let’s implement the authorization.
On every request, the application will verify the request by calling the request authorization service. If the request is authorized, it will set the current user
object to be used in the other controllers.
Notice how we don’t have lots of guard clauses and conditionals in our controllers, this is because of our error handling implementation.
Let’s remember that when signing up and authenticating a user we won’t need a token. We’ll only require user credentials. Thus, let’s skip request authentication for these two actions.
First, the authentication action.
Then the user signup action.
Run the tests and you’ll notice, our Todo and TodoItems API is failing. Don’t fret, this is exactly what we want; means our request authorization is working as intended. Let’s update the API to cater to this.
In the Todos request spec, we’ll make a partial update of all our requests to have authorization headers and a JSON payload.
Our todos controller doesn’t know about users yet. Let’s fix that.
Let’s update the Items API with the same.
Awesome, our specs are now up to date! Phew!
Let’s fire up the server for some manual testing.
That’s it for part two! At this point, you should have learned how to implement token-based authentication with JWT.
In the next part of this tutorial, we’ll wrap up with API versioning
, pagination
and serialization
. As always, hope to see you there. Cheers!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!