-
Notifications
You must be signed in to change notification settings - Fork 0
23 API Auth
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.