I am building a small API that uses basic authentication. What I have done, is that a user can generate a username and password, that could be used to authenticate to the API.
However I have discovered that it is not working 100% as intended. It appears that a request will slip through even though the username is not matching the password. However it will be blocked if the password is incorrect.
Example:
Username: foo
Password: bar
Works:
curl -u foo:bar http://api/protected
Works - But should not:
curl -u f:bar http://api/protected
Does not work - As intended:
curl -u foo:b http://api/protected
To me it appears that it only validates the password, and ignores the username. Here are some code.
# Authentication
protected
def authenticate
authenticate_or_request_with_http_basic do |username, password|
#user = User.includes(:keys).find_by( :keys => { :api_key => password } )
#user.keys.each do |key|
username == key.api_id && password == key.api_key
end
end
end
#user.keys.each do |key|
username == key.api_id && password == key.api_key
end
This piece of code returns a value of .each, which is the collection it's called on (#user.keys in this case). As it is a truthy value, the check will pass always, regardless of what are the results of evaluating your conditional in the block.
What you intended to ask is "Do any of user's keys match these credentials?". Use the appropriate method, Enumerable#any?
#user.keys.any? do |key|
username == key.api_id && password == key.api_key
end
Documentation: Enumerable#any?
Try this
#user = User.includes(:keys).find_by( :keys => { :api_key => password, :api_id => username } )
This way you'll make sure to find a user with valid both username and password, and you won't need the second line (iterating over keys).
The reason for that is: iterating over a block returns this block, not the value/values inside. So your authenticator found a user with a good password, iterated over his keys (which changed nothing) and returned #user.keys, which evaluates to true. Thus the authentication passed.
Related
I'm pretty new to Rails but we have an app using Devise and Omniauth for authentication and have recently integrated Omniauth-Saml by following the Omniauth documentation for Devise integration: https://github.com/omniauth/omniauth-saml#devise-integration The authentication works and we can create users and use these accounts without any issues.
In the SAML response attributes is an lacode (4-digit string). We want to check this user attribute against a reference lacode. If their cag matches the reference cag we want to set the verified_at attribute in the user.rb model.
I've updated the user model and to test if I set the oauth_lacode to "9064" to match the oauth_lacode_ref then the code works and the user's verified_at time and date are set at point of account creation.
app/models/user.rb
# Get the existing user by email if the provider gives us a verified email.
def self.first_or_initialize_for_oauth(auth)
oauth_email = auth.info.email
oauth_email_confirmed = oauth_email.present? && (auth.info.verified || auth.info.verified_email)
oauth_lacode = auth.extra.raw_info.lacode
oauth_lacode_ref = "9064"
oauth_lacode_confirmed = oauth_lacode == oauth_lacode_ref
oauth_user = User.find_by(email: oauth_email) if oauth_email_confirmed
oauth_user || User.new(
username: auth.info.name || auth.uid,
email: oauth_email,
oauth_email: oauth_email,
password: Devise.friendly_token[0, 20],
terms_of_service: "1",
confirmed_at: oauth_email_confirmed ? DateTime.current : nil,
verified_at: oauth_lacode_confirmed ? DateTime.current : nil
)
end
I'm not mapping and calling the lacode from the hash correctly as I see this error in the log "NoMethodError (undefined method `lacode' for #OneLogin::RubySaml::Attributes:0x00007f7a5040ad40):"
This is how I'm mapping the attributes in config/initializers/devise.rb
attribute_statements: { email: ['urn:oid:0.9.2342.19200300.100.1.22'],
lacode: ['urn:oid:0.9.2342.19200300.100.1.17']}
I have confirmed with the IDP that 'urn:oid:0.9.2342.19200300.100.1.17' is mapped to the lacode in the SAML response.
As in the User model above, this is how I'm trying to access the lacode from within the User model.
"saml_cag = auth.extra.raw_info.lacode"
This is the guidance from Omniauth Saml:
:attribute_statements - Used to map Attribute Names in a SAMLResponse
to entries in the OmniAuth info hash. For example, if your
SAMLResponse contains an Attribute called 'EmailAddress', specify
{:email => ['EmailAddress']} to map the Attribute to the corresponding
key in the info hash. URI-named Attributes are also supported, e.g.
{:email =>
['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']}.
Note: All attributes can also be found in an array under
auth_hash[:extra][:raw_info], so this setting should only be used to
map attributes that are part of the OmniAuth info hash schema.
Does this sentence at the end mean I don't need to/can't map the attribute. Can anyone help or point me in the right direction?
I managed to get this working. Only attribute names specified in the Omniauth Hash Schema can be used.
Mapping the lacode to description in the attribute statement, I was able to access it using "auth.info.description"
I'm tryig to get the user address from facebook with Omniauth but did not work.
i added their address on update callback after login.
If i removed their address from omniauth the app did not update their address.
Someone have any idea how to get their address and why the app did not edit and update their address after the login?
thank's
def omniauth_callback
auth_hash = request.env['omniauth.auth']
user = User.find_by_uid(auth_hash[:uid])
if user.nil? && auth_hash[:info][:email]
user = User.find_by_email(auth_hash[:info][:email])
end
if user.nil?
email_domain = ''
email_domain = '#facebook.com' if auth_hash[:provider] == 'facebook'
user = User.new(email: auth_hash[:info][:email] || auth_hash[:info][:nickname] + email_domain, name: auth_hash[:info][:first_name] || '', surname: auth_hash[:info][:last_name] || '', gender: 'I')
user.password_digest = ''
user.save!(validate: false)
end
user.update_attribute(:login_at, Time.zone.now)
user.update_attribute(:address)
user.update_attribute(:neighborhood)
user.update_attribute(:postal_code)
user.update_attribute(:ip_address, request.remote_ip)
user.update_attribute(:provider, auth_hash[:provider])
user.update_attribute(:uid, auth_hash[:uid])
user.update_attribute(:oauth_token, auth_hash[:credentials][:token])
user.update_attribute(:oauth_expires_at, Time.at(auth_hash[:credentials][:expires_at]))
cookies[:auth_token] = { value: user.oauth_token, expires: user.oauth_expires_at}
redirect_to root_url
end
One reason your code will not work is because this
user.update_attribute(:address)
Doesn't do anything - except raise an error. You have to pass a value into update_attribute as well as specify the field.
Also as #mysmallidea points out, you'd be better advised to use update as that will allow you to update multiple fields in one database action.
If present, the address data will be within the auth_hash. So I suggest that you first work out the structure of that hash. In your development environment, add the following:
Rails.logger.info auth_hash.inspect
That will output the current auth_hash to logs/development.log. Use that to determine where in the hash the address data is. You'll then be able to do something like:
user.update address: auth_hash[:info][:address]
However, you may find that the address is not included in the data returned by the facebook oauth system. In which case you will need to return to their documentation to see if this is possible.
my users have the option to add their website, facebook and twitter URL's to their profile.
I want to let them enter either the full URL (http://www.facebook.com/USERNAME) or part of the URL Eg. www.facebook.com/USERNAME or just USERNAME, and then have the https://facebook.com/ added automatically if needed. I want the http:// as then the entered URL will link directly to their website/facebook etc.
For the website URL I have:
before_validation :add_url_protocol
def add_url_protocol
if self.website && !url_protocol_present?
self.website = "http://#{self.website}"
end
end
def url_protocol_present?
self.website[/\Ahttp:\/\//] || self.website[/\Ahttps:\/\//]
end
There is then further regex validation.
This works fine.
The thing is I don't have much of an idea about regex and I am unsure on how to add the facebook.com/ part to this before_validation code.
Any help would be greatly appreciated, thanks.
UPDATE:
def add_url_protocol
if self.website && !url_protocol_present?
self.website = "http://#{self.website}"
end
if self.facebook && !url_facebook_present?
self.facebook = "http://facebook.com/#{self.facebook}"
end
end
This almost works. If a user inputs USERNAME then the output is good. If the user inputs www.facebook.com/USERNAME then the ouput becomes http://facebook.com/www.facebook.com/USERNAME
The best way to do this would be to collect user input in a form model, you can roll your own with by including ActiveModel::Model or use something like Reform.
The easiest thing you can do is simply treat their input as a string containing their facebook username separated by "/" so whatever the string they enter you can get the username by
"https://www.facebook.com/their.username".split('/')[-1] # 'their.username'
"www.facebook.com/their.username".split('/')[-1] # 'their.username'
"their.username".split('/')[-1] # 'their.username'
Simply declare a username attribute in your form model and overwrite the setter to extract the facebook username. Then only save the facebook username in your database, and write a method such as
def facebook_profile_url
"www.facebook.com/#{fb_username}"
end
No need to persist the redundant facebook url part.
I would try
match = /^(https?:\/\/)?((www\.)?facebook\.com)?(.*)/.match(self.website)
self.website = match[1] || 'http://'
self.website << match[2] || 'facebook.com'
self.website << match[3]
You can create capture groups by using parentheses. The ? means that group or char before it is optional (so I was able to condense your protocol search)
You may need to check my indexes into the match array...
Sorry this is untested as it is from a phone...
How can l implement a hash function (SHA512) that encrypt the username and check if this username and password is correct in the SQL server?
Working login right now if you set a cleartext username
user = User.find_by(username: params[:session][:username].downcase)
if user && user.authenticate(params[:session][:password])
log_in user
redirect_to :root
else
flash.now[:warning] = 'Wrong username/password'
render 'new'
end
I have tried encrypt the username before it runs the if statement but I can get it to work.
The way I hash the username is:
username = params[:session][:username].downcase
username_encrypted = Digest::SHA2.new(512).hexdigest(username)
any ideas how this can be made?
What i am trying to achieve with this is. Too have one column in SQL with encrypted usernames and another column with "public" usernames. Where public usernames is visible for everybody on the site and encrypted usernames is stored in SQL only for authentication.
I would like to create rake task to set the username of all users' without a username to the part before the '#' in their email address. So if my email is test#email.eu, my username should become test. If it's not available, prepend it by a number (1).
So i have problem witch checking uniqness of username. Code below isn`t working after second loop ex: when i have three emails: test#smt.com, test#smt.pl, test#oo.com username for test#oo.com will be empty.
I have of course uniqness validation for username in User model.
desc "Set username of all users wihout a username"
task set_username_of_all_users: :environment do
users_without_username = User.where(:username => ["", nil])
users_without_username.each do |user|
username = user.email.split('#').first
users = User.where(:username => username)
if users.blank?
user.username = username
user.save
else
users.each_with_index do |u, index|
pre = (index + 1).to_s
u.username = username.insert(0, pre)
u.save
end
end
end
end
Other ideas are in Gist: https://gist.github.com/3067635#comments
You could use a simple while loop for checking the username:
users_without_username = User.where{ :username => nil }
users_without_username.each do |user|
email_part = user.email.split('#').first
user.username = email_part
prefix = 1
while user.invalid?
# add and increment prefix until a valid name is found
user.username = prefix.to_s + email_part
prefix += 1
end
user.save
end
However, it might be a better approach to ask the user to enter a username upon next login.
if i understand your code correct, you are changing the username of existing users in the else branch? that does not look as if it's a good idea.
you should also use a real finder to select your users that don't have a username. otherwise you will load all the users before selecting on them.
i don't know if it "matches your requirements" but you could just put a random number to the username so that you do not have the problem of duplicates.
another thing that you can use is rubys retry mechanism. just let active-record raise an error and retry with a changed username.
begin
do_something # exception raised
rescue
# handles error
retry # restart from beginning
end
In your query User.find_by_username(username), you only expect 1 record to be provided. So you don't need any each. You should add your index in another way.