Mimimal API authentication on Rails

来源:互联网 时间:1970-01-01


We’ve been building a lot of Ember.jsauthentication mechanisms for several projects, and we’re starting to think that we’ve got it down to a fine art. We’ve been treating my front-end apps as simply an API consumer, which means that they need a similar method of authenticating their API access as that of a tradition (service-based) API client.

A lot of our current thinking on how to put this together can be found in Blogomat, an example application We’re using as a test-bed for ideas of how to build apps with Ember and Rails.

Let’s have a look at our requirements for how we’re going to authorise API client accesses:

Some sort of short-lived session token which can be acquired by the API client to use when accessing API resources to authorise the client. A way of authenticating a user with their username and password to attach it to our session token. A way of authenticating a service with a secret that is shared by the server and the client, without that secret being transmitted over the wire.

Some additional design goals include:

Write the minimum amount needed to implement the required features without adding any overly complicated dependencies to our application (ie Devise). Authentication methanisms should be easily understood so that those writing API clients don’t have to struggle to understand what’s going on. Try and stay as close to RESTful ideals, and compatible with JSON APIas possible.

So, in Blogomat, we have a simple (non-versioned) API namespace in my routes file:

Blogomat::Application.routes.draw do namespace :api, defaults: {format: :json} do resource :sessions, only: [:create, :show, :destroy] endend

Seems straight forward enough, let’s create our Api::SessionsController. The first thing we do is create a abstract ApiController class, which (much like the conventional ApplicationControllercan encapsulate shared behaviour across all API endpoints).

class Api::SessionsController < ApiController def create endend API Session Tokens

We know at this point that we’re going to need some sort of api token object for the Api::SessionsControllerto create and return, let’s think about this object a little. What do we know about it from the requirements?

We know that it needs some sort of unique identifier. We know that it is short-lived, so needs to have a TTL. We know that it needs to be able to refer to a user object.

One thing we don’t know is that it needs to be stored in the database, and in fact, since we know that we will be checking this token every time we receive an API request, it’s probably overkill.

We also think that it would be nice if we could store tokens in some sort of fast, shared storage so that if we need to scale our app horizontally we have that capability without massively changing the underlying authorisation scheme. In this vein, we have chosen to store the session tokens in Redis, however you could choose to use Memcacheor even just in a Hash, should you want (and in fact, in my tests we have it using a faked-out Redis client backed by a Hash).

The main reason we chose Redis is that it’s reasonably ubiquitous, and has the handy ability to expire keys, so that we don’t have to worry garbage collecting our ApiSessionTokens. This example simply uses a hash to cut it down to a reasonable size to demo.

class ApiSessionToken TTL = 20.minutes def self.store @store ||= Hash.new end def initialize(existing_token=nil) @token = existing_token self.last_seen = Time.now unless expired? end def token @token ||= MicroToken.generate 128 end def ttl return TTL unless last_seen elapsed = Time.now - last_seen remaining = (TTL - elapsed).floor remaining > 0 ? remaining : 0 end def last_seen store[:last_seen_at] end def last_seen=(as_at) store[:last_seen_at] = as_at end def user return if expired? store[:user] endo def user=(user) store[:user] = user end def expired? ttl < 1 end def valid? !expired? end private def store self.class.store[token] ||= {} endend

Now that we have an object we can use, let’s alter our sessions controller to return it:

class Api::SessionsController < ApiController def create if params[:username] @user = User.find_by_username(params[:username]) token.user = @user if _provided_valid_password? || _provided_valid_api_key? end respond_with token end private def _provided_valid_password? params[:password] == 'foo password' end def _provided_valid_api_key? params[:api_key] == 'foo key' endend

So this is pretty cool. Now if we have receive a POST it’ll return a valid API session token, which will last for 20 minutes and then drop off the map after that.

If they post again with a username and password, or username and api key then we’ll let then sign in as that user for the duration of their session, provided they have provided valid credentials.

Now we can go back and write some helpers in ApiControllerto make working with these sessions easier, and add a before filter to protect API actions from access without a session token.

class ApiController < ApplicationController before_filter :api_session_token_authenticate! private def signed_in? !!current_api_session_token.user end def current_user current_api_session_token.user end def api_session_token_authenticate! return _not_authorized unless _authorization_header && current_api_session_token.valid? end def current_api_session_token @current_api_session_token ||= ApiSessionToken.new(_authorization_header) end def _authorization_header request.headers['HTTP_AUTHORIZATION'] end def _not_authorized message = "Not Authorized" render json: {error: message}, status: 401 endendclass Api::SessionsController < ApiController skip_before_filter :api_session_token_authenticate!, only: [:create]end Password Authentication

The next task is to get password authentication working. We’ve decided to run with the herd and use bcrypt, which saves us having to write our own hashing algorythm. Phew!

class User < ActiveRecord::Base validates_presence_of :username, on: :create validates_uniqueness_of :username, on: :create validates_presence_of :email_address, on: :create validates_uniqueness_of :email_address, on: :create validates_presence_of :password, on: :create def password=(secret) write_attribute(:password, BCrypt::Password.create(secret)) endend

Now we can create a UserAuthenticationServiceto encapsulate our authentication logic. Basically we just delegate to bcrypt-ruby.

module UserAuthenticationService module_function def authenticate_with_password(user, attempt) user && BCrypt::Password.new(user.password) == attempt endend

And wire it up to our sessions controller.

class Api::SessionController < ApiController def _provided_valid_password? params[:password] && UserAuthenticationService.authenticate_with_password!(@user, params[:password]) endend

Great! Now our user an send an API request to authenticate with their password in order to be able to access the API.

API Key Authentication

The next step is to provide a way of authenticating with a shared-secret, which isn’t transmitted between the client and the API during normal API usage. This sort of authentication is usually used by services trying to consume your API, rather than users themselves.

So, our design goals are as follows:

Based on a secret shared between the client and server. Must be easily computed by both the server and client. Must be unique enough that it’s not succeptable to a MITMor Replayattack.

Of course, your API is running over SSL, right?!

With our requirements mapped out, we can implmenent our API key authentication and wire it up to our controller:

module UserAuthenticationService module_function def authenticate_with_api_key(user, key, current_token) user && key && current_token && OpenSSL::Digest::SHA256.new("#{user.username}:#{user.api_secret}:#{current_token}") == key endendclass Api::SessionsController < ApiController def _provided_valid_api_key? params[:api_key] && UserAuthenticationService.authenticate_with_api_key!(@user, params[:api_key], current_api_session_token.token) endend Summary

This has been a much longer blog post than we initially sat down to write, so we’ll stop here. We plan to follow up soon with an entry about implementing the client-side API session token logic in Ember.js.

Thanks for taking the time to read all this!



相关阅读:
Top