Rails authentication with LDAP and local database - ruby-on-rails

I am trying to rewrite an older app that was created with PHP/MySQL.
The authentication system used has a users table in the database that stores username, email etc... but NOT passwords.
Whenever the user logs in it first checks the database to see if the user exists if not then returns a login error. If the user exists in the local database then it tries to bind to the active directory using the username/password combination entered by the user and creates a session if successful.
What is the best way to accomplish this using Rails?

Ruby's Net::LDAP library is pretty good.
Here's a simplified version of what I've been using for years:
# sessions_controller.rb
def create
user = User.find_by_login(params[:login])
if user && Ldap.authenticate(params[:login], params[:password])
self.current_user = user
Rails.logger.info "Logged in #{user.name}"
flash[:notice] = "Successfully Logged In!"
redirect_back_or_default root_url
else
flash[:alert] = "Invalid User credentials"
render :new
end
end
# lib/ldap.rb
# Ldap.authenticate('user','password')
# Returns true if validated
# Returns false if invalidated
# Returns nil if LDAP unavailable
require 'net/ldap'
class Ldap
def self.config
# this is actually loaded from a yaml config file
{
:domain => 'YOURDOMAIN',
:host => '10.10.10.100'
}
end
def self.authenticate(login, password)
conn = Net::LDAP.new(
:host => config[:host],
:port => 636,
:base => "dc=#{config[:domain]}, dc=local",
:encryption => :simple_tls,
:auth => {
:username => "#{login}##{config[:domain]}.local",
:password => password,
:method => :simple
}
)
Timeout::timeout(15) do
return conn.bind ? true : false
end
rescue Net::LDAP::LdapError => e
notify_ldap_admin(config[:host],'Error',e)
nil
rescue Timeout::Error => e
notify_ldap_admin(config[:host],'Timeout',e)
nil
end
def self.notify_ldap_admin(host,error_type,error)
msg = "LDAP #{error_type} on #{host}"
RAILS_DEFAULT_LOGGER.debug(msg)
DeveloperMailer.deliver_ldap_failure_msg(msg,error)
end
end

Check out the devise and devise_ldap_authenticatable libraries.

Related

Difficulty debugging RSepec

I'm trying to debug my feature spec in RSpec. But I'm unable to get an exception. If I put a binding.pry before auth.save!, I'm able to break in. I then check if auth.valid? and it returns true. I also call auth.save manually with no exception being thrown. But if I put a pry after user.update_for_omniauth omniauth It doesn't get hit. The method update_from_omniauth is not actually being called because I am stubbing it. There is a rescue block at the end of my method. Placing a pry there doesn't trigger anything either. My spec is failing because I doesn't find an Authentication nor User in the database.
authentication controller
def create
user = merge_users! if current_user
auth = current_auth
user ||= auth&.user
email_user = User.has_email.where(email: omniauth.info.email).first_or_initialize
if user && email_user.persisted? && user != email_user
user.merge! email_user, auth
end
user ||= email_user
auth ||= user.authentications.build uid: omniauth.uid,
provider: omniauth.provider, image_url: omniauth.info.image
auth.token = omniauth.credentials.token
if Authentication::GOOGLE.include?(params[:provider].to_sym)
auth.token_expire = Time.at(omniauth.credentials.expires_at)
end
if omniauth.credentials.refresh_token
auth.refresh_token = omniauth.credentials.refresh_token
end
auth.refresh_facebook_token # get a longer running token
auth.save!
user.update_for_omniauth omniauth
user.save!
if params[:provider] == 'google_contacts'
params[:sync_status] = Contact.sync player, auth.token
end
sign_in_and_redirect user
rescue => e
if Rails.env.production?
Raven.capture_exception e, extra: omniauth
redirect_back fallback_location: new_user_session_path, flash: {error: e.message}
else
raise
end
spec version 1
it 'set organically login user as propertyuser' do
visit '/users/sign_in'
click_link 'Login via Facebook'
expect(Authentication.last.uid).to eq('654321')
end
spec version 2
it 'set organically login user as propertyuser' do
visit '/users/sign_in'
click_link 'Login via Facebook'
expect(User.last.email).to eq('facebookuser#mail.com')
end
more spec code
before do
setup_omniauth
application_controller_patch
allow(Facebook).to receive_message_chain(:oauth_for_app, :exchange_access_token_info).and_return('access_token' => '123', 'expires' => 500_000)
allow_any_instance_of(Authentication).to receive(:refresh_facebook_token) #.and_return(true)
allow_any_instance_of(User).to receive(:update_facebook_properties)# .and_return(true)
allow_any_instance_of(User).to receive(:update_for_omniauth)# .and_return(true)
end
def setup_omniauth
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:facebook_app_rewards] = OmniAuth::AuthHash.new(
'provider' => 'facebook',
'uid' => '654321',
'info' => {
'first_name' => 'Facebook',
'last_name' => 'User',
'email' => 'facebookuser#mail.com',
'image' => 'https://randomuser.me/api/portraits/med/men/65.jpg'
},
'credentials' => {
'token' => '123456',
'secret' => 'top_secret',
'expires_at' => 2.days.from_now
}
)
Rails.application.env_config['devise.mapping'] = Devise.mappings[:user]
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:facebook_app_rewards]
end
def application_controller_patch
ApplicationController.class_eval do
def omniauth
Rails.application.env_config['omniauth.auth']
end
end
end
The most likely reason your tests are failing is because actions triggered by click_link are not guaranteed to have completed when click_link returns. You can test that by adding a few second sleep after the click_link. If that fixes your test then you'll want to replace the sleep with an expectation of visual change on the page.
click_link 'Login via Facebook'
expect(page).to have_text('You have logged in!') # expectation for whatever happens on the page after a successful login
expect ... # rest of your test.
Note: Mocking/Stubbing methods on User in a feature test is generally a bad idea (mocking of anything but external services is generally a bad code smell in a feature test) as is direct DB access from feature tests. Feature tests are designed to be all about testing what a user experiences on the site, not about directly testing the implementation details.

Why ldap bind is failing on server?

I'm authenticating against LDAP server in my rails application,
the code below is working locally but not on the server.
On the server it throws Net::LDAP::BindingInformationInvalidError (Invalid binding information) when trying to login in the app but works through the console
I'm pretty new to Ruby and can't figure out the proper way to debug it... I know the LDAP configuration is right because i can authenticate and bind from the console or on my local development environment.. I tried to pass :verbose => true to the LDAP constructor but without effect...
require 'net/ldap'
require 'devise/strategies/authenticatable'
module Devise
module Strategies
class LdapAuthenticatable < Authenticatable
def authenticate!
if params[:user]
ldap = Net::LDAP.new :host => 'XX.XX.XX.XX',
:port => 636,
:connect_timeout => 5,
:base => 'CN=Configuration,DC=internal,DC=XX,DC=XX',
:encryption => {
:method => :simple_tls
},
:auth => {
:method => :simple,
:username => ENV['LDAP_USER'],
:password => ENV['LDAP_PASSWORD']
}
result = ldap.bind_as(:base => "OU=Users,OU=XX,DC=XX,DC=XX,DC=XX",
:filter => "(userPrincipalName=#{email})",
:password => password,
)
if result
user = User.find_by(email: email)
success!(user)
else
return fail(:invalid_login)
end
end
end
def email
params[:user][:email]
end
def password
params[:user][:password]
end
end
end
end
Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable)
SOLVED
turned out it was the ENV variables that were not read.
Maybe that account is not authorized? Sounds like the problem is in the binding configuration: base => "OU=Users,OU=XX,DC=XX,DC=XX,DC=XX"
More information from other users who encountered this error:
https://gitlab.com/gitlab-org/gitlab-ce/issues/21937
LDAP groups authentication fails: Invalid Binding Information

request.env['omniauth.auth'] is always nil when using seperate admin login using omniauth-identity gem

I want to use seperate admin login for my application using idenity provider.
I have written this in config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :identity, :model => Credential, :on_failed_registration =>SessionsController.action(:register)
provider :identity, :model => Credential, :name => 'admin', :on_failed_registration => SessionsController.action(:login_admin)
provider :google_oauth2, '000000000.apps.googleusercontent.com', '00000000000'
end
In config/routes.rb
match '/auth/admin/callback', :to => 'sessions#authenticate_admin'
In app/controllers/sessions_controller.rb
def authenticate_admin
auth_hash = request.env['omniauth.auth']
session[:admin_user] = auth_hash['user_info']['email']
if admin?
redirect_to '/'
else
render :text => '401 Unauthorized', :status => 401
end
end
But when i try to access request.env['omniauth.auth'], it always gets nil. While it is accessible when using default callback for normal users at sessison#create action. I just want to know if there is anything that has been missed in this code. I am following this blog http://www.intridea.com/blog/2011/1/31/easy-rails-admin-login-with-google-apps-and-omniauth.

How can my Sinatra API manage user logins in client apps?

I'm building an API using Sinatra, which should be able to manage the user login sessions for any of the client apps that send requests to it. So far (for the login functionality) what I've got is a route for /login, and if the User credentials are valid, an AccessToken is created in the RDBMS. See below:
#
# Login
#
post '/login' do
if (#input["email"].nil? || #input["password"].nil?) then
response = {
"success" => false,
"msg" => "Email and password must be provided"
}
$log.error "Email or password not sent to /login"
return response.to_json
end
email = #input["email"]
password = Digest::SHA2.hexdigest(#input["password"])
user = User.where(:email => email, :password => password).first
if user.nil?
response = {
"success" => false,
"msg" => "No users exist with that email/password combo"
}
$log.error "Invalid Email or Password sent to /login"
return response.to_json
end
token = AccessToken.new(:user_id => user.id)
token.save!
$log.info "User valid. Logged in. Token: #{token.token}"
response = {
"success" => true,
"msg" => "User logged in",
"data" => {"token" => token.token}
}
return response.to_json
end
Then for any other routes that require authentication, I'm checking if the token is being sent as a param with the request, e.g.:
#
# Return Clients for User
#
# get '/clients/:token' do
get '/clients' do
token = AccessToken.where(:token => params[:token]).first
#current_user = User.find(token.user_id) unless token.nil?
if #current_user.nil?
response = {
"success" => false,
"msg" => "User must be logged in to do this"
}
$log.error "User is not logged in"
return response.to_json
end
response = {
"success" => true,
"msg" => "Clients successfully retrieved",
"data" => {"clients" => #current_user.clients}
}
$log.info "Clients successfully retrieved"
return response.to_json
end
So I'm wondering: (a) is this a good/robust way of handling user sessions with an API, and (b) is there a standard way of handling it on the client side (iOS, Android, and Web apps)? Should they perhaps store the access token in a session variable, and then with every request add the token to the params being sent?
Thanks!
Look into JSON Web Tokens for a standardized way of exchanging tokens: http://jwt.io/
For the Sinatra part, you could use the following pattern to keep yourself from repeating the authorization code:
before do
token = AccessToken.where(:token => params[:token]).first
#current_user = User.find(token.user_id) unless token.nil?
end
Register an auth method:
register do
def auth (type)
condition do
return {
"success" => false,
"msg" => "User must be logged in to do this"
}.to_json unless #current_user
end
end
end
Now you can add this requirement to any route:
get '/clients', :auth => :user do
# Your implementation
end

How do I get Devise session#create test to pass

Here is my test:
require 'test_helper'
class SessionsControllerTest < ActionController::TestCase
setup do
#request.env["devise.mapping"] = Devise.mappings[:user]
#u = Factory :user, :password => :mypass, :password_confirmation => :mypass
end
test 'log in page loads' do
get :new
assert :success
end
test 'log in with devise password' do
post :create, :user => {:email => #u.email, :password => 'mypass'}
ap session
end
end
gives this output, indicating that the sign in failed:
Loaded suite test/functional/sessions_controller_test
Started
.{
"action" => "create",
"locale" => "en",
"controller" => "sessions",
"user" => {
"password" => "mypass",
"email" => "458286#email.com"
}
}
{
"flash" => {
:alert => "Invalid email or password."
}
}
.
Finished in 0.49123 seconds.
This is my session controller:
#this is an extension of the devise controller for sessions
class SessionsController < Devise::SessionsController
before_filter :set_title_h1, :only => :new
before_filter :debug, :only => :create
before_filter :old_password_system_fix, :only => :create
private
def set_title_h1
#layout[:show_h1] = false
title 'Sign in Or Register'
end
def after_sign_in_path_for(resource)
#override Devise default sign in path /opt/local/lib/ruby/gems/1.8/gems/devise-1.1.2/lib/devise/controllers/helpers.rb
#edit_user_registration_path
'/en/main/index' #forces locale to be defined
end
def after_sign_out_path_for(resource)
#override Devise default sign out path /opt/local/lib/ruby/gems/1.8/gems/devise-1.1.2/lib/devise/controllers/helpers.rb
main_index_path
end
def old_password_system_fix
#purpose is to bring old users into the new system by setting their old password to the new format
require 'digest/md5'
email = params[:user][:email]
pw = params[:user][:password]
#get user
u = User.find_by_email email
return if u.nil?
#if they don't have a devise-style pw, authenticate with old
if u.encrypted_password.blank? && u.old_password.present?
#if [params pw] == md5 [old pw] then create devise-style pw & salt, store it, and let them through to devise auth action
if u.old_password == Digest::MD5.hexdigest(pw)
set_devise_style_pw(u, pw)
#if no match, give "invalid email or pw" message.
else
#flash[:notice] = "Sign in failed."
flash[:notice] = t 'devise.failure.invalid'
#render :new
redirect_to new_user_session_path
end
end
end
def debug
ap params
end
end
What am I missing and how can I test a new session via a functional test?
Turns out you have to use an integration test, not a functional test. Don't ask me why...

Resources