Skip to content

Instantly share code, notes, and snippets.

@josevalim
Last active December 18, 2023 10:57
Star You must be signed in to star a gist
Save josevalim/fb706b1e933ef01e4fb6 to your computer and use it in GitHub Desktop.
# 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
# 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
@ahacking
Copy link

ahacking commented Nov 8, 2013

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.

@gonzalo-bulnes
Copy link

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!

@rusllonrails
Copy link

Thanks to gonzalo-bulnes for nice gist https://gist.github.com/gonzalo-bulnes/7659739

@gonzalo-bulnes
Copy link

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,

@kreigne
Copy link

kreigne commented Dec 18, 2013

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.

@redhoodie
Copy link

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

@mkempe
Copy link

mkempe commented Jan 13, 2014

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.

@shuklaneerajdev
Copy link

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.

@guilhermesimoes
Copy link

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.

@gohlance
Copy link

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!

@gonzalo-bulnes
Copy link

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!

@joelcdoyle
Copy link

This works! I had to remove the devise method

before_filter :authenticate_user!

and do my own rendering of 401 response.

@mezerny
Copy link

mezerny commented Jun 5, 2014

Do I understand correctly, that if I want to use both token authentication and :timeoutable extension, I need to remove store: false from sign_in method? Otherwise, token expiration doesn't work.
What should I do, to enable both token expiration and also require token on every request?

@colllin
Copy link

colllin commented Jun 23, 2014

@joelcdoyle I kept before_filter :authenticate_user! for my web actions but rendered a 401 within def authenticate_user_from_token! if I believed they were attempting to token-authenticate (params[:user_email].present? && params[:user_token].present?).

@mattwarrenrnp
Copy link

this gist ensures that every user has a password (token) stored in the database in plain text!! If someone gained read access to the DB they would have access to all accounts.

@lstone
Copy link

lstone commented Jul 25, 2014

The gem does not work for me on Rails 4.1.4. I always get a 401.

@zph
Copy link

zph commented Oct 31, 2014

Yep, after discussing it w/ colleagues, treat the token like a password, ie hash it securely before storage. Then when user provides token, hash that to compare against what's stored in DB. This means that we can't retrieve the original token, only regenerate a new one on request. Like passwords, we shouldn't be able to retrieve a token once it's created and shown to user.

This avoids the possibility of someone compromising DB and knowing the real token values (unhashed).

@timolehto
Copy link

But if you do the find with an email then doesn't this open up a similar possibility to do timing attacks to figure out which emails have registered to the service? That alone wouldn't let him in ofc., but it's just one password short of getting in? The id seems much better idea in this sense 'cos it is mostly worthless information. Best option would be to use something more or less public, like a unique slug key or id of the user that's used on the users profile pages path (obviously applies only to services where users have public profiles). If that's not an option then I'd just have a short, unique, random string as the find the user to log in key. Third option would be to mess around with some artificial delays as someone suggested here.

@Manju244
Copy link

Nice to see this post. Working perfectly with single devise. The scenario is, When Two devices(ios, Andriod) logged-in and after some time one device(ios) logout from app then auth_token becomes NULL. So i will get an error on other device. It will ask for authentication and the app will get crashed.

How to overcome this scenario with existing code? And any valid suggestion and Code stuff to solve this problem?

@williamnoto
Copy link

Manju244, maybe add some aspect of the device to your user session model, e.g., UUID or something, so logging out only logs you out on that device.

@gonzalo-bulnes
Copy link

Hello!

@lstone, @Manju244 and everyone in the same case:

I do review this conversation from time to time but I also believe it is not the better place to discuss Simple Token Authentication usage. You may find responses in the gem FAQ. But if you don't, please feel welcome to open new issues, your comments will certainly help us all to perform token authentication correctly!

@milesmatthias
Copy link

the sign_in(user, store:false) call is still creating a session cookie for me. I've opened a ticket in warden, since calling the low level warden.set_user with store: false doesn't work for me either: wardencommunity/warden#110

@brianhempel
Copy link

One option I haven't seen suggested yet is to do the DB query with only part of the token, then do a secure compare with all the records that are returned. An attacker will be able to recover part of the token, but not enough to actually sign in. If your tokens are 128bits and you look up users by only the first 24bits of the token, that still leaves 104bits that the attacker has to guess. Not a feasible attack for an HTTP service.

@tadas-s
Copy link

tadas-s commented Jun 24, 2015

If I used Trackable module, wouldn't this trigger sign in event + updates to sign_in_count and the rest of the trackable properties?

sign_in user, store: false

@aruprakshit
Copy link

I used this link also with the steps shown here.

@fastdivision
Copy link

@tadas-s Yes, if you're using the Trackable module you can skip it like so:

env['devise.skip_trackable'] = true
sign_in user, store: false

@melsatar
Copy link

melsatar commented Jun 28, 2017

Hi guys,

I added a token reset after successful sign in so, it will be always new token assigned after the login in which will make it more secure, what do you think?

	def authenticate_user_from_token!
		if !current_user.nil?
		
		else
			user_email = params[:user_email].presence
			user = user_email && User.find_by_email(user_email)
			if params[:user_token].present?
				if user && Devise.secure_compare(user.authentication_token, params[:user_token])
					sign_in :user, user
					current_user.update(:authentication_token => Devise.friendly_token)
				else
					respond_to do |format|
		    			format.html { redirect_to "/authentication_failure", :layout => false, status: 301 }
		    		end					
				end
			end
		end	
	end

@StMacLennan
Copy link

StMacLennan commented Jul 7, 2017

This is super cool thanks! I made a few changes, as I want tokens to be generated on user creation, and then refreshed or replaced (depending how old the token is) when I send a transactional email with a auth_tokened link. So I created a custom lib with the token generator, added a token lifetime check to see when it was activated and a custom error message and login redirect for when the user has the right token but it's old. I decided to do this database side, as if you include the validation date in the token itself, the user could effectively have 2 reminder emails with different tokens (one working, and one not). So when I send an email with the token, i'll refresh or replace it depending on it's age.

My tweaked code:

/lib/users/authentication_token.rb

class Users::AuthenticationToken
  attr_accessor :user

  def initialize(user)
    @user = user
  end

  def generate_authentication_token
    new_token(@user)
    activate_token(@user)
    @user.save
  end

  def reactivate_authentication_token
    activate_token(@user)
    @user.save
  end

  private
  def new_token(user)
    user.auth_token = Devise.friendly_token
  end

  def activate_token(user)
    user.auth_token_activated_at = DateTime.now
  end
end

controllers/application_controller

def authenticate_user_from_token!
  user_token = params[:user_token].presence
  user       = user_token && User.find_by_auth_token(user_token.to_s)

  if user
    activation_date = User.find_by_auth_token(user_token.to_s).auth_token_activated_at
    if (DateTime.now.to_date - activation_date.to_date).to_i <= 2
      sign_in user
    else
      flash[:warning] = "Please sign in to continue"
      redirect_to(request.referrer || new_user_session_path)
    end
  end
end

models/user

before_save :ensure_authentication_token

def ensure_authentication_token
    if auth_token.blank?
      tokenizer = Users::AuthenticationToken.new(self)
      tokenizer.generate_authentication_token
    end
end

Basically I can then include a custom method in my mailers controllers to check the age of the users token, and then replace or refresh it before sending an email.

I'll probably edit this authentication to include the email and encrypt/decrypt the whole token (as suggested above) when using it with users. But thought I'd share this in the mean time! Hope it helps someone!

@briankung
Copy link

With 2_safe_token_authentication.rb, is it less secure to just encode the user email / user token combo to make a mega token for the API client to use? Something like this:

# Before giving the "public" API token to the user:

email_and_token = "#{user.email}:#{user.internal_token}" #=> "me@example.com:abc123"

public_token = Base64.encode64(email_and_token)

# When receiving public token from parameters (I am also considering using ActiveSupport::MessageEncryptor instead of simply Base64 encoding):

decoded_token = Base64.decode64(params[:public_token]) #=> "me@example.com:abc123"

user_email, user_token = decoded_token.split(':') # Naive - this will introduce errors if, for example, there's no colon
user       = user_email && User.find_by_email(user_email)

if user && Devise.secure_compare(user.authentication_token, user_token)
  sign_in user, store: false
end

Actually, I this seems similar to what @ahacking was proposing? Speaking of which, according to this security stackexchange post, adding a random delay isn't an effective strategy for dealing with timing attacks: https://security.stackexchange.com/questions/96489/can-i-prevent-timing-attacks-with-random-delays

@remy727
Copy link

remy727 commented Jan 3, 2022

We can refactor this line:
break token unless User.where(authentication_token: token).first
to
break token unless User.exists?(authentication_token: token)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment