loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
loop do
token = Devise.friendly_token
break token unless self.class.unscoped.where(authentication_token: token).first
end
I'm a newbie to ruby and it's rails so, hoping you guys will not downvote my question, since I google around but could not understand between differences.
The difference is User.where vs self.class.unscoped. They do the same thing essentially.
In the first construct the class name User is hardcoded into the expression. So the construct will only call where on the User class. And the User class has to respond to the where message; it must have a class method called where.
In the second construct the class name is derived. In Ruby, self refers to the instance you are currently working within. If you call self.class inside an instance, you get the class name of the instance. For example:
apple = Apple.new
puts apple.class.name
# => "Apple"
So the second construct can be used inside any class.
The last difference is the unscoped call. This is an ActiveRecord method that removes any defined scopes on a class. Think of it like "remove any filters" before finding records where authentication_token equals token.
I believe unscoped was deprecated.
Given you are in the class User e.g. in user.rb self.class maps to User. So the difference is actually another scope added to the where clause:
class User
def some_method_to_find_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
def some_other_method_to_find_token
loop do
token = Devise.friendly_token
break token unless User.unscoped.where(authentication_token: token).first
end
end
end
I guess the intention is to create a new token if no token exists. And there probably is a default scope in place which probably filters out deleted_users. Not a really good coding example actually. The first loop is unnecessary as you always have to check if this token is set to a deleted user anyways.
class User
def new_untaken_token
loop do
token = Devise.friendly_token
break token unless User.unscoped.exists?(authentication_token: token)
end
end
end
This could also be written recursivly:
class User
def new_untaken_token
token = Devise.friendly_token # Create a new token
if User.unscoped.exists?(authentication_token: token) # check if it exists
new_untaken_token # find another one
else
token # use token
end
end
end
Related
I'm trying to monkey patch ActiveRecord::FinderMethods in order to use hashed ids for my models. So for example User.find(1) becomes User.find("FEW"). Sadly my overwritten method doesn't get called. Any ideas how to overwrite the find_one method?
module ActiveRecord
module FinderMethods
alias_method :orig_find_one, :find_one
def find_one(id)
if id.is_a?(String)
orig_find_one decrypt_id(id)
else
orig_find_one(id)
end
end
end
end
Here's an article that discusses how to actually do what you want by overriding the User.primary_key method like:
class User
self.primary_key = 'hashed_id'
end
Which would allow you to call User.find and pass it the "hashed_id":
http://ruby-journal.com/how-to-override-default-primary-key-id-in-rails/
So, it's possible.
That said, I would recommend against doing that, and instead using something like User.find_by_hashed_id. The only difference is that this method will return nil when a result is not found instead of throwing an ActiveRecord::RecordNotFound exception. You could throw this manually in your controller:
def show
#user = User.find_by_hashed_id(hashed_id)
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
Finally, one other note to make this easier on you -- Rails also has a method you can override in your model, to_param, to tell it what property to use when generating routes. By default, of course, it users the id, but you would probably want to use the hashed_id.
class User
def to_param
self.hashed_id
end
end
Now, in your controller, params[:id] will contain the hashed_id instead of the id.
def show
#user = User.find_by_hashed_id(params[:id])
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
I agree that you should be careful when doing this, but it is possible.
If you have a method decode_id that converts a hashed ID back to the original id, then the following will work:
In User.rb
# Extend AR find method to allow finding records by an encoded string id:
def self.find(*ids)
return super if ids.length > 1
# Note the short-circuiting || to fall-back to default behavior
find_by(id: decode_id(ids[0])) || super
end
Just make sure that decode_id returns nil if it's passed an invalid hash. This way you can find by Hashed ID and standard ID, so if you had a user with id 12345, then the following:
User.find(12345)
User.find("12345")
User.find(encode_id(12345))
Should all return the same user.
I'm trying to write a custom (remote) authentication for devise.
All the API doc I've found is this example, so I'm proceding by trials and errors.
I'm particularly interested in understanding what does the 'mapping.to.new' line do.
It seems to be crucial since if it returns nil, the authentication process will fail.
But what are those "mappings", where are they defined?
Furthermore, the call
mapping.to.new
has something strange, it seems like an object instantiation... isn't it?
I've also found a different implementation, that looks like:
resource = mapping.to.where(["username = ?", auth_params[:username]]).first
where it seems that mapping.to returns a relation object, but again, where am i expected to define what my mappings are?
class RemoteAuthenticatable < Authenticatable
def authenticate!
auth_params = authentication_hash
auth_params[:password] = password
resource = mapping.to.new
return fail! unless resource
if validate(resource){ resource.remote_authentication(auth_params) }
success!(resource)
end
end
end
mapping.to is instance to class of model on which you map authentification ( often is a User or Admin).
So if you call mapping.to.new is the same as you call User.new.
And if you call mapping.to.where(...) it return same result as User.where(...).
More you can find on mapping.rb
With Ruby on Rails, my models are being created with increasing unique ids. For example, the first user has a user id of 1, the second 2, the third 3.
This is not good from a security perspective because if someone can snoop on the user id of the last created user (perhaps by creating a new user), they can infer your growth rate. They can also easily guess user ids.
Is there a good way to use random ids instead?
What have people done about this? Google search doesn't reveal much of anything.
I do not consider exposing user IDs to public as a security flaw, there should be other mechanisms for security. Maybe it is a "marketing security flaw" when visitors find out you do not have that million users they promise ;-)
Anyway:
To avoid IDs in urls at all you can use the user's login in all places. Make sure the login does not contain some special characters (./\#? etc.), that cause problems in routes (use a whitelist regex). Also login names may not be changed later, that can cause trouble if you have hard links/search engine entries to your pages.
Example calls are /users/Jeff and /users/Jeff/edit instead of /users/522047 and /users/522047/edit.
In your user class you need to override the to_param to use the login for routes instead of the user's id. This way there is no need to replace anything in your routes file nor in helpers like link_to #user.
class User < ActiveRecord::Base
def to_param
self.login
end
end
Then in every controller replace User.find by User.find_by_login:
class UsersController < ApplicationController
def show
#user = User.find_by_login(params[:id])
end
end
Or use a before_filter to replace the params before. For other controllers with nested resources use params[:user_id]:
class UsersController < ApplicationController
before_filter :get_id_from_login
def show
#user = User.find(params[:id])
end
private
# As users are not called by +id+ but by +login+ here is a function
# that converts a params[:id] containing an alphanumeric login to a
# params[:id] with a numeric id
def get_id_from_login
user = User.find_by_login(params[:id])
params[:id] = user.id unless user.nil?
end
end
Even if you would generate random INTEGER id it also can be compromted very easy. You should generate a random token for each user like MD5 or SHA1 ("asd342gdfg4534dfgdf"), then it would help you. And you should link to user profile with this random hash.
Note, this is not actually the hash concept, it just a random string.
Another way is to link to user with their nick, for example.
However, my guess is knowing the users ID or users count or users growth rate is not a vulnerability itself!
Add a field called random_id or whatever you want to your User model. Then when creating a user, place this code in your UsersController:
def create
...
user.random_id = User.generate_random_id
user.save
end
And place this code in your User class:
# random_id will contain capital letters and numbers only
def self.generate_random_id(size = 8)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
key = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if random_id exists in database, regenerate key
key = generate_random_id(size) if User.find_by_random_id(key)
# output the key
return key
end
If you need lowercase letters too, add them to alphanumerics and make sure you get the correct random number from the kernel, i.e. Kernel.rand(62).
Also be sure to modify your routes and other controllers to utilize the random_id instead of the default id.
You need to add a proper authorization layer to prevent un-authorized access.
Let us say you you display the user information in show action of the Users controller and the code is as shown below:
class UsersController < ActionController::Base
before_filter :require_user
def show
#user = User.find(params[:id])
end
end
This implementation is vulnerable to id guessing. You can easily fix it by ensuring that show action always shows the information of the logged in user:
def show
#user = current_user
end
Now regardless of what id is given in the URL you will display the current users profile.
Let us say that we want to allow account admin and account owner to access the show action:
def show
#user = current_user.has_role?(:admin) ? User.find(params[:id]) : current_user
end
OTH authorization logic is better implemented using a gem like CanCan.
Here's what I'm using. The token doesn't necessarily have to be heard to guess, it's more like a short url identifier than anything else, and I want to keep it short. I've followed some examples I've found online and in the event of a collision, I think the code below will recreate the token, but I'm not real sure. I'm curious to see better suggestions, though, as this feels a little rough around the edges.
def self.create_token
random_number = SecureRandom.hex(3)
"1X#{random_number}"
while Tracker.find_by_token("1X#{random_number}") != nil
random_number = SecureRandom.hex(3)
"1X#{random_number}"
end
"1X#{random_number}"
end
My database column for the token is a unique index and I'm also using validates_uniqueness_of :token on the model, but because these are created in batches automatically based on a user's actions in the app (they place an order and buy the tokens, essentially), it's not feasible to have the app throw an error.
I could also, I guess, to reduce the chance of collisions, append another string at the end, something generated based on the time or something like that, but I don't want the token to get too long.
-- Update EOY 2022 --
It's been some time since I answered this. So much so that I've not even taken a look at this answer for ~7 years. I have also seen this code used in many organizations that rely on Rails to run their business.
TBH, these days I wouldn't consider my earlier solution, or how Rails implemented it, a great one. Its uses callbacks which can be PITA to debug and is pessimistic 🙁 in nature, even though there is a very low chance of collision for SecureRandom.urlsafe_base64. This holds true for both long and short-lived tokens.
What I would suggest as a potentially better approach is to be optimistic 😊 about it. Set a unique constraint on the token in the database of choice and then just attempt to save it. If saving produces an exception, retry until it succeeds.
class ModelName < ActiveRecord::Base
def persist_with_random_token!(attempts = 10)
retries ||= 0
token = SecureRandom.urlsafe_base64(nil, false)
save!
rescue ActiveRecord::RecordNotUnique => e
raise if (retries += 1) > attempts
Rails.logger.warn("random token, unlikely collision number #{retries}")
token = SecureRandom.urlsafe_base64(16, false)
retry
end
end
What is the result of this?
One query less as we are not checking for the existence of the token beforehand.
Quite a bit faster, overall because of it.
Not using callbacks, which makes debugging easier.
There is a fallback mechanism if a collision happens.
A log trace (metric) if a collision does happen
Is it time to clean old tokens maybe,
or have we hit the unlikely number of records when we need to go to SecureRandom.urlsafe_base64(32, false)?).
-- Update --
As of January 9th, 2015. the solution is now implemented in Rails 5 ActiveRecord's secure token implementation.
-- Rails 4 & 3 --
Just for future reference, creating safe random token and ensuring it's uniqueness for the model (when using Ruby 1.9 and ActiveRecord):
class ModelName < ActiveRecord::Base
before_create :generate_token
protected
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless ModelName.exists?(token: random_token)
end
end
end
Edit:
#kain suggested, and I agreed, to replace begin...end..while with loop do...break unless...end in this answer because previous implementation might get removed in the future.
Edit 2:
With Rails 4 and concerns, I would recommend moving this to concern.
# app/models/model_name.rb
class ModelName < ActiveRecord::Base
include Tokenable
end
# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
protected
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless self.class.exists?(token: random_token)
end
end
end
Ryan Bates uses a nice little bit of code in his Railscast on beta invitations. This produces a 40 character alphanumeric string.
Digest::SHA1.hexdigest([Time.now, rand].join)
This might be a late response but in order to avoid using a loop you can also call the method recursively. It looks and feels slightly cleaner to me.
class ModelName < ActiveRecord::Base
before_create :generate_token
protected
def generate_token
self.token = SecureRandom.urlsafe_base64
generate_token if ModelName.exists?(token: self.token)
end
end
There are some pretty slick ways of doing this demonstrated in this article:
https://web.archive.org/web/20121026000606/http://blog.logeek.fr/2009/7/2/creating-small-unique-tokens-in-ruby
My favorite listed is this:
rand(36**8).to_s(36)
=> "uur0cj2h"
If you want something that will be unique you can use something like this:
string = (Digest::MD5.hexdigest "#{ActiveSupport::SecureRandom.hex(10)}-#{DateTime.now.to_s}")
however this will generate string of 32 characters.
There is however other way:
require 'base64'
def after_create
update_attributes!(:token => Base64::encode64(id.to_s))
end
for example for id like 10000, generated token would be like "MTAwMDA=" (and you can easily decode it for id, just make
Base64::decode64(string)
This may be helpful :
SecureRandom.base64(15).tr('+/=', '0aZ')
If you want to remove any special character than put in first argument '+/=' and any character put in second argument '0aZ' and 15 is the length here .
And if you want to remove the extra spaces and new line character than add the things like :
SecureRandom.base64(15).tr('+/=', '0aZ').strip.delete("\n")
Hope this will help to anybody.
Try this way:
As of Ruby 1.9, uuid generation is built-in. Use the SecureRandom.uuid function.
Generating Guids in Ruby
This was helpful for me
you can user has_secure_token https://github.com/robertomiranda/has_secure_token
is really simple to use
class User
has_secure_token :token1, :token2
end
user = User.create
user.token1 => "44539a6a59835a4ee9d7b112b48cd76e"
user.token2 => "226dd46af6be78953bde1641622497a8"
To create a proper, mysql, varchar 32 GUID
SecureRandom.uuid.gsub('-','').upcase
def generate_token
self.token = Digest::SHA1.hexdigest("--#{ BCrypt::Engine.generate_salt }--")
end
I think token should be handled just like password. As such, they should be encrypted in DB.
I'n doing something like this to generate a unique new token for a model:
key = ActiveSupport::KeyGenerator
.new(Devise.secret_key)
.generate_key("put some random or the name of the key")
loop do
raw = SecureRandom.urlsafe_base64(nil, false)
enc = OpenSSL::HMAC.hexdigest('SHA256', key, raw)
break [raw, enc] unless Model.exist?(token: enc)
end
I want to make simple authentication like
# SessionController
require 'session.rb'
def create
Session.new(params[:email], params[:password])
end
# lib/session.rb
class Session
def initialize(email, password)
client = Client.find_by_email_and_password(email, password)
cookies[:user_id] = client.id if client
end
end
only problem here is that I can't use cookies or sessions out of controller. I can replace cookies setting into controller, but it isn't what I want :)
Question is how to use cookies out of controller and what are best practices.
Firstly, don't explicitly add '.rb' to a require statement. You'd just use require 'session' normally. Second, Rails will load it for you when you reference Session, so you don't need to require it explicitly. Also, client ? cookies[:user_id] = client.id is invalid Ruby code. You probably want cookies[:user_id] = client.id if client.
An obvious solution would be to simply pass the cookies object to the Session you're creating, but my recommendation would be to simply put the code in the controller. Why do you have a Session object in the first place? I'd recommend not overcomplicating things by creating an unnecessary class.
Update:
To illustrate my last comment, here's some sample code:
class Session
attr_reader :controller
def initialize(controller)
#controller = controller
end
def client
if controller.cookies[:user_id]
return Client.find_by_id(controller.cookies[:user_id])
elsif (email = controller.params[:email]) && (password = controller.params[:password])
client = Client.find_by_email_and_password(email, password)
controller.cookies[:user_id] = client.id if client
return client
end
end
end