secret
Last active

  • Download Gist
1_unsafe_token_authenticatable.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
# This snippet shows how TokenAuthenticatable works in Devise today.
# In case you want to maintain backwards compatibility, you can ditch
# devise's token mechanism in favor of this hand-rolled one. If not,
# it is recommended to migrate to the mechanism defined in the following
# snippet (2_safe_token_authenticatable.rb).
#
# In both snippets, we are assuming the User is the Devise model.
class User < ActiveRecord::Base
# You likely have this before callback set up for the token.
before_save :ensure_authentication_token
 
def ensure_authentication_token
if authentication_token.blank?
self.authentication_token = generate_authentication_token
end
end
 
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
end
 
# With a token setup, all you need to do is override
# your application controller to also consider token
# lookups:
class ApplicationController < ActionController::Base
# This is our new function that comes before Devise's one
before_filter :authenticate_user_from_token!
# This is Devise's authentication
before_filter :authenticate_user!
 
private
# For this example, we are simply using token authentication
# via parameters. However, anyone could use Rails's token
# authentication features to get the token from a header.
def authenticate_user_from_token!
user_token = params[:user_token].presence
user = user_token && User.find_by_authentication_token(user_token.to_s)
 
if user
# Notice we are passing store false, so the user is not
# actually stored in the session and a token is needed
# for every request. If you want the token to work as a
# sign in token, you can simply remove store: false.
sign_in user, store: false
end
end
end
2_safe_token_authentication.rb
Ruby
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
# We could make the authentication mechanism above a bit more safe
# by requiring a token **AND** an e-mail for token authentication.
# The code in the model looks the same, we just need to slightly
# change the controller:
class ApplicationController < ActionController::Base
# This is our new function that comes before Devise's one
before_filter :authenticate_user_from_token!
# This is Devise's authentication
before_filter :authenticate_user!
 
private
def authenticate_user_from_token!
user_email = params[:user_email].presence
user = user_email && User.find_by_email(user_email)
 
# Notice how we use Devise.secure_compare to compare the token
# in the database with the token given in the params, mitigating
# timing attacks.
if user && Devise.secure_compare(user.authentication_token, params[:user_token])
sign_in user, store: false
end
end
end

I have a different idea:

Why not use "#{user.id}:#{user.authentication_token}" as the authentication token?
We could then split up the parameter and use the user id to query the user and securely compare the authentication token. Querying by id should be also faster in most database systems and we will not break existing APIs this way.

@Mic92 I'm going to try this in our application. I'll report back on how it goes.

I just implemented option #2 and it works nicely. I agree with @Mic92, that there is a better performant way other than using email address. However, I think using the PK of the user record may be a bit too revealing of the underlying system. So I'm thinking of a secure hash composed of "#{user.id}:#{user.authentication_token}" that can be generate by either client/server via a shared secret token.

For option #1, why did you add "before_filter :authenticate_user!" ?

If I add this, calls to sessions#create returns an error:

{"error":"You need to sign in or sign up before continuing."}

is the loop in the token generation to avoid possible collisions in the event of a duplicate?

seems you can make line 23 more performant as well by avoiding the ActiveRecord overhead of building a record and just asking User.where(authentication_token: token).exists? no benchmarks to prove it but i generally try to avoid building an AR object i don't need, right?

How does passing email+token make it safer? If a HTTP request is hacked then the hacker will get both anyway. If the intruder steals the DB he will have both anyway too. What's the point?

@dnagir the intent is to specifically protect against the timing attacks on User.find_by_authentication_token.

If we look up the record with a known key (in this case email) and then do a safe compare on the token, the attacker can't gain any information about the validity of the token by studying timings. This is the only attack this is mitigating.

What does the HTTP request look like? http://www.website.com/profile?user_token=authentication_token&user_email=user@email.com something like that?

I had to change line 21 of 2_safe_token_authentication.rb to

sign_in :user, @user  # sign_in(scope, resource)

to get things working. If interested, see the documentation:

# Sign in a user that already was authenticated. This helper is useful for logging
      # users in after sign up.
      #
      # All options given to sign_in is passed forward to the set_user method in warden.
      # The only exception is the :bypass option, which bypass warden callbacks and stores
      # the user straight in session. This option is useful in cases the user is already
      # signed in, but we want to refresh the credentials in session.
      #
      # Examples:
      #
      #   sign_in :user, @user                      # sign_in(scope, resource)
      #   sign_in @user                             # sign_in(resource)
      #   sign_in @user, :event => :authentication  # sign_in(resource, options)
      #   sign_in @user, :store => false            # sign_in(resource, options)
      #   sign_in @user, :bypass => true            # sign_in(resource, options)
      #
      def sign_in(resource_or_scope, *args)
        options  = args.extract_options!
        scope    = Devise::Mapping.find_scope!(resource_or_scope)
        resource = args.last || resource_or_scope

        expire_session_data_after_sign_in!

        if options[:bypass]
          warden.session_serializer.store(resource, scope)
        elsif warden.user(scope) == resource && !options.delete(:force)
          # Do nothing. User already signed in and we are not forcing it.
          true
        else
          warden.set_user(resource, options.merge!(:scope => scope))
        end
      end

1_unsafe_token_authenticatable.rb:

Shouldn't it be params[Devise.token_authentication_key].presence instead of params[:user_token].presence?

It seems to me we need to raise an exception in authenticate_user_from_token! otherwise by providing the :auth_token parameter we may still be able to sign in by entering the :authenticate_user! method no ?

Alright I just needed to comment out :token_authenticatable from the list of devise modules since it's a custom one.

Should the authentication_token be stored in plain text?

Another approach to consider is to use a quantised time duration for all failed token matches and just do a straight DB or cache lookup on the token. By quantising the time taken for failed lookups we can mitigate timing based attacks.

def authenticate_user_from_token!
  auth_token = params[Devise.token_authentication_key]
  if auth_token
    t = Time.now
    if (user = User.where(authentication_token: auth_token).first)
      sign_in user, store: false
    else
      # ensure requests with a failed token match are quantised to 200ms
      sleep((200 - Time.now + t) % 200) / 1000.0)
    end
  end
end

Yet another approach is to add a small random delay if the token is not matched.

def authenticate_user_from_token!
  auth_token = params[Devise.token_authentication_key]
  if auth_key
    if (user = User.where(authentication_token: auth_token).first)
      sign_in user, store: false
    else
      # sleep 200-400ms
      sleep(200 + rand(200)) / 1000.0)
    end
  end
end

However that said, I think I would prefer an approach similar to Rails signed and encrypted cookies. With a sever secret key, one can perform a decrypt and authenticate of the token without any database overhead and recover the token expiry AND the user id (or anything else you wish to stuff in the token). This avoids any requirement to store tokens in the database as they can be created on the fly (eg multiple devices/client instances per user). The use of AES GCM or AES CCM authenticated encryption modes would work very well.

(Untested). But something like the following for token encryption/decryption

# auth_token.rb
require 'openssl'
require 'base64'

class AuthTokenCipher
  CIPHER_MODE = 'aes-256-gcm'

  def self.make_key
     OpenSSL::Cipher.new(CIPHER_MODE).encrypt.random_key
  end

  def self.encrypt(key, data)
    cipher = OpenSSL::Cipher.new(CIPHER_MODE).encrypt
    iv = cipher.random_iv
    cipher.key = key
    cipher.auth_data = ""
    cipher.update(data)cipher.final
    Base64.strict_encode64(iv + cipher.update(data) + cipher.final + cipher.auth_tag)
  end

  # raises AurgumentError or CipherError if the token is bad
  def self.decrypt(key, data)
    data = Base64.strict_decode64 data
    cipher = OpenSSL::Cipher.new(CIPHER_MODE).decrypt
    cipher.iv = data[0..15]
    cipher.key = key
    cipher.auth_tag = data[-16..-1]
    cipher.auth_data = ""
    cipher.update(data[16..-17]) + cipher.final
  end
end

And use as follows:

def authenticate_user_from_token!
  auth_token = params[Devise.token_authentication_key]
  if auth_token
    # raises AurgumentError or CipherError if the token is bad
    auth_data = AuthTokenCipher.decrypt TOKEN_AUTH_KEY, auth_token

    # at this point we have authenticated data so we can trust the expiry and user id was what we set originally

    # check expiry and lookup the user (if needed)
    expiry, user_id = auth_data.split ':'
    if Time.now < Time.at(expiry) && (user = User.find(user_id))
      sign_in user, store: false
    end
  end
end

Creating the auth token, eg after login with username/password would be achieved as follows:

AuthTokenCipher.encrypt TOKEN_AUTH_KEY, "#{user.id}:#{(Time.now + TOKEN_TTL).to_i}"

As I said, none of the above code has been tested or used in anger and is simply provided as an idea for discussion.

The use the above cipher mode you will need a recent version of OpenSSL (>= 1.0.1) on your system.

Hi everybody, hi @josevalim, thanks for this gist! I've packaged it as two concerns (gist) in order to make it's integration as straightforward as possible. I wouldn't commit a mistake here, and if some of you have a few minutes, peer review will be very appreciated!

Hi everybody,
hi @RuslanHamidullin, I'm glad you appreciated the gist.

I've created a little gem (simple_token_authentication) from it, which makes the integration easier. It may be interesting to you.

(BTW, it's missing tests but I don't know really what to test. If somebody could provide me some help or pointers, they'll be very welcome.)

Regards,

Hi everybody,
first, thanks for this implementation, it really helps with API authentication.

Is there any example of how to integrate this with all the other controllers from Devise (Sessions, Passwords, Registrations, etc.) and it's functionalities (recoverable, trackable, etc.)?

Is it the same as with the previous token_authenticatable implementation?

Thanks in advance.

Thanks @ahacking,

I'm pretty new to this, where should TOKEN_AUTH_KEY and TOKEN_TTL be set/stored?
Could Devise.secret_key be used as TOKEN_AUTH_KEY?

I've added these to the top of auth_token.rb is this bad?

# auth_token.rb
require 'openssl'
require 'base64'

TOKEN_AUTH_KEY = Devise.secret_key
TOKEN_TTL = 1.week

class AuthTokenCipher

Regards

Hi everybody,

just found a great blog post: “Mimimal API Authentication on Rails” (http://resistor.io/blog/2013/08/07/mimimal-api-authentication-on-rails) by @jamesotron. A demo application is also available: https://github.com/jamesotron/Blogomat.

Thanks for the gist here. Very useful and informative. Especially for someone who is new to Ruby on Rails.

@dnagir and @danielfone
I think the use of email+token makes the system more secure when it has more number of users.
Lets say, X is the probability that a random string generated by a hacker is the same as the one in your system as token when you have only 10 users.
And Y is the probability of the same string in your system when you have 10 million of users.

Common sense would suggest that Y >>X by an order of 1 million.

Having email used would secure a case where hacker would be accidentally able to generate a correct token.

No, @shklnrj, that's not the type of attack the email+token combo is protecting.

It is protecting the token look-up from Timing Attacks.

Hi guys,

I am new to this i have applied the codes to my project but i tried to call the following
http://localhost:3000/users/sign_in?email=a@a.com&password=1234567

I got the following returned ActionController::InvalidAuthenticityToken at /users/sign_in

Not sure what's wrong. If anyone could point me in the right direction will be great!

Hi @gohlance,

Of course I'm biased, but I think you should take a look at the Simple Token Authentication gem.
It aims at making this gist implementation easier and, if things remain unclear, questions are welcome the issues ; )

Regards!

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.