Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ rspec

## Version History

[0.7.0](https://github.com/jonmbake/discourse-ldap-auth/tree/v0.7.0)- Temporarily pulled in omniauth-ldap due to omniauth version conflict with Discourse; bumped net-ldap version

[0.6.1](https://github.com/jonmbake/discourse-ldap-auth/tree/v0.6.1)- Bump net-ldap dependency version in order to support Ruby 3

[0.6.0](https://github.com/jonmbake/discourse-ldap-auth/tree/v0.6.0)- Update omit_username to override_username following Discourse core changes
Expand Down
147 changes: 147 additions & 0 deletions lib/omniauth-ldap/adaptor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# taken from https://github.com/omniauth/omniauth-ldap/blob/master/lib/omniauth-ldap/adaptor.rb
#this code borrowed pieces from activeldap and net-ldap
require 'rack'
require 'net/ldap'
require 'net/ntlm'
require 'sasl'
require 'kconv'
module OmniAuth
module LDAP
class Adaptor
class LdapError < StandardError; end
class ConfigurationError < StandardError; end
class AuthenticationError < StandardError; end
class ConnectionError < StandardError; end

VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base, :allow_anonymous, :filter]

# A list of needed keys. Possible alternatives are specified using sub-lists.
MUST_HAVE_KEYS = [:host, :port, :method, [:uid, :filter], :base]

METHOD = {
:ssl => :simple_tls,
:tls => :start_tls,
:plain => nil,
}

attr_accessor :bind_dn, :password
attr_reader :connection, :uid, :base, :auth, :filter
def self.validate(configuration={})
message = []
MUST_HAVE_KEYS.each do |names|
names = [names].flatten
missing_keys = names.select{|name| configuration[name].nil?}
if missing_keys == names
message << names.join(' or ')
end
end
raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty?
end
def initialize(configuration={})
Adaptor.validate(configuration)
@configuration = configuration.dup
@configuration[:allow_anonymous] ||= false
@logger = @configuration.delete(:logger)
VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
instance_variable_set("@#{name}", @configuration[name])
end
method = ensure_method(@method)
config = {
:host => @host,
:port => @port,
:base => @base
}
@bind_method = @try_sasl ? :sasl : (@allow_anonymous||!@bind_dn||!@password ? :anonymous : :simple)


@auth = sasl_auths({:username => @bind_dn, :password => @password}).first if @bind_method == :sasl
@auth ||= { :method => @bind_method,
:username => @bind_dn,
:password => @password
}
config[:auth] = @auth
config[:encryption] = method
@connection = Net::LDAP.new(config)
end

#:base => "dc=yourcompany, dc=com",
# :filter => "(mail=#{user})",
# :password => psw
def bind_as(args = {})
result = false
@connection.open do |me|
rs = me.search args
if rs and rs.first and dn = rs.first.dn
password = args[:password]
method = args[:method] || @method
password = password.call if password.respond_to?(:call)
if method == 'sasl'
result = rs.first if me.bind(sasl_auths({:username => dn, :password => password}).first)
else
result = rs.first if me.bind(:method => :simple, :username => dn,
:password => password)
end
end
end
result
end

private
def ensure_method(method)
method ||= "plain"
normalized_method = method.to_s.downcase.to_sym
return METHOD[normalized_method] if METHOD.has_key?(normalized_method)

available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ")
format = "%s is not one of the available connect methods: %s"
raise ConfigurationError, format % [method.inspect, available_methods]
end

def sasl_auths(options={})
auths = []
sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
sasl_mechanisms.each do |mechanism|
normalized_mechanism = mechanism.downcase.gsub(/-/, '_')
sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}"
next unless respond_to?(sasl_bind_setup, true)
initial_credential, challenge_response = send(sasl_bind_setup, options)
auths << {
:method => :sasl,
:initial_credential => initial_credential,
:mechanism => mechanism,
:challenge_response => challenge_response
}
end
auths
end

def sasl_bind_setup_digest_md5(options)
bind_dn = options[:username]
initial_credential = ""
challenge_response = Proc.new do |cred|
pref = SASL::Preferences.new :digest_uri => "ldap/#{@host}", :username => bind_dn, :has_password? => true, :password => options[:password]
sasl = SASL.new("DIGEST-MD5", pref)
response = sasl.receive("challenge", cred)
response[1]
end
[initial_credential, challenge_response]
end

def sasl_bind_setup_gss_spnego(options)
bind_dn = options[:username]
psw = options[:password]
raise LdapError.new( "invalid binding information" ) unless (bind_dn && psw)

nego = proc {|challenge|
t2_msg = Net::NTLM::Message.parse( challenge )
bind_dn, domain = bind_dn.split('\\').reverse
t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain
t3_msg = t2_msg.response( {:user => bind_dn, :password => psw}, {:ntlmv2 => true} )
t3_msg.serialize
}
[Net::NTLM::Message::Type1.new.serialize, nego]
end

end
end
end
102 changes: 102 additions & 0 deletions lib/omniauth/strategies/ldap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# taken from https://github.com/omniauth/omniauth-ldap/blob/master/lib/omniauth/strategies/ldap.rb
require 'omniauth'

module OmniAuth
module Strategies
class LDAP
include OmniAuth::Strategy
@@config = {
'name' => 'cn',
'first_name' => 'givenName',
'last_name' => 'sn',
'email' => ['mail', "email", 'userPrincipalName'],
'phone' => ['telephoneNumber', 'homePhone', 'facsimileTelephoneNumber'],
'mobile' => ['mobile', 'mobileTelephoneNumber'],
'nickname' => ['uid', 'userid', 'sAMAccountName'],
'title' => 'title',
'location' => {"%0, %1, %2, %3 %4" => [['address', 'postalAddress', 'homePostalAddress', 'street', 'streetAddress'], ['l'], ['st'],['co'],['postOfficeBox']]},
'uid' => 'dn',
'url' => ['wwwhomepage'],
'image' => 'jpegPhoto',
'description' => 'description'
}
option :title, "LDAP Authentication" #default title for authentication form
option :port, 389
option :method, :plain
option :uid, 'sAMAccountName'
option :name_proc, lambda {|n| n}

def request_phase
OmniAuth::LDAP::Adaptor.validate @options
f = OmniAuth::Form.new(:title => (options[:title] || "LDAP Authentication"), :url => callback_path)
f.text_field 'Login', 'username'
f.password_field 'Password', 'password'
f.button "Sign In"
f.to_response
end

def callback_phase
@adaptor = OmniAuth::LDAP::Adaptor.new @options

return fail!(:missing_credentials) if missing_credentials?
begin
@ldap_user_info = @adaptor.bind_as(:filter => filter(@adaptor), :size => 1, :password => request['password'])
return fail!(:invalid_credentials) if !@ldap_user_info

@user_info = self.class.map_user(@@config, @ldap_user_info)
super
rescue Exception => e
return fail!(:ldap_error, e)
end
end

def filter adaptor
if adaptor.filter and !adaptor.filter.empty?
Net::LDAP::Filter.construct(adaptor.filter % {username: @options[:name_proc].call(request['username'])})
else
Net::LDAP::Filter.eq(adaptor.uid, @options[:name_proc].call(request['username']))
end
end

uid {
@user_info["uid"]
}
info {
@user_info
}
extra {
{ :raw_info => @ldap_user_info }
}

def self.map_user(mapper, object)
user = {}
mapper.each do |key, value|
case value
when String
user[key] = object[value.downcase.to_sym].first if object.respond_to? value.downcase.to_sym
when Array
value.each {|v| (user[key] = object[v.downcase.to_sym].first; break;) if object.respond_to? v.downcase.to_sym}
when Hash
value.map do |key1, value1|
pattern = key1.dup
value1.each_with_index do |v,i|
part = ''; v.collect(&:downcase).collect(&:to_sym).each {|v1| (part = object[v1].first; break;) if object.respond_to? v1}
pattern.gsub!("%#{i}",part||'')
end
user[key] = pattern
end
end
end
user
end

protected

def missing_credentials?
request['username'].nil? or request['username'].empty? or request['password'].nil? or request['password'].empty?
end # missing_credentials?
end
end
end

OmniAuth.config.add_camelization 'ldap', 'LDAP'
7 changes: 4 additions & 3 deletions plugin.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# name:ldap
# about: A plugin to provide ldap authentication.
# version: 0.6.1
# version: 0.7.0
# authors: Jon Bake <[email protected]>

enabled_site_setting :ldap_enabled

gem 'pyu-ruby-sasl', '0.0.3.3', require: false
gem 'rubyntlm', '0.3.4', require: false
gem 'net-ldap', '0.17.1'
gem 'omniauth-ldap', '1.0.5'
gem 'net-ldap', '0.18.0'

require 'yaml'
require_relative 'lib/omniauth-ldap/adaptor'
require_relative 'lib/omniauth/strategies/ldap'
require_relative 'lib/ldap_user'

class ::LDAPAuthenticator < ::Auth::Authenticator
Expand Down
Loading