Skip to content
unixmonkey edited this page Dec 14, 2014 · 3 revisions

API Authentication and Authorization

API authorization

Now that we've got that working, we need to scope the notes to the currently logged-in user.

Just as we wrote an authorize_user method for authorization in the web app, we also need to write a before_action to check for a current_user when using the API. Since this is a JSON API, we can't expect to be able to save a session cookie, so we will use a token—a user-specific API key—in its place.

Let's create a migration with bin/rails g migration add_api_key_to_user:

class AddAPIKeyToUser < ActiveRecord::Migration
  def change
    add_column :users, :api_key, :string
  end
end

Let's assume that a user's API token is just a longish string of random characters, kind of like a password. We've seen that has_secure_password creates a longish digest that looks like that sort of thing. It does so using BCrypt.

Let's use BCrypt to encrypt something for the API key as well, because encryption is cool.

By the way, BCrypt encryption is not reversible encryption. It creates a complex digest of whatever you pass it. If you pass it the same thing twice, the digests will match, but there's no way to get the password back from the digest, short of using brute force to try all possible letter combinations to make a successful match, which would take a very long time.

In the real world, we would add a screen to the web app for users to generate their API keys, but for simplicity's sake let's just make a before_create action on our User model to set a BCrypt digest as the api key for all new users:

app/models/user.rb

before_create :generate_api_key

private

def generate_api_key
  self.api_key = BCrypt::Password.create(password_digest)
end

Whoa, what's going on here? We're taking the password_digest (which is already a long BCrypt-generated string), and using it as the seed for a new BCrypt key, and setting it on the user when they sign up. This ensures that the seed for our new BCrypt key is nice and complex.

Now we need a way to authorize, and a way to get current_user in the scope of a JSON request.

We could add this to our ApplicationController, but then we'd be mixing our API chocolate into the web app's peanut butter, so let's make a new base controller for just the API.

app/controllers/api/v1/api_controller.rb

class API::APIController < ApplicationController
  skip_before_action :verify_authenticity_token

  protected

  def authorize_api_key
    unless current_api_user.present?
      render nothing: true, status: :unauthorized
    end
  end

  private

  def current_api_user
    @current_api_user ||= User.find_by(api_key: params[:api_key]) if params[:api_key]
  end
end

This is going to inherit from our main ApplicationController (which, in turn, inherits from ActionController::Base). We could have inherited from ActionController::Base directly here, but this way we could potentially have access to any application-wide helpers we may need in the future.

We're also turning off [CSRF](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) protection for the entire API here. Making use of the form authenticity token in the API is doable, but it's outside the scope of this example.

Let's go back to our API NotesController and change it to inherit from APIController and to check for an api key on every request: app/controllers/api/v1/notes_controller.rb

class API::V1::NotesController < API::APIController

  before_action :authorize_api_key

Let's scope all the queries for notes to the current_api_user: app/controllers/api/v1/notes_controller.rb

def index
  @notes = current_api_user.notes.all
end

def show
  @note = current_api_user.notes.find params[:id]
end

Now, if we've wired everything together correctly, we should be able to see our stuff if we pass params[:api_key] to all our requests.

(Many APIs actually have you send the API key in an HTTP header rather than a query string parameter, but let's keep things simple for this example.)

Let's get the api_key for a user in our app—use the console to call generate_api_key on an existing user if you like—and pass that along in the request like this:

http://localhost:3000/api/v1/notes?api_key=some_long_api_key

Huzzah! Sort of. We're not quite finished yet.

Because PostgreSQL (and some other databases) won't always return records in order, so let's add a scope to make sure they always come back in the order they were created at, newest stuff first.

app/models/note.rb

scope :ordered, -> { order('created_at DESC') }

app/controllers/api/v1/notes_controller.rb

  def index
    @notes = current_api_user.notes.ordered
  end

This seems like a good time to commit.