Omniauth-Saml with Devise - mapping and using an attribute - ruby-on-rails

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"

Related

Rails Devise token generator with service objects

I am working on a fairly large Rails app which due to large no of activities uses service objects (& light-services gem). Typically actions are performed in steps in the services, with many of the functions/objects used in these steps instantiated in the model it is working with (Customer.rb) in this case.
In one step I am trying to generate a Devise reset_password_token, but hit a glitch
Customer.rb
def self.generate_token
Devise.token_generator
end
and then in service object reset_password.rb
def generate_token
raw, hashed = Devise.token_generator.generate(Customer, :reset_password_token)
producer.reset_password_token = hashed
producer.reset_password_token_sent_at = Time.now.utc
#token = raw
end
All that goes in to db is the timestamp, and a nil, nil for token although token gets put in to the url endpoint so is being generated
If anyone encountered this scenario before, tips more than welcome!
Here is rest of code, not setting the token in DB properly.
def validate_producer_email
self.valid_email = EmailValidator.valid?(producer.email)
end
# #return [TrueClass, FalseClass]
def validate_token
self.producer.reset_password_token = reset_password_token, params['reset_password_token']
customer = Customer.producer.find_by(reset_password_token: reset_password_token)
# true if customer.email && Devise.secure_compare(reset_password_token, params[:reset_password_token])
customer.present?
end
def update_producer
return if ask_underwriter?
Producer::Update
.with(self)
.run(
producer: producer,
force: current_administrator.blank?,
params: {
customer: {
reset_password_token: reset_password_token
}
}
)
end
If anyone has any tips on how to fix?
Thanks

Rails Facebook Omniauth get user address

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.

Ruby on rails. Add facebook.com/ to URL if not present

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...

Rails basic auth not working properly

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.

interning empty string error

Currently, in my request model I have:
belongs_to :requestor, :class_name => 'User'
So the requestor is the current_user.
Problem is when the current_user clicks the create button to submit a form for a request, all of the attributes get updated to the database that are in the form.
But since requestor_id is not a value to fill out in the form, that brings back a value of null in the database when the new request record is created.
What I want is an integer (which equates to the primary key of the Users table) updated in the requestor_id column in the request table when the user clicks the create button.
So I thought that maybe adding a requestor_id as a symbol in the params for the create action would solve that:
def create_common
#a = Request.new
b = #a.requestor_id
#resource = yield params[:contact + "#{b}".to_sym]
self.resource = #resource
But instead it returns the following error:
interning empty string
Thanks for any suggestions.
I kept getting this error. I traced it to very simple code and reproduced in the console with this:
ruby-1.8.7-p174 :084 > a = 'fred'
=> "fred"
ruby-1.8.7-p174 :085 > a.to_sym
=> :fred
ruby-1.8.7-p174 :086 > a = ''
=> ""
ruby-1.8.7-p174 :087 > a.to_sym
ArgumentError: interning empty string
from (irb):87:in `to_sym'
from (irb):87
Can you not just pass the Request the current_user when you create it?
#req = Request.new(:requestor => current_user)
I am not quite sure what the yield params statement is meant to be doing,
If I'm understanding your question correctly, you want to assign the current_user's id as the requestor id to the Request model?
# assign values passed in via form
#request = Request.new(params[:request])
# assign current_user to request
#request.requestor = current_user
# save the request
#request.save!
Hope this helps!
I had a '.' in an error message similar to:
errors.add('You entered a non-supported widget.')
and was getting the "interning empty string error"
This post saved me:
http://www.tonyamoyal.com/2009/10/20/interning-empty-string-error-in-ruby-on-rails/
Simply changing to:
errors.add('You entered a non-supported widget')
fixed it. Rails 2.3.4 and Ruby 1.8.5

Resources