-
-
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 |
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.
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?
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.
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!
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
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.
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
I used this link also with the steps shown here.
@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
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
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!
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
We can refactor this line:
break token unless User.where(authentication_token: token).first
to
break token unless User.exists?(authentication_token: token)
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).