Using LDAP to Login and Logout Users in Rails apps

In this article, we will use LDAP to login and logout a user using OmniAuth and LDAP. We will also see how to search for a user and test LDAP server connectivity.

Why LDAP?

There are many reasons why LDAP is used for user authentication. In the context of this article, the Rails app does not have it's own users table in the database. It uses the existing users in the LDAP server to authenticate users.

Login and Logout using OmniAuth and LDAP

Add omniauth and omniauth-ldap gems to Gemfile.

gem 'omniauth', '1.4.2'
gem 'omniauth-ldap', '1.0.5'

In app/lib/omni_auth/strategies folder, create ruby_plus_ldap.rb file that implements ldap functionality:

module OmniAuth
  module Strategies
    class RubyPlusLdap
      include OmniAuth::Strategy

      option :title, 'RubyPlus LDAP Authentication'
      option :uid, 'sAMAccountName'
      option :host, 'rubyplus.com'
      option :port, 389
      option :name_proc, lambda {|name| "#{name}@rubyplus.com"}
      option :base, 'OU=Users,OU=RubyPlus,DC=rubyplus,DC=com'
      option :name, 'rubyplus'
      option :login_fields, [:username, :password]

      def request_phase
        redirect "/login"
      end

      def callback_phase
        if request['username'].blank? || request['password'].blank?
          return fail! :missing_credentials
        end
        log :info, "Checking credentials for #{request['username']}"
        user_name = options.name_proc.call(request['username'])
        client = Net::LDAP.new host: options.host, port: options.port, base: options.base
        client.auth user_name, request['password']
        results = client.search filter: Net::LDAP::Filter.eq('userPrincipalName', user_name)
        unless results
          log :info, "Auth failed for #{user_name} -> #{client.get_operation_result.message}"
          return fail! :invalid_credentials
        end
        @ldap_entry = results.first
        super
      end

      uid do
        @ldap_entry.dn
      end

      extra do
        {:raw_info => @ldap_entry}
      end

      info do
        {
            name: @ldap_entry.displayname.first,
            email: @ldap_entry.mail.first,
            nickname: @ldap_entry.samaccountname.first,
            first_name: @ldap_entry.givenname.first,
            last_name: @ldap_entry.sn.first,
            groups: @ldap_entry.respond_to?(:memberof) ? @ldap_entry.memberof : []
        }
      end
    end
  end
end

The user model has attr_accessor for smaaccountname:

class User < ApplicationRecord
  attr_accessor :samaccountname
end

The sessions controller implements the actions need to login and logout users using LDAP.

class SessionsController < ApplicationController
  skip_before_action :check_login, except: [:destroy]

  def create
    existing_user = User.find_by(email: auth_hash['info']['email'])

    if existing_user.nil?
      flash[:alert] = "Account does not exist"

      redirect_to login_url
    else
      reset_session
      session[:current_user] = existing_user.id
      logger.info "Set Session current_user -> #{session[:current_user].inspect}"

      redirect_to home_url
    end
  end

  def destroy
    reset_session

    redirect_to login_url, :notice => "Signed out!"
  end

  def new
    auth_strategy = OmniAuth::Strategies::RubyPlusLdap.new(Rails.application.app)
    @auth_path = auth_strategy.callback_path
  end

  def auth_failed
    redirect_to(login_path, alert: 'Please try your login again.')
  end

  private

  def auth_hash
    request.env['omniauth.auth'].dup
  end
end

The routes.rb needed to make login, logout and omniauth using LDAP to work:

  # The entry point for entering username/password
  get "/login" => "sessions#new", :as => :login  
  # The url to call when the user wants to end their session
  delete "/signout" => "sessions#destroy", :as => :signout
  # The url that is called when authentication is completed (pass or fail) <- for OmniAuth
  post "/auth/:provider/callback" => "sessions#create"
  match 'auth/failure', to: 'sessions#auth_failed', via: [:get, :post]

The login page in app/views/sessions/new.html.erb:

<div class="card">
  <div class="card-body">
    <%= form_tag @auth_path  do %>

      <fieldset>
        <div class="form-group">
        <%= label_tag(:username, 'User Name', class: 'col-form-label') %>
        <%= text_field_tag(:username, nil, class: 'form-control') %>
      </div>
      <div class="form-group">
        <%= label_tag(:password, 'Password', class: 'col-form-label') %>
        <%= password_field_tag(:password, nil, class: 'form-control', placeholder: 'Password') %>
      </div>
      <button type="submit" class="btn btn-lg  btn-block">Sign In</button>
        <%- if flash[:origin] %>
          <%= hidden_field_tag :origin, flash[:origin] %>
        <%- end %>
      </fieldset>

    <%- end %>
  </div>
</div>

Searching LDAP for a User

You can search for a user in the LDAP using:

require 'net/ldap'

class LdapGateway
  EMAIL_KEY = "mail"
  NEEDED_ATTRIBUTES = [:givenname, :sn, :samaccountname]

  def self.search(email)
    hash = {}
    client = Net::LDAP.new :host => Credential.ldap_host,
                           :port => Credential.ldap_port,
                           :base => Credential.ldap_base
    client.auth(Credential.ldap_server_login, Credential.ldap_server_password)
    filter = Net::LDAP::Filter.eq(EMAIL_KEY, email)
    result = client.search(filter: filter)

    result.each do |entry|
      entry.each do |attribute, values|
        if NEEDED_ATTRIBUTES.include?(attribute)
          values.each do |value|
            hash[attribute] = value
          end
        end 
      end
    end     
    hash
  end  

  def self.test
    ldap = Net::LDAP.new
    ldap.host = Credential.ldap_host
    ldap.port = Credential.ldap_port
    ldap.auth Credential.ldap_server_login, Credential.ldap_server_password

    ldap.bind
  end
end

You can encapsulate the credential in its own class:

class Credential
  def self.ldap_host
    ENV['LDAP_HOST']
  end

  def self.ldap_port
    ENV['LDAP_PORT']
  end

  def self.ldap_base
    ENV['LDAP_BASE']
  end

  def self.ldap_server_login
    ENV['LDAP_SERVER_LOGIN']    
  end

  def self.ldap_server_password
    ENV['LDAP_SERVER_PASSWORD']
  end
end

The users controller search action:

  def search
    result = LdapGateway.search(params[:email])

    if result.empty?
      flash[:alert] = 'User does not exist'

      render :new
    else
      @user = User.new(name: result[:givenname] + ' ' + result[:sn], 
                       email: params[:email],
                       samaccountname: result[:samaccountname])
    end
  end

If the user is found, we initialize the user variable that is displayed in the view. Otherwise, we notify that the user does not exist. The route for search:

post 'users/search' => 'users#search', :as => :search_user

LDAP Connectivity Tester

A rake task to test LDAP server connectivity:

namespace :infrastructure do
  desc "Test LDAP connectivity"

  task :ldap_connectivity => :environment do
    success = LdapGateway.test

    if success
      puts 'authentication succeeded'
    else
      puts 'authentication failed'
    end
  end
end

You can run it:

rake infrastructure:ldap_connectivity

This is a useful tool to make sure that your credentials are correct and test LDAP server connection.


Related Articles


Ace the Technical Interview

  • Easily find the gaps in your knowledge
  • Get customized lessons based on where you are
  • Take consistent action everyday
  • Builtin accountability to keep you on track
  • You will solve bigger problems over time
  • Get the job of your dreams

Take the 30 Day Coding Skills Challenge

Gain confidence to attend the interview

No spam ever. Unsubscribe anytime.