├── .rspec ├── .gitignore ├── lib ├── omniauth-ldap │ ├── version.rb │ └── adaptor.rb ├── omniauth-ldap.rb └── omniauth │ └── strategies │ └── ldap.rb ├── .travis.yml ├── Gemfile ├── Rakefile ├── Guardfile ├── spec ├── spec_helper.rb ├── omniauth-ldap │ └── adaptor_spec.rb └── omniauth │ └── strategies │ └── ldap_spec.rb ├── gitlab_omniauth-ldap.gemspec ├── Gemfile.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | coverage 3 | -------------------------------------------------------------------------------- /lib/omniauth-ldap/version.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module LDAP 3 | VERSION = "1.2.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | branches: 2 | only: 3 | - 'master' 4 | rvm: 5 | - 2.0.0 6 | - 2.1.5 7 | script: "bundle exec rspec spec" 8 | -------------------------------------------------------------------------------- /lib/omniauth-ldap.rb: -------------------------------------------------------------------------------- 1 | require "omniauth-ldap/version" 2 | require "omniauth-ldap/adaptor" 3 | require 'omniauth/strategies/ldap' 4 | 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development, :test do 6 | gem 'rspec' 7 | gem 'pry' 8 | gem 'rake' 9 | gem 'rack-test' 10 | end 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require 'rspec/core/rake_task' 4 | 5 | desc 'Default: run specs.' 6 | task :default => :spec 7 | 8 | desc "Run specs" 9 | RSpec::Core::RakeTask.new 10 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', :version => 2 do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | 7 | 8 | guard 'bundler' do 9 | watch('Gemfile') 10 | watch(/^.+\.gemspec/) 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('..', __FILE__) 2 | $:.unshift File.expand_path('../../lib', __FILE__) 3 | require 'rspec' 4 | require 'rack/test' 5 | require 'omniauth' 6 | require 'omniauth-ldap' 7 | 8 | RSpec.configure do |config| 9 | config.include Rack::Test::Methods 10 | config.extend OmniAuth::Test::StrategyMacros, :type => :strategy 11 | end 12 | 13 | -------------------------------------------------------------------------------- /gitlab_omniauth-ldap.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/omniauth-ldap/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Ping Yu"] 6 | gem.email = ["ping@intridea.com"] 7 | gem.description = %q{A LDAP strategy for OmniAuth.} 8 | gem.summary = %q{A LDAP strategy for OmniAuth.} 9 | gem.homepage = "https://github.com/gitlabhq/omniauth-ldap" 10 | gem.license = "MIT" 11 | 12 | gem.add_runtime_dependency 'omniauth', '~> 1.0' 13 | gem.add_runtime_dependency 'net-ldap', '~> 0.9' 14 | gem.add_runtime_dependency 'pyu-ruby-sasl', '~> 0.0.3.1' 15 | gem.add_runtime_dependency 'rubyntlm', '~> 0.3' 16 | 17 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | gem.files = `git ls-files`.split("\n") 19 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 20 | gem.name = "gitlab_omniauth-ldap" 21 | gem.require_paths = ["lib"] 22 | gem.version = OmniAuth::LDAP::VERSION 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gitlab_omniauth-ldap (1.2.1) 5 | net-ldap (~> 0.9) 6 | omniauth (~> 1.0) 7 | pyu-ruby-sasl (~> 0.0.3.1) 8 | rubyntlm (~> 0.3) 9 | 10 | GEM 11 | remote: http://rubygems.org/ 12 | specs: 13 | coderay (1.0.8) 14 | diff-lcs (1.1.3) 15 | hashie (3.4.0) 16 | method_source (0.8.1) 17 | net-ldap (0.11) 18 | omniauth (1.2.2) 19 | hashie (>= 1.2, < 4) 20 | rack (~> 1.0) 21 | pry (0.9.10) 22 | coderay (~> 1.0.5) 23 | method_source (~> 0.8) 24 | slop (~> 3.3.1) 25 | pyu-ruby-sasl (0.0.3.3) 26 | rack (1.4.1) 27 | rack-test (0.6.2) 28 | rack (>= 1.0) 29 | rake (10.0.3) 30 | rspec (2.12.0) 31 | rspec-core (~> 2.12.0) 32 | rspec-expectations (~> 2.12.0) 33 | rspec-mocks (~> 2.12.0) 34 | rspec-core (2.12.2) 35 | rspec-expectations (2.12.1) 36 | diff-lcs (~> 1.1.3) 37 | rspec-mocks (2.12.1) 38 | rubyntlm (0.5.0) 39 | slop (3.3.3) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | gitlab_omniauth-ldap! 46 | pry 47 | rack-test 48 | rake 49 | rspec 50 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/ldap.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth' 2 | 3 | module OmniAuth 4 | module Strategies 5 | class LDAP 6 | include OmniAuth::Strategy 7 | @@config = { 8 | 'name' => 'cn', 9 | 'first_name' => 'givenName', 10 | 'last_name' => 'sn', 11 | 'email' => ['mail', "email", 'userPrincipalName'], 12 | 'phone' => ['telephoneNumber', 'homePhone', 'facsimileTelephoneNumber'], 13 | 'mobile' => ['mobile', 'mobileTelephoneNumber'], 14 | 'nickname' => ['uid', 'userid', 'sAMAccountName'], 15 | 'title' => 'title', 16 | 'location' => {"%0, %1, %2, %3 %4" => [['address', 'postalAddress', 'homePostalAddress', 'street', 'streetAddress'], ['l'], ['st'],['co'],['postOfficeBox']]}, 17 | 'uid' => 'dn', 18 | 'url' => ['wwwhomepage'], 19 | 'image' => 'jpegPhoto', 20 | 'description' => 'description' 21 | } 22 | option :title, "LDAP Authentication" #default title for authentication form 23 | option :port, 389 24 | option :method, :plain 25 | option :uid, 'sAMAccountName' 26 | option :name_proc, lambda {|n| n} 27 | 28 | def request_phase 29 | OmniAuth::LDAP::Adaptor.validate @options 30 | f = OmniAuth::Form.new(:title => (options[:title] || "LDAP Authentication"), :url => callback_path) 31 | f.text_field 'Login', 'username' 32 | f.password_field 'Password', 'password' 33 | f.button "Sign In" 34 | f.to_response 35 | end 36 | 37 | def callback_phase 38 | @adaptor = OmniAuth::LDAP::Adaptor.new @options 39 | 40 | return fail!(:missing_credentials) if missing_credentials? 41 | begin 42 | @ldap_user_info = @adaptor.bind_as(:filter => filter(@adaptor), :size => 1, :password => request['password']) 43 | 44 | return fail!(:invalid_credentials) if !@ldap_user_info 45 | 46 | @user_info = self.class.map_user(@@config, @ldap_user_info) 47 | super 48 | rescue Exception => e 49 | return fail!(:ldap_error, e) 50 | end 51 | end 52 | 53 | def filter adaptor 54 | if adaptor.filter and !adaptor.filter.empty? 55 | username = Net::LDAP::Filter.escape(@options[:name_proc].call(request['username'])) 56 | Net::LDAP::Filter.construct(adaptor.filter % { username: username }) 57 | else 58 | Net::LDAP::Filter.equals(adaptor.uid, @options[:name_proc].call(request['username'])) 59 | end 60 | end 61 | 62 | uid { 63 | @user_info["uid"] 64 | } 65 | info { 66 | @user_info 67 | } 68 | extra { 69 | { :raw_info => @ldap_user_info } 70 | } 71 | 72 | def self.map_user(mapper, object) 73 | user = {} 74 | mapper.each do |key, value| 75 | case value 76 | when String 77 | user[key] = object[value.downcase.to_sym].first if object.respond_to? value.downcase.to_sym 78 | when Array 79 | value.each {|v| (user[key] = object[v.downcase.to_sym].first; break;) if object.respond_to? v.downcase.to_sym} 80 | when Hash 81 | value.map do |key1, value1| 82 | pattern = key1.dup 83 | value1.each_with_index do |v,i| 84 | part = ''; v.collect(&:downcase).collect(&:to_sym).each {|v1| (part = object[v1].first; break;) if object.respond_to? v1} 85 | pattern.gsub!("%#{i}",part||'') 86 | end 87 | user[key] = pattern 88 | end 89 | end 90 | end 91 | user 92 | end 93 | 94 | protected 95 | 96 | def missing_credentials? 97 | request['username'].nil? or request['username'].empty? or request['password'].nil? or request['password'].empty? 98 | end # missing_credentials? 99 | end 100 | end 101 | end 102 | 103 | OmniAuth.config.add_camelization 'ldap', 'LDAP' 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab fork | OmniAuth LDAP [![build status](https://secure.travis-ci.org/gitlabhq/omniauth-ldap.png)](https://travis-ci.org/gitlabhq/omniauth-ldap) 2 | 3 | ### LDAP 4 | 5 | Use the LDAP strategy as a middleware in your application: 6 | 7 | use OmniAuth::Strategies::LDAP, 8 | :title => "My LDAP", 9 | :host => '10.101.10.1', 10 | :port => 389, 11 | :method => :plain, 12 | :base => 'dc=intridea, dc=com', 13 | :uid => 'sAMAccountName', 14 | :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')}, 15 | :bind_dn => 'default_bind_dn', 16 | # Or, alternatively: 17 | #:filter => '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))' 18 | :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')} 19 | :bind_dn => 'default_bind_dn' 20 | :password => 'password' 21 | 22 | All of the listed options are required, with the exception of :title, :name_proc, :bind_dn, and :password. 23 | Allowed values of :method are: :plain, :ssl, :tls. 24 | 25 | :bind_dn and :password is the default credentials to perform user lookup. 26 | most LDAP servers require that you supply a complete DN as a binding-credential, along with an authenticator 27 | such as a password. But for many applications, you often don’t have a full DN to identify the user. 28 | You usually get a simple identifier like a username or an email address, along with a password. 29 | Since many LDAP servers don't allow anonymous access, search function will require a bound connection, 30 | :bind_dn and :password will be required for searching on the username or email to retrieve the DN attribute 31 | for the user. If the LDAP server allows anonymous access, you don't need to provide these two parameters. 32 | 33 | :uid is the LDAP attribute name for the user name in the login form. 34 | typically AD would be 'sAMAccountName' or 'UserPrincipalName', while OpenLDAP is 'uid'. 35 | 36 | :filter is the LDAP filter used to search the user entry. It can be used in place of :uid for more flexibility. 37 | `%{username}` will be replaced by the user name processed by :name_proc. 38 | 39 | :name_proc allows you to match the user name entered with the format of the :uid attributes. 40 | For example, value of 'sAMAccountName' in AD contains only the windows user name. If your user prefers using 41 | email to login, a name_proc as above will trim the email string down to just the windows login name. 42 | In summary, use :name_proc to fill the gap between the submitted username and LDAP uid attribute value. 43 | 44 | :try_sasl and :sasl_mechanisms are optional. :try_sasl [true | false], :sasl_mechanisms ['DIGEST-MD5' | 'GSS-SPNEGO'] 45 | Use them to initialize a SASL connection to server. If you are not familiar with these authentication methods, 46 | please just avoid them. 47 | 48 | Direct users to '/auth/ldap' to have them authenticated via your company's LDAP server. 49 | 50 | 51 | ## License 52 | 53 | Copyright (C) 2011 by Ping Yu and Intridea, Inc. 54 | 55 | Permission is hereby granted, free of charge, to any person obtaining a copy 56 | of this software and associated documentation files (the "Software"), to deal 57 | in the Software without restriction, including without limitation the rights 58 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 59 | copies of the Software, and to permit persons to whom the Software is 60 | furnished to do so, subject to the following conditions: 61 | 62 | The above copyright notice and this permission notice shall be included in 63 | all copies or substantial portions of the Software. 64 | 65 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 66 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 67 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 68 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 69 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 70 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 71 | THE SOFTWARE. 72 | -------------------------------------------------------------------------------- /spec/omniauth-ldap/adaptor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | describe "OmniAuth::LDAP::Adaptor" do 3 | 4 | describe 'initialize' do 5 | it 'should throw exception when must have field is not set' do 6 | #[:host, :port, :method, :bind_dn] 7 | lambda { OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'plain'})}.should raise_error(ArgumentError) 8 | end 9 | 10 | it 'should throw exception when method is not supported' do 11 | lambda { OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'myplain', uid: 'uid', port: 389, base: 'dc=com'})}.should raise_error(OmniAuth::LDAP::Adaptor::ConfigurationError) 12 | end 13 | 14 | it 'should setup ldap connection with anonymous' do 15 | adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) 16 | adaptor.connection.should_not == nil 17 | adaptor.connection.host.should == '192.168.1.145' 18 | adaptor.connection.port.should == 389 19 | adaptor.connection.base.should == 'dc=intridea, dc=com' 20 | adaptor.connection.instance_variable_get('@auth').should == {:method => :anonymous, :username => nil, :password => nil} 21 | end 22 | 23 | it 'should setup ldap connection with simple' do 24 | adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password'}) 25 | adaptor.connection.should_not == nil 26 | adaptor.connection.host.should == '192.168.1.145' 27 | adaptor.connection.port.should == 389 28 | adaptor.connection.base.should == 'dc=intridea, dc=com' 29 | adaptor.connection.instance_variable_get('@auth').should == {:method => :simple, :username => 'bind_dn', :password => 'password'} 30 | end 31 | 32 | it 'should setup ldap connection with sasl-md5' do 33 | adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', try_sasl: true, sasl_mechanisms: ["DIGEST-MD5"], bind_dn: 'bind_dn', password: 'password'}) 34 | adaptor.connection.should_not == nil 35 | adaptor.connection.host.should == '192.168.1.145' 36 | adaptor.connection.port.should == 389 37 | adaptor.connection.base.should == 'dc=intridea, dc=com' 38 | adaptor.connection.instance_variable_get('@auth')[:method].should == :sasl 39 | adaptor.connection.instance_variable_get('@auth')[:mechanism].should == 'DIGEST-MD5' 40 | adaptor.connection.instance_variable_get('@auth')[:initial_credential].should == '' 41 | adaptor.connection.instance_variable_get('@auth')[:challenge_response].should_not be_nil 42 | end 43 | 44 | it 'should setup ldap connection with sasl-gss' do 45 | adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', try_sasl: true, sasl_mechanisms: ["GSS-SPNEGO"], bind_dn: 'bind_dn', password: 'password'}) 46 | adaptor.connection.should_not == nil 47 | adaptor.connection.host.should == '192.168.1.145' 48 | adaptor.connection.port.should == 389 49 | adaptor.connection.base.should == 'dc=intridea, dc=com' 50 | adaptor.connection.instance_variable_get('@auth')[:method].should == :sasl 51 | adaptor.connection.instance_variable_get('@auth')[:mechanism].should == 'GSS-SPNEGO' 52 | adaptor.connection.instance_variable_get('@auth')[:initial_credential].should =~ /^NTLMSSP/ 53 | adaptor.connection.instance_variable_get('@auth')[:challenge_response].should_not be_nil 54 | end 55 | end 56 | 57 | describe 'bind_as' do 58 | let(:args) { {:filter => Net::LDAP::Filter.eq('sAMAccountName', 'username'), :password => 'password', :size => 1} } 59 | let(:rs) { Struct.new(:dn).new('new dn') } 60 | 61 | it 'should bind simple' do 62 | adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.126", method: 'plain', base: 'dc=score, dc=local', port: 389, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password'}) 63 | adaptor.connection.should_receive(:open).and_yield(adaptor.connection) 64 | adaptor.connection.should_receive(:search).with(args).and_return([rs]) 65 | adaptor.connection.should_receive(:bind).with({:username => 'new dn', :password => args[:password], :method => :simple}).and_return(true) 66 | adaptor.bind_as(args).should == rs 67 | end 68 | 69 | it 'should bind sasl' do 70 | adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', try_sasl: true, sasl_mechanisms: ["GSS-SPNEGO"], bind_dn: 'bind_dn', password: 'password'}) 71 | adaptor.connection.should_receive(:open).and_yield(adaptor.connection) 72 | adaptor.connection.should_receive(:search).with(args).and_return([rs]) 73 | adaptor.connection.should_receive(:bind).and_return(true) 74 | adaptor.bind_as(args).should == rs 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/omniauth-ldap/adaptor.rb: -------------------------------------------------------------------------------- 1 | #this code borrowed pieces from activeldap and net-ldap 2 | 3 | require 'rack' 4 | require 'net/ldap' 5 | require 'net/ntlm' 6 | require 'sasl' 7 | require 'kconv' 8 | module OmniAuth 9 | module LDAP 10 | class Adaptor 11 | class LdapError < StandardError; end 12 | class ConfigurationError < StandardError; end 13 | class AuthenticationError < StandardError; end 14 | class ConnectionError < StandardError; end 15 | 16 | VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base, :allow_anonymous, :filter] 17 | 18 | # A list of needed keys. Possible alternatives are specified using sub-lists. 19 | MUST_HAVE_KEYS = [:host, :port, :method, [:uid, :filter], :base] 20 | 21 | METHOD = { 22 | :ssl => :simple_tls, 23 | :tls => :start_tls, 24 | :plain => nil, 25 | } 26 | 27 | attr_accessor :bind_dn, :password 28 | attr_reader :connection, :uid, :base, :auth, :filter 29 | def self.validate(configuration={}) 30 | message = [] 31 | MUST_HAVE_KEYS.each do |names| 32 | names = [names].flatten 33 | missing_keys = names.select{|name| configuration[name].nil?} 34 | if missing_keys == names 35 | message << names.join(' or ') 36 | end 37 | end 38 | raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty? 39 | end 40 | def initialize(configuration={}) 41 | Adaptor.validate(configuration) 42 | @configuration = configuration.dup 43 | @configuration[:allow_anonymous] ||= false 44 | @logger = @configuration.delete(:logger) 45 | VALID_ADAPTER_CONFIGURATION_KEYS.each do |name| 46 | instance_variable_set("@#{name}", @configuration[name]) 47 | end 48 | method = ensure_method(@method) 49 | config = { 50 | :host => @host, 51 | :port => @port, 52 | :encryption => method, 53 | :base => @base 54 | } 55 | 56 | @bind_method = @try_sasl ? :sasl : (@allow_anonymous||!@bind_dn||!@password ? :anonymous : :simple) 57 | 58 | 59 | @auth = sasl_auths({:username => @bind_dn, :password => @password}).first if @bind_method == :sasl 60 | @auth ||= { :method => @bind_method, 61 | :username => @bind_dn, 62 | :password => @password 63 | } 64 | config[:auth] = @auth 65 | @connection = Net::LDAP.new(config) 66 | end 67 | 68 | #:base => "dc=yourcompany, dc=com", 69 | # :filter => "(mail=#{user})", 70 | # :password => psw 71 | def bind_as(args = {}) 72 | result = false 73 | @connection.open do |me| 74 | rs = me.search args 75 | if rs and rs.first and dn = rs.first.dn 76 | password = args[:password] 77 | method = args[:method] || @method 78 | password = password.call if password.respond_to?(:call) 79 | if method == 'sasl' 80 | result = rs.first if me.bind(sasl_auths({:username => dn, :password => password}).first) 81 | else 82 | result = rs.first if me.bind(:method => :simple, :username => dn, 83 | :password => password) 84 | end 85 | end 86 | end 87 | result 88 | end 89 | 90 | private 91 | def ensure_method(method) 92 | method ||= "plain" 93 | normalized_method = method.to_s.downcase.to_sym 94 | return METHOD[normalized_method] if METHOD.has_key?(normalized_method) 95 | 96 | available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ") 97 | format = "%s is not one of the available connect methods: %s" 98 | raise ConfigurationError, format % [method.inspect, available_methods] 99 | end 100 | 101 | def sasl_auths(options={}) 102 | auths = [] 103 | sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms 104 | sasl_mechanisms.each do |mechanism| 105 | normalized_mechanism = mechanism.downcase.gsub(/-/, '_') 106 | sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}" 107 | next unless respond_to?(sasl_bind_setup, true) 108 | initial_credential, challenge_response = send(sasl_bind_setup, options) 109 | auths << { 110 | :method => :sasl, 111 | :initial_credential => initial_credential, 112 | :mechanism => mechanism, 113 | :challenge_response => challenge_response 114 | } 115 | end 116 | auths 117 | end 118 | 119 | def sasl_bind_setup_digest_md5(options) 120 | bind_dn = options[:username] 121 | initial_credential = "" 122 | challenge_response = Proc.new do |cred| 123 | pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password] 124 | sasl = SASL.new("DIGEST-MD5", pref) 125 | response = sasl.receive("challenge", cred) 126 | response[1] 127 | end 128 | [initial_credential, challenge_response] 129 | end 130 | 131 | def sasl_bind_setup_gss_spnego(options) 132 | bind_dn = options[:username] 133 | psw = options[:password] 134 | raise LdapError.new( "invalid binding information" ) unless (bind_dn && psw) 135 | 136 | nego = proc {|challenge| 137 | t2_msg = Net::NTLM::Message.parse( challenge ) 138 | bind_dn, domain = bind_dn.split('\\').reverse 139 | t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain 140 | t3_msg = t2_msg.response( {:user => bind_dn, :password => psw}, {:ntlmv2 => true} ) 141 | t3_msg.serialize 142 | } 143 | [Net::NTLM::Message::Type1.new.serialize, nego] 144 | end 145 | 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /spec/omniauth/strategies/ldap_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "OmniAuth::Strategies::LDAP" do 4 | # :title => "My LDAP", 5 | # :host => '10.101.10.1', 6 | # :port => 389, 7 | # :method => :plain, 8 | # :base => 'dc=intridea, dc=com', 9 | # :uid => 'sAMAccountName', 10 | # :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')} 11 | # :bind_dn => 'default_bind_dn' 12 | # :password => 'password' 13 | class MyLdapProvider < OmniAuth::Strategies::LDAP; end 14 | 15 | let(:app) do 16 | Rack::Builder.new { 17 | use OmniAuth::Test::PhonySession 18 | use MyLdapProvider, :name => 'ldap', :title => 'MyLdap Form', :host => '192.168.1.145', :base => 'dc=score, dc=local', :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')} 19 | run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] } 20 | }.to_app 21 | end 22 | 23 | let(:session) do 24 | last_request.env['rack.session'] 25 | end 26 | 27 | it 'should add a camelization for itself' do 28 | OmniAuth::Utils.camelize('ldap').should == 'LDAP' 29 | end 30 | 31 | describe '/auth/ldap' do 32 | before(:each){ get '/auth/ldap' } 33 | 34 | it 'should display a form' do 35 | last_response.status.should == 200 36 | last_response.body.should be_include(" 1 48 | end 49 | end 50 | 51 | describe 'post /auth/ldap/callback' do 52 | before(:each) do 53 | @adaptor = double(OmniAuth::LDAP::Adaptor, {:uid => 'ping'}) 54 | @adaptor.stub(:filter) 55 | OmniAuth::LDAP::Adaptor.stub(:new).and_return(@adaptor) 56 | end 57 | 58 | context 'failure' do 59 | before(:each) do 60 | @adaptor.stub(:bind_as).and_return(false) 61 | end 62 | 63 | it 'should fail with missing_credentials' do 64 | post('/auth/ldap/callback', {}) 65 | last_response.should be_redirect 66 | last_response.headers['Location'].should =~ %r{missing_credentials} 67 | end 68 | 69 | it 'should redirect to error page' do 70 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 71 | last_response.should be_redirect 72 | last_response.headers['Location'].should =~ %r{invalid_credentials} 73 | end 74 | 75 | it 'should redirect to error page when there is exception' do 76 | @adaptor.stub(:bind_as).and_throw(Exception.new('connection_error')) 77 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 78 | last_response.should be_redirect 79 | last_response.headers['Location'].should =~ %r{ldap_error} 80 | end 81 | 82 | context "when username is not preset" do 83 | it 'should redirect to error page' do 84 | post('/auth/ldap/callback', {}) 85 | 86 | last_response.should be_redirect 87 | last_response.headers['Location'].should =~ %r{missing_credentials} 88 | end 89 | end 90 | 91 | context "when username is empty" do 92 | it 'should redirect to error page' do 93 | post('/auth/ldap/callback', {:username => ""}) 94 | 95 | last_response.should be_redirect 96 | last_response.headers['Location'].should =~ %r{missing_credentials} 97 | end 98 | end 99 | 100 | context "when username is present" do 101 | context "and password is not preset" do 102 | it 'should redirect to error page' do 103 | post('/auth/ldap/callback', {:username => "ping"}) 104 | 105 | last_response.should be_redirect 106 | last_response.headers['Location'].should =~ %r{missing_credentials} 107 | end 108 | end 109 | 110 | context "and password is empty" do 111 | it 'should redirect to error page' do 112 | post('/auth/ldap/callback', {:username => "ping", :password => ""}) 113 | 114 | last_response.should be_redirect 115 | last_response.headers['Location'].should =~ %r{missing_credentials} 116 | end 117 | end 118 | end 119 | 120 | context "when username and password are present" do 121 | context "and bind on LDAP server failed" do 122 | it 'should redirect to error page' do 123 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 124 | 125 | last_response.should be_redirect 126 | last_response.headers['Location'].should =~ %r{invalid_credentials} 127 | end 128 | context 'and filter is set' do 129 | it 'should bind with filter' do 130 | @adaptor.stub(:filter).and_return('uid=%{username}') 131 | Net::LDAP::Filter.should_receive(:construct).with('uid=ping') 132 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 133 | 134 | last_response.should be_redirect 135 | last_response.headers['Location'].should =~ %r{invalid_credentials} 136 | end 137 | end 138 | 139 | end 140 | 141 | context "and communication with LDAP server caused an exception" do 142 | before :each do 143 | @adaptor.stub(:bind_as).and_throw(Exception.new('connection_error')) 144 | end 145 | 146 | it 'should redirect to error page' do 147 | post('/auth/ldap/callback', {:username => "ping", :password => "password"}) 148 | 149 | last_response.should be_redirect 150 | last_response.headers['Location'].should =~ %r{ldap_error} 151 | end 152 | end 153 | end 154 | end 155 | 156 | context 'success' do 157 | let(:auth_hash){ last_request.env['omniauth.auth'] } 158 | 159 | before(:each) do 160 | @adaptor.stub(:filter) 161 | @adaptor.stub(:bind_as).and_return(Net::LDAP::Entry.from_single_ldif_string( 162 | %Q{dn: cn=ping, dc=intridea, dc=com 163 | mail: ping@intridea.com 164 | givenname: Ping 165 | sn: Yu 166 | telephonenumber: 555-555-5555 167 | mobile: 444-444-4444 168 | uid: ping 169 | title: dev 170 | address: k street 171 | l: Washington 172 | st: DC 173 | co: U.S.A 174 | postofficebox: 20001 175 | wwwhomepage: www.intridea.com 176 | jpegphoto: http://www.intridea.com/ping.jpg 177 | description: omniauth-ldap 178 | } 179 | )) 180 | end 181 | 182 | it 'should not redirect to error page' do 183 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 184 | last_response.should_not be_redirect 185 | end 186 | 187 | context 'and filter is set' do 188 | it 'should bind with filter' do 189 | @adaptor.stub(:filter).and_return('uid=%{username}') 190 | Net::LDAP::Filter.should_receive(:construct).with('uid=ping') 191 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 192 | 193 | last_response.should_not be_redirect 194 | end 195 | end 196 | 197 | it 'should map user info to Auth Hash' do 198 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 199 | auth_hash.uid.should == 'cn=ping, dc=intridea, dc=com' 200 | auth_hash.info.email.should == 'ping@intridea.com' 201 | auth_hash.info.first_name.should == 'Ping' 202 | auth_hash.info.last_name.should == 'Yu' 203 | auth_hash.info.phone.should == '555-555-5555' 204 | auth_hash.info.mobile.should == '444-444-4444' 205 | auth_hash.info.nickname.should == 'ping' 206 | auth_hash.info.title.should == 'dev' 207 | auth_hash.info.location.should == 'k street, Washington, DC, U.S.A 20001' 208 | auth_hash.info.url.should == 'www.intridea.com' 209 | auth_hash.info.image.should == 'http://www.intridea.com/ping.jpg' 210 | auth_hash.info.description.should == 'omniauth-ldap' 211 | end 212 | end 213 | 214 | context 'alternate fields' do 215 | let(:auth_hash){ last_request.env['omniauth.auth'] } 216 | 217 | before(:each) do 218 | @adaptor.stub(:filter) 219 | @adaptor.stub(:bind_as).and_return(Net::LDAP::Entry.from_single_ldif_string( 220 | %Q{dn: cn=ping, dc=intridea, dc=com 221 | userprincipalname: ping@intridea.com 222 | givenname: Ping 223 | sn: Yu 224 | telephonenumber: 555-555-5555 225 | mobile: 444-444-4444 226 | uid: ping 227 | title: dev 228 | address: k street 229 | l: Washington 230 | st: DC 231 | co: U.S.A 232 | postofficebox: 20001 233 | wwwhomepage: www.intridea.com 234 | jpegphoto: http://www.intridea.com/ping.jpg 235 | description: omniauth-ldap 236 | } 237 | )) 238 | end 239 | 240 | it 'should map user info to Auth Hash' do 241 | post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) 242 | auth_hash.uid.should == 'cn=ping, dc=intridea, dc=com' 243 | auth_hash.info.email.should == 'ping@intridea.com' 244 | auth_hash.info.first_name.should == 'Ping' 245 | auth_hash.info.last_name.should == 'Yu' 246 | auth_hash.info.phone.should == '555-555-5555' 247 | auth_hash.info.mobile.should == '444-444-4444' 248 | auth_hash.info.nickname.should == 'ping' 249 | auth_hash.info.title.should == 'dev' 250 | auth_hash.info.location.should == 'k street, Washington, DC, U.S.A 20001' 251 | auth_hash.info.url.should == 'www.intridea.com' 252 | auth_hash.info.image.should == 'http://www.intridea.com/ping.jpg' 253 | auth_hash.info.description.should == 'omniauth-ldap' 254 | end 255 | end 256 | end 257 | end 258 | --------------------------------------------------------------------------------