I'm currently working on a rails project in which I am in charge of user authentication.
We've decided to use third party authentication and I tried following an example setup.
The example is done by Kevin Thompson and is called example.
According to the LDAP sever's documentation, the steps I need to do are:
Connect to the LDAP server.
Bind anonymously (no DN and password).
Search for the LDAP entry using the username
Retrieve the DN for the username if found.
Rebind with the user's DN and password that they supplied.
If this rebind succeeds, the user is authenticated.
I've followed Thompson's example, except that I'm not using nifty; using devise for user management and omniauth-ldap for authentication. However, it's not quite working, and I'm wondering if it has to do with a discrepancy between what the server documentation tells me to do and what omniauth-ldap is actually doing...
Specifically, my problem is that I always get an "Invalid credentials" error.
Is this because of a mismatch between what I need to do and what omniauth-ldap is doing?
Advice or suggestions are greatly appreciated!
A little more information about how I've set up (to maintain anonymity, I replaced some things)
I can post more of my code upon request.
config/initializers/devise.rb:
config.omniauth :ldap,
:host => 'ldap1.its.domain.ext',
:base => 'ou=People, dc=domain, dc=ext',
:port => 389,
:attrs => 'uid',
:method => :plain,
:uid => 'uid'
app/controllers/users/omniauth_callbacks_controller.rb:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_filter :verify_authenticity_token
def ldap
ldap_return = request.env["omniauth.auth"]["extra"]["raw_info"]
username = ldap_return.uid[0].to_s
if #user = User.find_by_username(username)
sign_in_and_redirect #user
else
#user = User.create(:username => username,)
sign_in_and_redirect #user
end
end
end
I just solved a similar issue.
First, you'll need to determine if your domain allows anonymous binding. This was not allowed in my case. There is an great pull request for using the current user to bind, dorren/omniauth-ldap. Otherwise, you'll need a system account. Just to get moving initially, I used my username(i.e., userPrincipalName)/password for :bind_dn and :password.
Secondly, for LDAP authentication, the two uid values that are used to authenticate are sAMAccountName(username) or userPrincipalName(username#ldap.domain.ext). I found that my system uses userPrincipalName. To prevent the user from entering that in, I just concatenated the domain prior to submitting the form.
Try this config.
config.omniauth :ldap,
:host => 'ldap.domain.ext',
:base => 'dc=ldap, dc=domain, dc=ext',
:port => 389,
:method => :plain,
:uid => 'userPrincipalName',
:bind_dn => 'bind_dn',
:password => 'password'
I believe :bind_dn can be of the form:
'CN=LastName\, FirstName, OU=People, DC=ldap, DC=domain, DC=ext'
OR
'username#ldap.domain.ext'
I also found writing a ruby script using Net::LDAP, to bind and search really helped me learn about Active Directory as well, since I had no knowledge of this subject prior to this task.
I can't add this as a comment, as I don't have 50 reputation yet, however I found this which may be of use to some people here.
http://blackfistsecurity.blogspot.com.au/2011/12/rails-authentication-using-devise-and.html
I was originally trying to use Omniauth and Omniauth-LDAP without anything else, but the lack of documentation on omniauth-ldap's part makes things difficult.
edit:
Instead of using Omniauth-LDAP, I ended up opting for a vanilla devise install, and wrote my own LDAP functionality. Please note: I use mongoid, and as such the code below is directed towards MongoDB. It can be easily modified for ActiveRecord, however.
In order to do this, I edited the new action in the sessions controller, similarly to as follows:
ldap = Net::LDAP.new
ldap.host = 'domainOrIP'
ldap.port = 389
ldap.auth 'user', 'password'
if ldap.bind
# success, so let's check if the user exists
#existing_user = User.where({username: params[:user][:username] }).first
if #existing_user == nil
#create the user
#user = User.new( {username: params[:user][:username], password: ''})
# I didn't personally store the user's password, as I use LDAP for authentication. (If you save this, please hash and salt it first!!)
#user.save
flash[:notice] = "Success!"
redirect_to '/'
else # already existed
#user = User.find({ username: params[:user][:username] })
flash[:notice] = "Success!"
redirect_to '/'
end
else
flash[:danger] = "An error occurred whilst authenticating with your LDAP server. Please check the configuration and try again."
redirect_to '/'
end
I then left Devise to handle everything else. Worked brilliantly for me -- the code above is from memory, however, so may not be 100% accurate. :)
edit 2:
More information on how to use the Net::LDAP class can be found here:
http://www.rubydoc.info/gems/ruby-net-ldap/Net/LDAP
Related
EDIT: I changed how I'd like to do this I think I will use a MySQL table to whitelist the devise logins using google. The changed question is posted here: Restrict Login with Google OAuth2.0 and Devise to Specific Whitelist Table using Ruby
Alright so I am trying to get restricted authentication for my ruby on rails website using Devise and Omni-Auth2 and only google. Everything is working so far, but I only want emails coming from a certain domain to be accepted. I am open to anyway to do this.
I have done some googling but it seems some PHP users have a bit more local files than I do, maybe because of using the google API client locally? I'm not exactly sure, as I am quite new to coding in general and surprised I made it this far.
Here is an example: Google Oauth2.0 with Python: How do I limit access to a specific domain?
And here: Restrict Login Email with Google OAuth2.0 to Specific Domain Name
Both seem to use the "hd:domain" or something similar, but there seems to be issues with that plus I'm not sure how I would impliment it in my app.
Now for some more info, I am only using the gem devise and omniauth-google-oauth2 (https://github.com/zquestz/omniauth-google-oauth2) I feel like theres a way to do it with that gem but still not entirely sure. Any help would be appreciated if I can post any more info let me know.
My omniauth_callbacks_controller:
class User::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def google_oauth2
# You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.find_for_google_oauth2(request.env["omniauth.auth"], current_user)
if #user.persisted?
flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Google"
sign_in_and_redirect #user, :event => :authentication
else
session["devise.google_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
I know this question is old but answering it just for reference. You need to change config/initializer/omniauth.rb and add "hd" to provider.
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV["google_client_id"], ENV["google_client_secret"],
{
hd: 'domain.com'
}
end
Why don't you just add a validation to the Model to restrict the domain?
validates :email,
presence: true,
uniqueness: true,
format: {
message: 'domain must be example.com',
with: /\A[\w+-.]+#example.com\z/i
}
Other user answered here:
Restrict Login with Google OAuth2.0 and Devise to Specific Whitelist Table using Ruby
I'm new to oauth and api integrations, and am having a hell (can I say that here) of a time trying to figure it out.
I'd like to connect my rails app to Magento (a php ecommerce cart).
They have some basic docs here:
http://www.magentocommerce.com/api/rest/authentication/oauth_authentication.html
While I understand the idea of oauth in principle, I have no idea how to implement a custom solution. I've used a few gems (ex: omniauth) to connect to Twitter, and that was fine, but I just don't know how to create my own strategy for connecting to Magento.
Does anyone know how to do it? Is there a walk-through or screencast somewhere I can use?
If not, what tools or approaches might you recommend for me to figure it out -- if only by trial and error?
Thanks in advance for your help!
Here are detailed instructions from the repo of the omniauth-magento gem / strategy I created:
Setting up Magento
Consumer key & secret
Set up a consumer in Magento and write down consumer key and consumer secret
Privileges
In the Magento Admin backend, go to System > Web Services > REST Roles, select Customer, and tick Retrieve under Customer. Add more privileges as needed.
Attributes
In the Magento Admin backend, go to System > Web Services > REST Attributes, select Customer, and tick Email, First name and Last name under Customer > Read. Add more attributes as needed.
Setting up Rails
Parts of these instructions are based on these OmniAuth instructions, which you can read in case you get stuck.
Devise
Install Devise if you haven't installed it
Add / replace this line in your routes.rb. This will be called once Magento has successfully authorized and returns to the Rails app.
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks"}
Magento oAuth strategy
Load this library into your Gemfile gem "omniauth-magento" and run bundle install
Modify config/initializers/devise.rb:
Devise.setup do |config|
# deactivate SSL on development environment
OpenSSL::SSL::VERIFY_PEER ||= OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
config.omniauth :magento,
"ENTER_YOUR_MAGENTO_CONSUMER_KEY",
"ENTER_YOUR_MAGENTO_CONSUMER_SECRET",
{ :client_options => { :site => "ENTER_YOUR_MAGENTO_URL_WITHOUT_TRAILING_SLASH" } }
# example:
# config.omniauth :magento, "12a3", "45e6", { :client_options => { :site => "http://localhost/magento" } }
Optional: If you want to use the Admin API (as opposed to the Customer API), you need to overwrite the default authorize_path like so:
{ :client_options => { :authorize_path => "/admin/oauth_authorize", :site => ENTER_YOUR_MAGENTO_URL_WITHOUT_TRAILING_SLASH } }
In your folder controllers, create a subfolder users
In that subfolder app/controllers/users/, create a file *omniauth_callbacks_controller.rb* with the following code:
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def magento
# You need to implement the method below in your model (e.g. app/models/user.rb)
#user = User.find_for_magento_oauth(request.env["omniauth.auth"], current_user)
if #user.persisted?
sign_in_and_redirect #user, :event => :authentication #this will throw if #user is not activated
set_flash_message(:notice, :success, :kind => "magento") if is_navigational_format?
else
session["devise.magento_data"] = request.env["omniauth.auth"]
redirect_to new_user_registration_url
end
end
end
User model & table
Here's an example of useful Magento information you can store in your User table once you have created these columns:
email
first_name
last_name
magento_id
magento_token
magento_secret
Optional: You might want to encrypt *magento_token* and *magento_secret* with the *attr_encrypted gem* for example (requires renaming magento_token to encrypted_magento_token and magento_secret to encrypted_magento_secret).
Set up your User model to be omniauthable :omniauthable, :omniauth_providers => [:magento] and create a method to save retrieved information after successfully authenticating.
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :timeoutable,
:omniauthable, :omniauth_providers => [:magento]
def self.find_for_magento_oauth(auth, signed_in_resource=nil)
user = User.find_by(email: auth.info.email)
if !user
user = User.create!(
first_name: auth.info.first_name,
last_name: auth.info.last_name,
magento_id: auth.uid,
magento_token: auth.credentials.token,
magento_secret: auth.credentials.secret,
email: auth.info.email,
password: Devise.friendly_token[0,20]
)
else
user.update!(
magento_id: auth.uid,
magento_token: auth.credentials.token,
magento_secret: auth.credentials.secret
)
end
user
end
end
Link to start authentication
Add this line to your view <%= link_to "Sign in with Magento", user_omniauth_authorize_path(:magento) %>
Authenticating
Start your Rails server
Start your Magento server
Log into Magento with a customer account (or admin account if you want to use the Admin API)
In your Rails app, go to the view where you pasted this line <%= link_to "Sign in with Magento", user_omniauth_authorize_path(:magento) %>
Click on the link
You now should be directed to a Magento view where you are prompted to authorize access to the Magento user account
Once you have confirmed, you should get logged into Rails and redirected to the Rails callback URL specified above. The user should now have magento_id, magento_token and magento_secret stored.
Making API calls
Create a class that uses magento_token and magento_secret to do API calls for instance in *lib/magento_inspector.rb*. Example:
class MagentoInspector
require "oauth"
require "omniauth"
require "multi_json"
def initialize
#access_token = prepare_access_token(current_user) # or pass user in initialize method
#response = MultiJson.decode(#access_token.get("/api/rest/customers").body) # or pass query in initialize method, make sure privileges and attributes are enabled for query (see section at top)
end
private
# from http://behindtechlines.com/2011/08/using-the-tumblr-api-v2-on-rails-with-omniauth/
def prepare_access_token(user)
consumer = OAuth::Consumer.new("ENTER_YOUR_MAGENTO_CONSUMER_KEY", "ENTER_YOUR_MAGENTO_CONSUMER_SECRET", {:site => "ENTER_YOUR_MAGENTO_URL_WITHOUT_TRAILING_SLASH"})
token_hash = {:oauth_token => user.magento_token, :oauth_token_secret => user.magento_secret}
access_token = OAuth::AccessToken.from_hash(consumer, token_hash)
end
end
Make sure Rails loads files in the folder where this class is placed. For the lib folder, put this in config/application.rb: config.autoload_paths += Dir["#{config.root}/lib/**/"]
Perform query MagentoInspector.new
Extend class to suit your needs
I get this kind of guttural response when I hear the M-word (Magento), having spent several months attempting to make it do anything useful. But... assuming you have no choice, and assuming Magento offers a standard OAuth server, then you should be able to use OmniAuth to connect to Mag...blurrgh..ento.
Magento will need to provide several tokens, client_id and client_secret. You use these to request an access token for your app. Once you have it, you can reuse it semi-permanently. OmniAuth might be able to help you with that.
Once you have the access token, you'll need to pass an HTTP header that looks like Authentication: OAuth <access-token> with every request you make to the service.
Requests are made using standard https (I would hope, vs http) to the server. Within Rails you could roll your own REST client (using Net::HTTP and friends), but you might find a gem like RestClient, linked here. There are others out there -- check The Ruby Toolbox for more.
Does that get you started?
I am trying to create a session explicitly like this UserSession.create(#user, true) but the session is not getting created, current_user is nil.
But when I do this, I get < #UserSession: {:unauthorized_record=>""}>us = UserSession.create(#user, true)
RAILS_DEFAULT_LOGGER.info(us.inspect) #=> UserSession: {:unauthorized_record=>""}
I had a look at Authlogic::Session::UnauthorizedRecord here it says
Be careful with this, because Authlogic is assuming that you have already confirmed that the user is who he says he is. For example, this is the method used to persist the session internally. Authlogic finds the user with the persistence token. At this point we know the user is who he says he is, so Authlogic just creates a session with the record. This is particularly useful for 3rd party authentication methods, such as OpenID. Let that method verify the identity, once it’s verified, pass the object and create a session.
which is exactly what I am trying to do (i am authenticating using omniauth and creating session using authlogic).
How do I fix this, so that I can get a valid session in current_user ?
I had a similar issue caused by the persistence_token being nil on the user. Reset it before creating the UserSession. So...
#user.reset_persistence_token!
UserSession.create(#user, true)
I'm not sure about the .create(object, bool) method signature, but the following works using authlogic.
class Api::ApiBaseController < ApplicationController
protected
def verify_token
return false if params[:token].blank?
#session = UserSession.new(User.find_by_single_access_token(params[:token]))
#session.save
end
end
If that doesn't work for you -- I think the #user isn't being set correctly.
If you map the active_record_store to the authlogic user_sessions table your session information will be stored in the database, and you will be able to store larger sets of data.
Inside your config folder:
config/initializers/session_store.rb
Comment out App::Application.config.session_store :cookie_store, :key => '_App_session'
Add or uncomment App::Application.config.session_store :active_record_store
Inside of config/application.rb
At the end of the class for you application add:
ActiveRecord::SessionStore::Session.table_name = 'user_sessions'
Restart your app, and any information stored in the user session will be saved in the authlogic user_sessions table.
Goto: http://apidock.com/rails/ActiveRecord/SessionStore
For more information
For now you can replace
UserSession.create #user
to
UserSession.create :email => #user.email, :password => #user.password
not a big deal.
But that caught me other way. I forgot that my user got active? == false when created. I've set it to true and session is created.
I ran into this problem today. In my case it ended up being related to CSRF tokens.
We are creating a user and session in our app in response to an OAuth callback. It appears that if the CSRF token is invalid, which would be the case when coming from a third party, authlogic won't create the user session.
Can't verify CSRF token authenticity
The fix was simple:
class Oauth::UserSessionsController < ApplicationController
skip_before_action :verify_authenticity_token, only: :callback
def new
# code removed...
end
def callback
# code removed...
UserSession.create(#user)
redirect_to root_path
end
end
What's the best way to enable users to log in with their email address OR their username? I am using warden + devise for authentication. I think it probably won't be too hard to do it but i guess i need some advice here on where to put all the stuff that is needed. Perhaps devise already provides this feature? Like in the config/initializers/devise.rb you would write:
config.authentication_keys = [ :email, :username ]
To require both username AND email for signing in. But i really want to have only one field for both username and email and require only one of them. I'll just visualize that with some ASCII art, it should look something like this in the view:
Username or Email:
[____________________]
Password:
[____________________]
[Sign In]
I have found a solution for the problem. I'm not quite satisfied with it (I'd rather have a way to specify this in the initializer), but it works for now. In the user model I added the following method:
def self.find_for_database_authentication(conditions={})
find_by(username: conditions[:email]) || find_by(email: conditions[:email])
end
As #sguha and #Chetan have pointed out, another great resource is available on the official devise wiki.
From their Wiki — How To: Allow users to sign in using their username or email address.
def self.find_for_authentication(conditions)
conditions = ["username = ? or email = ?", conditions[authentication_keys.first], conditions[authentication_keys.first]]
# raise StandardError, conditions.inspect
super
end
Use their example!
Make sure you already added username field and add username to attr_accessible.
Create a login virtual attribute in Users
1) Add login as an attr_accessor
# Virtual attribute for authenticating by either username or email
# This is in addition to a real persisted field like 'username'
attr_accessor :login
2) Add login to attr_accessible
attr_accessible :login
Tell Devise to use :login in the authentication_keys
Modify config/initializers/devise.rb to have:
config.authentication_keys = [ :login ]
Overwrite Devise’s find_for_database_authentication method in Users
# Overrides the devise method find_for_authentication
# Allow users to Sign In using their username or email address
def self.find_for_authentication(conditions)
login = conditions.delete(:login)
where(conditions).where(["username = :value OR email = :value", { :value => login }]).first
end
Update your views
Make sure you have the Devise views in your project so that you can customize them
remove <%= f.label :email %>
remove <%= f.email_field :email %>
add <%= f.label :login %>
add <%= f.text_field :login %>
https://gist.github.com/867932 : One solution for everything. Sign in, forgot password, confirmation, unlock instructions.
Platforma Tec (devise author) has posted a solution to their github wiki which uses an underlying Warden authentication strategy rather than plugging into the Controller:
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address
(An earlier answer had a broken link, which I believe was intended to link to this resource.)
If you are using MongoDB (with MongoId), you need to query differently:
def self.find_for_database_authentication(conditions={})
self.any_of({name: conditions[:email]},{email: conditions[:email]}).limit(1).first
end
just so it will be somewhere online.
With squeel gem you can do:
def self.find_for_authentication(conditions={})
self.where{(email == conditions[:email]) | (username == conditions[:email])}.first
end
I wrote like this and it works out. Don't know if it's "ugly fix", but if I'll come up with a a better solution I'll let you know...
def self.authenticate(email, password)
user = find_by_email(email) ||
username = find_by_username(email)
if user && user.password_hash = BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
I use a quick hack for this, to avoid changing any devise specific code and use it for my specific scenario (I particularly use it for an API where mobile apps can create users on the server).
I have added a before_filter to all the devise controllers where if username is being passed, I generate an email from the username ("#{params[:user][:username]}#mycustomdomain.com") and save the user. For all other calls as well, I generate the email based on same logic. My before_filter looks like this:
def generate_email_for_username
return if(!params[:user][:email].blank? || params[:user][:username].blank?)
params[:user][:email] = "#{params[:user][:username]}#mycustomdomain.com"
end
I am also saving username in the users table, so I know that users with email ending in #mycustomdomain.com were created using username.
Here's a Rails solution which refactors #padde's answer. It uses ActiveRecord's find_by to simplify the calls, ensures there's only one call based on the regex, and also supports numeric IDs if you want to allow that (useful for scripts/APIs). The regex for email is as simple as it needs to be in this context; just checking for the presence of an # as I assume your username validtor doesn't allow # characters.
def self.find_for_database_authentication(conditions={})
email = conditions[:email]
if email =~ /#/
self.find_by_email(email)
elsif email.to_s =~ /\A[0-9]+\z/
self.find(Integer(email))
else
self.find_by_username(email])
end
end
Like the wiki and #aku's answer, I'd also recommend making a new :login parameter using attr_accessible and authentication_keys instead of using :email here. (I kept it as :email in the example to show the quick fix.)
I'm using my Gmail Apps for Domain account to send email within my rails application for standard automated emails (user signup, forgot password, notify admin of new comment, etc), but I'm worried about the 500 emails per day limit set by Google.
Google suggests one way to overcome the limit is to use multiple user accounts.
So, I've setup 10 additional gmail user accounts (noreply1, noreply2, noreply3, etc) - I'd like to track when any of these accounts has sent 500 emails in a 24 hour period and use the idle account accordingly.
How do I dynamically set the :user_name value in ActionMailer::Base.smtp_settings?
Here's my current setup - NOTE: this sends from "noreply1" every time, even though i'm explicitly setting :user_name and :from to "noreply2":
--- development.rb ---
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
:address => "smtp.gmail.com",
:port => "587",
:domain => "mydomain.com",
:authentication => :plain,
:user_name => "noreply1#mydomain.com",
:password => "password"
}
--- account.rb --- (MODEL, called via a callback)
after_create :send_welcome_email
...
def send_welcome_email
#ActionMailer::Base.smtp_settings[:user_name] = 'noreply2#mydomain.com'
ActionMailer::Base.smtp_settings.merge!({:user_name => "noreply2#mydomain.com"})
SubscriptionNotifier.deliver_welcome(self)
end
--- subscription_notifier.rb --- (MODEL)
class SubscriptionNotifier < ActionMailer::Base
def welcome(account)
#sent_on = Time.now
#subject = "Welcome to the App"
#recipients = account.email
#from = "noreply2#mydomain.com"
#body = { :account => account }
end
end
You could also set up an MTA on your server and use that for sending the mail.
That's what we do.
You have to add your server's IP as a valid one for sending email in your domain's SPF record to avoid getting marked as spam.
Another benefit of this is that if you do this, you can set the From: address of the email to be one of your users, which you cannot do with GMail.
Store the available usernames in a table in the database, along with a 'last-modified', 'last-reset' and a sent count. You can then query this when sending an email to find the least used email address currently. Then increment the sent count and last-modified account. The 'last-reset' value can be used for your cleanup code so that you reset the counts each 24 hour period.
This also makes it easy to add new email accounts, retire accounts you aren't using anymore, implement in a different app, etc. as it's all just in a database table that you can change when required.
You should be able to set the :user_name element in the hash in the mailer in the same fashion as in the configuration, namely by doing:
ActionMailer::Base.smtp_settings[:user_name] = 'new_user_name'
Although this may require some extra code to force a reload of any internal action mailer config (not tested this myself)
The comment box was becoming too restrictive for my questions. Changing the ActionMailer::Base.smtp_settings hash dynamically works as expected for me, so I suspect there is some other factor at play here. Some things to try:
Are you using a TLS plugin? I used action_mailer_optional_tls with Rails 2.3.2 and Ruby 1.8.6.
What is being writing to the log/console?
You're changing the username but not the password: do all the noreply accounts have the same password?
Edit: more things to try
I'd have a good look at that smtp_tls.rb file mentioned in the comments to make sure nothing's hard-coded. Or remove it and try the plugin I linked above. To use it, you just need to add :tls => true to the smtp_settings hash.