loading...

Guide to devise_token_auth: Simple Authentication in Rails API

risafj profile image Risa Fujii Updated on ・4 min read

I wanted to create an authentication system for my Rails API, but one thing about APIs (with no client) is that you can't use sessions or cookies for authentication.
So instead, I used the gem devise_token_auth, which uses tokens. Put simply, this is how it works: when you make HTTP requests to sign up or log in, the response headers give you authentication tokens, which you send in subsequent HTTP requests' headers to prove that you're authenticated.

While the official docs provide most of the information you need, there were a few points that I found confusing so I'm leaving this article for future reference. Hope it helps!

Note: Guide is for Linux or MacOS.

Implementation Steps

Please also feel free to check out the bare-bones repository I created, as a proof of concept for how to use this gem.

1. Install devise_token_auth

Add the following to your Gemfile, then run bundle from your command line:

gem 'devise_token_auth'

2. Generate necessary files

Execute this from your command line.

rails g devise_token_auth:install User auth

This will do many things, including:

  • Create a User model, which stores information such as users' email addresses, and a corresponding migration file
  • Add a line in your config/routes.rb file specifying the route for authentication endpoints like sign up or sign in (If you want it routed somewhere other than /auth/, swap out the "auth" in the command with something else)

For the exhaustive list of what this command does, check out the docs.

Update

According to Rafael's comment below, it's now necessary to add extend Devise::Models to the User model file generated here. Details can be found in this Github issue.



<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">

Hi!
Nice post, only add below statement: "extends Devise::Models" to begin User's model, after devise_token_auth install. This to rails version 6.

something else is ok :)

3. Migrate your database

Run rails db:migrate in your command line to apply the migration file that was created in step #2, which probably looks something like db/migrate/YYYYMMDDTTTT_devise_token_auth_create_users.rb.

4. Configure your initializer file

Go to your config/initializers/devise_token_auth.rb file (also created in the rails g command in step #2).

Again, the docs have the complete list of configurations you can make, but to give an example:

config.change_headers_on_each_request = false

By default, the authorization headers change with each request. This means that you would get back new tokens with each request, and you would have to send back different tokens each time. I wanted to be able to keep using the same tokens, so I turned it off by setting it to false.
Reusing your tokens is not the best security practice, so it may be better to use fresh tokens each time in production.

5. Disable forgery protection for JSON requests

Rails controllers have pre-set measures against Cross-Site Request Forgery (CSRF) attacks. This involves comparing tokens in the rendered HTML to tokens that Rails stores automatically in your session, but for APIs, we have no sessions and will use our own tokens so it's unnecessary (explained here).

It's important to remember that XML or JSON requests are also affected and if you're building an API you should change forgery protection method in ApplicationController (by default: :exception)

So go ahead and disable forgery check for JSON format requests, in app/controllers/application_controller.rb.
Note: Only do this if all your requests come in through your API.

class ApplicationController < ActionController::Base
  protect_from_forgery unless: -> { request.format.json? }
end

Notes/Caveats

  • Skipping this step should result in this error: ActionController::InvalidAuthenticityToken in DeviseTokenAuth::RegistrationsController#create.
  • If your ApplicationController inherits from ActionController::API (which should be the case if you initialized the project with the --api flag), this step should be unnecessary as there is no forgery protection by default.

6. Try signing up a user

Now, let's try signing up a test user. Boot up your Rails server from the command line with rails s, and send a HTTP POST request to localhost:3000/auth/ (or your custom route) with the following parameters.

{
  "email": "test@email.com",
  "password": "password",
  "password_confirmation": "password"
}

It should return status 200 - success. Now your authentication system works!

7. Add authentication to your controller

Next, add a line in the respective controller file to make authentication necessary. For example, let's say we're building an API for a database that stores information on books. We have the file books_controller.rb for adding or deleting books from the database. We want to authenticate the user before they add or delete entries.

class BooksController < ApiController
  before_action :authenticate_user!

  # Code for methods such as create and delete should come here.
end

Now, any time you send a HTTP request to any methods in your books controller, an error will be returned unless you're authenticated.

Actual Usage

I mentioned earlier that the HTTP request for signing up users is a POST request to localhost:3000/auth/. As for the full list of methods (e.g. logging users in/out, and changing their passwords), please refer to the docs. You can see what request type, route, and parameters are required.

Finally, let's talk about actual usage. Here are the steps involved:

  1. Send authentication request (sign up or sign in)
  2. Status 200 is returned, with valid authentication tokens in the headers
  3. In your next request, send those tokens in your headers

The required token categories are:

  • access-token
  • client
  • uid

It's that simple!

I wanted to also discuss tips for testing, but since this is getting long, I will write that in a separate post. Thanks for reading :)

Update

Here is the article on testing!

Additional Update

It seems like the trackable module was dropped from the devise gem, which devise_token_auth is based on. So the default configuration in the User model, which includes :trackable, is likely to raise an error like this: NoMethodError: undefined method 'current_sign_in_at'.
For details on how to resolve this, see the links below:

Posted on by:

risafj profile

Risa Fujii

@risafj

I'm a self-taught web developer working for a small tech company in Tokyo 👩🏻‍💻 I enjoy writing about new topics I learned in a beginner-friendly way.

Discussion

pic
Editor guide
 

Gonna add this as I'm sure many will find this useful. ✊🏽

Rails.application.config.middleware.insert_before 0, Rack::Cors do
    [...]
      expose: ['access-token', 'uid', 'client'],
    [...]
  end
end
 

Thanks! devise_token_auth works great for me and I found your other article about testing useful as well.

But :)! What if you do need to store some session data? I'm trying to authenticate with the Discogs API which involves generating a request token, going to their website to authorize, which then redirects you to a callback route on the Rails API. What's the correct way to persist that request token in between those two requests?

Is it bad practice to just store it in a DB column for the user?

 

Hi! Thanks for reading, and I’m happy to hear it helped 😄
Please take my ideas below with a grain of salt, since I don’t know your specific use case and I haven't used the Discogs API.

generating a request token, going to their website to authorize, which then redirects you to a callback route on the Rails API.

I’m guessing from this description that your app has a browser client? In that case, you should be able to use session storage normally and store it like this: session[:discogs_token] = <the request token>
If you used Rails’s API mode when initializing your project (the --api flag), sessions won’t be available to you by default so it looks like you’ll have to configure a few things: stackoverflow.com/q/15342710/11249670

On the other hand, if you're supposed to store the token for a long time (longer than the session), then storing it in the DB sounds like a good idea.
For example, in a different blog post that I linked below, I talk about refresh tokens, which are supposed to be reused in every session.
In this blog post's case, I store normal access tokens in the session, and refresh tokens in the DB.

Hope this helps somewhat!

 

Thanks for taking the time to get back to me. My API is consumed by a Vue.js client (using vue-auth). I've tried all the different middlewares and setting api mode to false but always see my session contents emptied.

Ah well! For now I'm saving the request_token in my DB until the callback is called. And yes, the access_token is needed for using the API once authorized.

No problem, sorry I can't be more helpful. If your issue is that you can't use session at all with your configuration (not just for devise_token_auth), it might be a good question for Stack Overflow. Best of luck!

 

It's nice to let readers know that Devise gem must be installed first 🙃

 

Hi, thanks for the comment. I imagine that if you add devise_token_auth to your Gemfile and run bundle, the Devise gem would be added as a dependency (so you don't need to install Devise separately). Is this not the case?

 

Hi. It's not the case with Rails 6 API in my case. I do not know why. My Gemfile:

# Authentication
gem 'devise'
gem 'devise_token_auth'

Happy new year :)
Hmm, that's strange. Out of curiosity, I created a new repo and followed my own tutorial. I had to make some updates in the article to compensate for changes made in devise, but I didn't have to include the devise gem in the Gemfile. Feel free to check out the project: github.com/risafj/demo-for-devise-...

I had a fresh copy of the api installed using ruby 2.5.0. If I remove the devise gem, the auth breaks lol I know this is weird. I'll upgrade both ruby and rails and try again. But yea, I believe something weird is happening with my setup.

Thanks.

To update. Another fresh installation with ruby 2.7.0 and Rails 6.0.2.1 and all is well (production). Weird, I know. 😅

Hmm... we may never find out why 😂 Glad it’s working now though! Thanks for the update!

 

You actually can use session cookies for API authentication...as long as the API client is a web browser. Given that caveat, I thought this approach was interesting because it takes advantage of the battle-tested CSRF protection already built into Rails.

pragmaticstudio.com/tutorials/rail...

 

Thank you for your comment! Perhaps I should've specified - I meant Rails API with no front-end when I was talking about not being able to use sessions.

 

Awesome post! Exactly what I was looking for.

 

Thanks for the comment, happy to hear it helped!

 

Nice, this help me today...Thanks

 

Hi, happy to hear it! :)

 

Hi!
Nice post, only add below statement: "extends Devise::Models" to begin User's model, after devise_token_auth install. This to rails version 6.

something else is ok :)

 

Just saw this - thank you, I will add it as a note in the post!

 

Nailed it ! thanks for your work!

 

this is a very difficult topic.. thanks for covering it for us noobies

 

Thanks for the comment! Happy to hear it helped :)

 

Hi. Thanks for the tutorial, but I have a little challenge customizing the session controller .... I will be glad if I can get a little assistance in that regard...
Jerry A.
jesyontop01@gmail.com

 

Hi, what kind of issue are you having? I don't know if I'd be able to help (you may have a better time asking on the devise_token_auth's Github issues or Stack Overflow) but maybe I could take a look!