oa-enterprise 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -11,7 +11,8 @@ To get just enterprise functionality:
11
11
  For the full auth suite:
12
12
 
13
13
  gem install omniauth
14
-
14
+
15
+ CAS strategy
15
16
  == Stand-Alone Example
16
17
 
17
18
  Use the strategy as a middleware in your application:
@@ -22,7 +23,9 @@ Use the strategy as a middleware in your application:
22
23
 
23
24
  Then simply direct users to '/auth/cas' to have them sign in via your company's CAS server.
24
25
  See OmniAuth::Strategies::CAS::Configuration for more configuration options.
25
-
26
+
27
+ Then simply direct users to '/auth/cas' to have them sign in via your company's CAS server.
28
+ See OmniAuth::Strategies::CAS::Configuration for more configuration options.
26
29
 
27
30
  == OmniAuth Builder
28
31
 
@@ -36,3 +39,20 @@ If CAS is one of several authentication strategies, use the OmniAuth Builder:
36
39
  provider :cas, :server => 'http://cas.mycompany.com/cas'
37
40
  provider :campfire
38
41
  end
42
+
43
+ LDAP strategy
44
+
45
+ use OmniAuth::Strategies::LDAP, :host => '10.101.10.1', :port => 389, :method => :plain, :base => 'dc=intridea, dc=com', :uid => 'sAMAccountName', :try_sasl => true, :sasl_mechanisms => "GSS-SPNEGO"
46
+ or
47
+ use OmniAuth::Builder do
48
+ provider :LDAP, :host => '10.101.10.1', :port => 389, :method => :plain, :base => 'dc=intridea, dc=com', :uid => 'sAMAccountName', :try_sasl => true, :sasl_mechanisms => "GSS-SPNEGO"
49
+ end
50
+
51
+ LDAP server's :host and :port are required, :method is also a required field, and allowed values are :plain, :ssl, and :tls.
52
+ :base is required, it is the distinguish name (DN) for your organization, all users should be searchable under this base.
53
+ :uid is required, it is the LDAP attribute name for the user name in the login form. typically AD would be 'sAMAccountName' or 'UniquePersonalIdentifier', while
54
+ OpenLDAP is 'uid'. You can also use 'dn', if your user choose the put in the dn in the login form (but usually is too long for user to remember or know).
55
+ :try_sasl and :sasl_mechanisms are optional, use it to initial SASL connection to server. mechanism supported are DIGEST-MD5 and GSS-SPNEGO.
56
+
57
+ Then simply direct users to '/auth/ldap' to have them authenticated via your company's LDAP server.
58
+
@@ -3,5 +3,6 @@ require 'omniauth/core'
3
3
  module OmniAuth
4
4
  module Strategies
5
5
  autoload :CAS, 'omniauth/strategies/cas'
6
+ autoload :LDAP, 'omniauth/strategies/ldap'
6
7
  end
7
8
  end
@@ -0,0 +1,94 @@
1
+ require 'omniauth/enterprise'
2
+ require 'net/ldap'
3
+
4
+ module OmniAuth
5
+ module Strategies
6
+ class LDAP
7
+ include OmniAuth::Strategy
8
+
9
+ autoload :Adaptor, 'omniauth/strategies/ldap/adaptor'
10
+ @@config = {'first_name' => 'givenName', 'last_name' => 'sn', 'email' => ['mail', "email", 'userPrincipalName'],
11
+ 'phone' => ['telephoneNumber', 'homePhone', 'facsimileTelephoneNumber'],
12
+ 'mobile_number' => ['mobile', 'mobileTelephoneNumber'],
13
+ 'nickname' => ['uid', 'userid', 'sAMAccountName'],
14
+ 'title' => 'title',
15
+ 'location' => {"%0, %1, %2, %3 %4" => [['address', 'postalAddress', 'homePostalAddress', 'street', 'streetAddress'], ['l'], ['st'],['co'],['postOfficeBox']]},
16
+ 'uid' => 'dn',
17
+ 'url' => ['wwwhomepage'],
18
+ 'image' => 'jpegPhoto',
19
+ 'description' => 'description'}
20
+ def initialize(app, title, options = {})
21
+ super(app, options.delete(:name) || :ldap)
22
+ @title = title
23
+ @adaptor = OmniAuth::Strategies::LDAP::Adaptor.new(options)
24
+ end
25
+
26
+ protected
27
+
28
+ def request_phase
29
+ if env['REQUEST_METHOD'] == 'GET'
30
+ get_credentials
31
+ else
32
+ perform
33
+ end
34
+ end
35
+
36
+ def get_credentials
37
+ OmniAuth::Form.build(@title) do
38
+ text_field 'Login', 'username'
39
+ password_field 'Password', 'password'
40
+ end.to_response
41
+ end
42
+ def perform
43
+ begin
44
+ @adaptor.bind(:bind_dn => request.POST['username'], :password => request.POST['password'])
45
+ @ldap_user_info = @adaptor.search(:filter => Net::LDAP::Filter.eq(@adaptor.uid, request.POST['username']),:limit => 1)
46
+ @user_info = self.class.map_user(@@config, @ldap_user_info)
47
+ request.POST['auth'] = auth_hash
48
+ @env['REQUEST_METHOD'] = 'GET'
49
+ @env['PATH_INFO'] = "#{OmniAuth.config.path_prefix}/#{name}/callback"
50
+
51
+ @app.call(@env)
52
+ rescue Exception => e
53
+ puts e.message
54
+ fail!(:invalid_credentials)
55
+ end
56
+ end
57
+
58
+ def callback_phase
59
+ fail!(:invalid_request)
60
+ end
61
+
62
+ def auth_hash
63
+ OmniAuth::Utils.deep_merge(super, {
64
+ 'uid' => @user_info["uid"],
65
+ 'user_info' => @user_info,
66
+ 'extra' => @ldap_user_info
67
+ })
68
+ end
69
+
70
+ def self.map_user mapper, object
71
+ user = {}
72
+ mapper.each do |key, value|
73
+ case value
74
+ when String
75
+ user[key] = object[value.downcase.to_sym].to_s if object[value.downcase.to_sym]
76
+ when Array
77
+ value.each {|v| (user[key] = object[v.downcase.to_sym].to_s; break;) if object[v.downcase.to_sym]}
78
+ when Hash
79
+ value.map do |key1, value1|
80
+ pattern = key1.dup
81
+ value1.each_with_index do |v,i|
82
+ part = '';
83
+ v.each {|v1| (part = object[v1.downcase.to_sym].to_s; break;) if object[v1.downcase.to_sym]}
84
+ pattern.gsub!("%#{i}",part||'')
85
+ end
86
+ user[key] = pattern
87
+ end
88
+ end
89
+ end
90
+ user
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,293 @@
1
+ #this code boughts pieces from activeldap and net-ldap
2
+ require 'rack'
3
+ require 'net/ldap'
4
+ require 'net/ntlm'
5
+ require 'uri'
6
+ module OmniAuth
7
+ module Strategies
8
+ class LDAP
9
+ class Adaptor
10
+ class LdapError < StandardError; end
11
+ class ConfigurationError < StandardError; end
12
+ class AuthenticationError < StandardError; end
13
+ class ConnectionError < StandardError; end
14
+ VALID_ADAPTER_CONFIGURATION_KEYS = [:host, :port, :method, :bind_dn, :password,
15
+ :try_sasl, :sasl_mechanisms, :uid, :base]
16
+ MUST_HAVE_KEYS = [:host, :port, :method, :uid, :base]
17
+ METHOD = {
18
+ :ssl => :simple_tls,
19
+ :tls => :start_tls,
20
+ :plain => nil
21
+ }
22
+ attr_accessor :bind_dn, :password
23
+ attr_reader :connection, :uid, :base
24
+ def initialize(configuration={})
25
+ @connection = nil
26
+ @disconnected = false
27
+ @bound = false
28
+ @configuration = configuration.dup
29
+ @logger = @configuration.delete(:logger)
30
+ message = []
31
+ MUST_HAVE_KEYS.each do |name|
32
+ message << name if configuration[name].nil?
33
+ end
34
+ raise ArgumentError.new(message.join(",") +" MUST be provided") unless message.empty?
35
+ VALID_ADAPTER_CONFIGURATION_KEYS.each do |name|
36
+ instance_variable_set("@#{name}", configuration[name])
37
+ end
38
+ end
39
+
40
+ def connect(options={})
41
+ host = options[:host] || @host
42
+ method = options[:method] || @method || :plain
43
+ port = options[:port] || @port || ensure_port(method)
44
+ method = ensure_method(method)
45
+ @disconnected = false
46
+ @bound = false
47
+ @bind_tried = false
48
+ config = {
49
+ :host => host,
50
+ :port => port,
51
+ }
52
+ config[:encryption] = {:method => method} if method
53
+ @connection, @uri, @with_start_tls =
54
+ begin
55
+ uri = construct_uri(host, port, method == :simple_tls)
56
+ with_start_tls = method == :start_tls
57
+ puts ({:uri => uri, :with_start_tls => with_start_tls}).inspect
58
+ [Net::LDAP::Connection.new(config), uri, with_start_tls]
59
+ rescue Net::LDAP::LdapError
60
+ raise ConnectionError, $!.message
61
+ end
62
+ end
63
+
64
+ def unbind(options={})
65
+ @connection.close # Net::LDAP doesn't implement unbind.
66
+ end
67
+
68
+ def bind(options={})
69
+ connect(options) unless connecting?
70
+ begin
71
+ @bind_tried = true
72
+
73
+ bind_dn = (options[:bind_dn] || @bind_dn).to_s
74
+ try_sasl = options.has_key?(:try_sasl) ? options[:try_sasl] : @try_sasl
75
+
76
+ # Rough bind loop:
77
+ # Attempt 1: SASL if available
78
+ # Attempt 2: SIMPLE with credentials if password block
79
+ if try_sasl and sasl_bind(bind_dn, options)
80
+ puts "bind with sasl"
81
+ elsif simple_bind(bind_dn, options)
82
+ puts "bind with simple"
83
+ else
84
+ message = yield if block_given?
85
+ message ||= ('All authentication methods for %s exhausted.') % target
86
+ raise AuthenticationError, message
87
+ end
88
+
89
+ @bound = true
90
+ rescue Net::LDAP::LdapError
91
+ raise AuthenticationError, $!.message
92
+ end
93
+ end
94
+
95
+ def disconnect!(options={})
96
+ unbind(options)
97
+ @connection = @uri = @with_start_tls = nil
98
+ @disconnected = true
99
+ end
100
+
101
+ def rebind(options={})
102
+ unbind(options) if bound?
103
+ connect(options)
104
+ end
105
+
106
+ def connecting?
107
+ !@connection.nil? and !@disconnected
108
+ end
109
+
110
+ def bound?
111
+ connecting? and @bound
112
+ end
113
+
114
+ def search(options={}, &block)
115
+ base = options[:base]
116
+ filter = options[:filter]
117
+ limit = options[:limit]
118
+
119
+ args = {
120
+ :base => @base,
121
+ :filter => filter,
122
+ :size => limit
123
+ }
124
+ puts args.inspect
125
+ attributes = {}
126
+ execute(:search, args) do |entry|
127
+ entry.attribute_names.each do |name|
128
+ attributes[name] = entry[name]
129
+ end
130
+ end
131
+ attributes
132
+ end
133
+ private
134
+ def execute(method, *args, &block)
135
+ result = @connection.send(method, *args, &block)
136
+ message = nil
137
+ if result.is_a?(Hash)
138
+ message = result[:errorMessage]
139
+ result = result[:resultCode]
140
+ end
141
+ unless result.zero?
142
+ message = [Net::LDAP.result2string(result), message].compact.join(": ")
143
+ raise LdapError, message
144
+ end
145
+ end
146
+
147
+ def ensure_port(method)
148
+ if method == :ssl
149
+ URI::LDAPS::DEFAULT_PORT
150
+ else
151
+ URI::LDAP::DEFAULT_PORT
152
+ end
153
+ end
154
+
155
+ def prepare_connection(options)
156
+ end
157
+
158
+ def ensure_method(method)
159
+ method ||= "plain"
160
+ normalized_method = method.to_s.downcase.to_sym
161
+ return METHOD[normalized_method] if METHOD.has_key?(normalized_method)
162
+
163
+ available_methods = METHOD.keys.collect {|m| m.inspect}.join(", ")
164
+ format = "%s is not one of the available connect methods: %s"
165
+ raise ConfigurationError, format % [method.inspect, available_methods]
166
+ end
167
+
168
+ def sasl_bind(bind_dn, options={})
169
+ sasl_mechanisms = options[:sasl_mechanisms] || @sasl_mechanisms
170
+ sasl_mechanisms.each do |mechanism|
171
+ begin
172
+ normalized_mechanism = mechanism.downcase.gsub(/-/, '_')
173
+ sasl_bind_setup = "sasl_bind_setup_#{normalized_mechanism}"
174
+ next unless respond_to?(sasl_bind_setup, true)
175
+ initial_credential, challenge_response =
176
+ send(sasl_bind_setup, bind_dn, options)
177
+ args = {
178
+ :method => :sasl,
179
+ :initial_credential => initial_credential,
180
+ :mechanism => mechanism,
181
+ :challenge_response => challenge_response,
182
+ }
183
+ info = {
184
+ :name => "bind: SASL", :dn => bind_dn, :mechanism => mechanism,
185
+ }
186
+ puts info.inspect
187
+ execute(:bind, args)
188
+ return true
189
+ rescue Exception => e
190
+ puts e.message
191
+ end
192
+ end
193
+ false
194
+ end
195
+
196
+ def parse_sasl_digest_md5_credential(cred)
197
+ params = {}
198
+ cred.scan(/(\w+)=(\"?)(.+?)\2(?:,|$)/) do |name, sep, value|
199
+ params[name] = value
200
+ end
201
+ params
202
+ end
203
+ CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
204
+ def generate_client_nonce(size=32)
205
+ nonce = ""
206
+ size.times do |i|
207
+ nonce << CHARS[rand(CHARS.size)]
208
+ end
209
+ nonce
210
+ end
211
+ def sasl_bind_setup_digest_md5(bind_dn, options)
212
+ initial_credential = ""
213
+ nonce_count = 1
214
+ challenge_response = Proc.new do |cred|
215
+ params = parse_sasl_digest_md5_credential(cred)
216
+ qops = params["qop"].split(/,/)
217
+ unless qops.include?("auth")
218
+ raise ActiveLdap::AuthenticationError,
219
+ _("unsupported qops: %s") % qops.inspect
220
+ end
221
+ qop = "auth"
222
+ server = @connection.instance_variable_get("@conn").addr[2]
223
+ realm = params['realm']
224
+ uri = "ldap/#{server}"
225
+ nc = "%08x" % nonce_count
226
+ nonce = params["nonce"]
227
+ cnonce = generate_client_nonce
228
+ requests = {
229
+ :username => bind_dn.inspect,
230
+ :realm => realm.inspect,
231
+ :nonce => nonce.inspect,
232
+ :cnonce => cnonce.inspect,
233
+ :nc => nc,
234
+ :qop => qop,
235
+ :maxbuf => "65536",
236
+ "digest-uri" => uri.inspect,
237
+ }
238
+ a1 = "#{bind_dn}:#{realm}:#{options[:password]||@password}"
239
+ a1 = "#{Digest::MD5.digest(a1)}:#{nonce}:#{cnonce}"
240
+ ha1 = Digest::MD5.hexdigest(a1)
241
+ a2 = "AUTHENTICATE:#{uri}"
242
+ ha2 = Digest::MD5.hexdigest(a2)
243
+ response = "#{ha1}:#{nonce}:#{nc}:#{cnonce}:#{qop}:#{ha2}"
244
+ requests["response"] = Digest::MD5.hexdigest(response)
245
+ nonce_count += 1
246
+ requests.collect do |key, value|
247
+ "#{key}=#{value}"
248
+ end.join(",")
249
+ end
250
+ [initial_credential, challenge_response]
251
+ end
252
+ def sasl_bind_setup_gss_spnego(bind_dn, options)
253
+ puts options.inspect
254
+ user,psw = [bind_dn, options[:password]||@password]
255
+ raise LdapError.new( "invalid binding information" ) unless (user && psw)
256
+
257
+ nego = proc {|challenge|
258
+ t2_msg = Net::NTLM::Message.parse( challenge )
259
+ user, domain = user.split('\\').reverse
260
+ t2_msg.target_name = Net::NTLM::encode_utf16le(domain) if domain
261
+ t3_msg = t2_msg.response( {:user => user, :password => psw}, {:ntlmv2 => true} )
262
+ t3_msg.serialize
263
+ }
264
+ [Net::NTLM::Message::Type1.new.serialize, nego]
265
+ end
266
+
267
+ def simple_bind(bind_dn, options={})
268
+ args = {
269
+ :method => :simple,
270
+ :username => bind_dn,
271
+ :password => options[:password]||@password,
272
+ }
273
+ execute(:bind, args)
274
+ true
275
+ end
276
+
277
+ def construct_uri(host, port, ssl)
278
+ protocol = ssl ? "ldaps" : "ldap"
279
+ URI.parse("#{protocol}://#{host}:#{port}").to_s
280
+ end
281
+
282
+ def target
283
+ return nil if @uri.nil?
284
+ if @with_start_tls
285
+ "#{@uri}(StartTLS)"
286
+ else
287
+ @uri
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
metadata CHANGED
@@ -1,21 +1,22 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oa-enterprise
3
3
  version: !ruby/object:Gem::Version
4
- hash: 27
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - James A. Rosen
14
+ - Ping Yu
14
15
  autorequire:
15
16
  bindir: bin
16
17
  cert_chain: []
17
18
 
18
- date: 2010-10-01 00:00:00 -05:00
19
+ date: 2010-10-04 00:00:00 -05:00
19
20
  default_executable:
20
21
  dependencies:
21
22
  - !ruby/object:Gem::Dependency
@@ -24,12 +25,12 @@ dependencies:
24
25
  requirements:
25
26
  - - "="
26
27
  - !ruby/object:Gem::Version
27
- hash: 27
28
+ hash: 25
28
29
  segments:
29
30
  - 0
30
31
  - 1
31
- - 0
32
- version: 0.1.0
32
+ - 1
33
+ version: 0.1.1
33
34
  requirement: *id001
34
35
  name: oa-core
35
36
  prerelease: false
@@ -52,6 +53,38 @@ dependencies:
52
53
  type: :runtime
53
54
  - !ruby/object:Gem::Dependency
54
55
  version_requirements: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 25
61
+ segments:
62
+ - 0
63
+ - 1
64
+ - 1
65
+ version: 0.1.1
66
+ requirement: *id003
67
+ name: net-ldap
68
+ prerelease: false
69
+ type: :runtime
70
+ - !ruby/object:Gem::Dependency
71
+ version_requirements: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 25
77
+ segments:
78
+ - 0
79
+ - 1
80
+ - 1
81
+ version: 0.1.1
82
+ requirement: *id004
83
+ name: rubyntlm
84
+ prerelease: false
85
+ type: :runtime
86
+ - !ruby/object:Gem::Dependency
87
+ version_requirements: &id005 !ruby/object:Gem::Requirement
55
88
  none: false
56
89
  requirements:
57
90
  - - ">="
@@ -60,12 +93,12 @@ dependencies:
60
93
  segments:
61
94
  - 0
62
95
  version: "0"
63
- requirement: *id003
96
+ requirement: *id005
64
97
  name: rake
65
98
  prerelease: false
66
99
  type: :development
67
100
  - !ruby/object:Gem::Dependency
68
- version_requirements: &id004 !ruby/object:Gem::Requirement
101
+ version_requirements: &id006 !ruby/object:Gem::Requirement
69
102
  none: false
70
103
  requirements:
71
104
  - - ~>
@@ -76,12 +109,12 @@ dependencies:
76
109
  - 0
77
110
  - 8
78
111
  version: 0.0.8
79
- requirement: *id004
112
+ requirement: *id006
80
113
  name: mg
81
114
  prerelease: false
82
115
  type: :development
83
116
  - !ruby/object:Gem::Dependency
84
- version_requirements: &id005 !ruby/object:Gem::Requirement
117
+ version_requirements: &id007 !ruby/object:Gem::Requirement
85
118
  none: false
86
119
  requirements:
87
120
  - - ~>
@@ -92,12 +125,12 @@ dependencies:
92
125
  - 3
93
126
  - 0
94
127
  version: 1.3.0
95
- requirement: *id005
128
+ requirement: *id007
96
129
  name: rspec
97
130
  prerelease: false
98
131
  type: :development
99
132
  - !ruby/object:Gem::Dependency
100
- version_requirements: &id006 !ruby/object:Gem::Requirement
133
+ version_requirements: &id008 !ruby/object:Gem::Requirement
101
134
  none: false
102
135
  requirements:
103
136
  - - ~>
@@ -108,12 +141,12 @@ dependencies:
108
141
  - 3
109
142
  - 4
110
143
  version: 1.3.4
111
- requirement: *id006
144
+ requirement: *id008
112
145
  name: webmock
113
146
  prerelease: false
114
147
  type: :development
115
148
  - !ruby/object:Gem::Dependency
116
- version_requirements: &id007 !ruby/object:Gem::Requirement
149
+ version_requirements: &id009 !ruby/object:Gem::Requirement
117
150
  none: false
118
151
  requirements:
119
152
  - - ~>
@@ -124,12 +157,12 @@ dependencies:
124
157
  - 5
125
158
  - 4
126
159
  version: 0.5.4
127
- requirement: *id007
160
+ requirement: *id009
128
161
  name: rack-test
129
162
  prerelease: false
130
163
  type: :development
131
164
  - !ruby/object:Gem::Dependency
132
- version_requirements: &id008 !ruby/object:Gem::Requirement
165
+ version_requirements: &id010 !ruby/object:Gem::Requirement
133
166
  none: false
134
167
  requirements:
135
168
  - - ~>
@@ -140,7 +173,7 @@ dependencies:
140
173
  - 4
141
174
  - 3
142
175
  version: 1.4.3
143
- requirement: *id008
176
+ requirement: *id010
144
177
  name: json
145
178
  prerelease: false
146
179
  type: :development
@@ -157,6 +190,8 @@ files:
157
190
  - lib/omniauth/strategies/cas/configuration.rb
158
191
  - lib/omniauth/strategies/cas/service_ticket_validator.rb
159
192
  - lib/omniauth/strategies/cas.rb
193
+ - lib/omniauth/strategies/ldap/adaptor.rb
194
+ - lib/omniauth/strategies/ldap.rb
160
195
  - README.rdoc
161
196
  - LICENSE.rdoc
162
197
  - CHANGELOG.rdoc