Skip to content

Instantly share code, notes, and snippets.

@josevalim
Last active August 15, 2024 15:12
Show Gist options
  • Save josevalim/fb706b1e933ef01e4fb6 to your computer and use it in GitHub Desktop.
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
@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}" #=> "[email protected]: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]) #=> "[email protected]: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