gitlab_omniauth-ldap-2.2.0/0000755000004100000410000000000014256370317015637 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/.travis.yml0000644000004100000410000000013314256370317017745 0ustar www-datawww-databranches: only: - 'master' rvm: - 2.0.0 - 2.1.5 script: "bundle exec rspec spec" gitlab_omniauth-ldap-2.2.0/.rspec0000644000004100000410000000001114256370317016744 0ustar www-datawww-data--colour gitlab_omniauth-ldap-2.2.0/README.md0000644000004100000410000001073214256370317017121 0ustar www-datawww-data# GitLab fork | OmniAuth LDAP [![build status](https://secure.travis-ci.org/gitlabhq/omniauth-ldap.png)](https://travis-ci.org/gitlabhq/omniauth-ldap) ### LDAP Use the LDAP strategy as a middleware in your application: use OmniAuth::Strategies::LDAP, :title => "My LDAP", :host => '10.101.10.1', :port => 389, :encryption => :plain, :base => 'dc=intridea, dc=com', :uid => 'sAMAccountName', :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')}, :bind_dn => 'default_bind_dn', # Or, alternatively: #:filter => '(&(uid=%{username})(memberOf=cn=myapp-users,ou=groups,dc=example,dc=com))' :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')} :bind_dn => 'default_bind_dn' :password => 'password' :tls_options => { :ssl_version => 'TLSv1_2', :ciphers => ["AES-128-CBC", "AES-128-CBC-HMAC-SHA1", "AES-128-CBC-HMAC-SHA256"] } All of the listed options are required, with the exception of :title, :name_proc, :bind_dn, and :password. - `encryption` is the type of encryption to use between this library and the LDAP server. `:plain` means no encryption. `:simple_tls` represents SSL/TLS (usually on port 636) while `:start_tls` represents StartTLS (usually port 389). - `:bind_dn` and `:password` are the default credentials to perform user lookup. most LDAP servers require that you supply a complete DN as a binding-credential, along with an authenticator such as a password. But for many applications, you often don’t have a full DN to identify the user. You usually get a simple identifier like a username or an email address, along with a password. Since many LDAP servers don't allow anonymous access, search function will require a bound connection, `:bind_dn` and `:password` will be required for searching on the username or email to retrieve the DN attribute for the user. If the LDAP server allows anonymous access, you don't need to provide these two parameters. - `:uid` is the LDAP attribute name for the user name in the login form. typically AD would be 'sAMAccountName' or 'UserPrincipalName', while OpenLDAP is 'uid'. - `:filter` is the LDAP filter used to search the user entry. It can be used in place of :uid for more flexibility. `%{username}` will be replaced by the user name processed by `:name_proc`. - `:name_proc` allows you to match the user name entered with the format of the :uid attributes. For example, value of 'sAMAccountName' in AD contains only the windows user name. If your user prefers using email to login, a name_proc as above will trim the email string down to just the windows login name. In summary, use `:name_proc` to fill the gap between the submitted username and LDAP uid attribute value. - `:try_sasl` and `:sasl_mechanisms` are optional. `:try_sasl` [`true` | `false`], `:sasl_mechanisms` [`'DIGEST-MD5'` | `'GSS-SPNEGO'`] Use them to initialize a SASL connection to server. If you are not familiar with these authentication methods, please just avoid them. - `:tls_options` allows you to pass in OpenSSL options like `:ssl_version`, `:ciphers` and more. See http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html for all available options and values. Direct users to '/auth/ldap' to have them authenticated via your company's LDAP server. ## License Copyright (C) 2011 by Ping Yu and Intridea, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gitlab_omniauth-ldap-2.2.0/spec/0000755000004100000410000000000014256370317016571 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/spec/omniauth/0000755000004100000410000000000014256370317020415 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/spec/omniauth/strategies/0000755000004100000410000000000014256370317022567 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/spec/omniauth/strategies/ldap_spec.rb0000644000004100000410000002341514256370317025053 0ustar www-datawww-datarequire 'spec_helper' describe "OmniAuth::Strategies::LDAP" do # :title => "My LDAP", # :host => '10.101.10.1', # :port => 389, # :method => :plain, # :verify_certificates => true, # :base => 'dc=intridea, dc=com', # :uid => 'sAMAccountName', # :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')} # :bind_dn => 'default_bind_dn' # :password => 'password' class MyLdapProvider < OmniAuth::Strategies::LDAP; end let(:app) do Rack::Builder.new { use OmniAuth::Test::PhonySession use MyLdapProvider, :name => 'ldap', :title => 'MyLdap Form', :host => '192.168.1.145', :base => 'dc=score, dc=local', :name_proc => Proc.new {|name| name.gsub(/@.*$/,'')} run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] } }.to_app end let(:session) do last_request.env['rack.session'] end it 'should add a camelization for itself' do OmniAuth::Utils.camelize('ldap').should == 'LDAP' end describe '/auth/ldap' do let!(:csrf_token) { SecureRandom.base64(32) } let(:post_env) { make_env('/auth/ldap', 'rack.session' => { csrf: csrf_token }, 'rack.input' => StringIO.new("authenticity_token=#{escaped_token}")) } let(:escaped_token) { URI.encode_www_form_component(csrf_token, Encoding::UTF_8) } before(:each) { post '/auth/ldap', nil, post_env } def make_env(path = '/auth/ldap', props = {}) { 'REQUEST_METHOD' => 'POST', 'PATH_INFO' => path, 'rack.session' => {}, 'rack.input' => StringIO.new('test=true') }.merge(props) end it 'should display a form' do last_response.status.should == 200 last_response.body.should be_include(" 1 end end describe 'post /auth/ldap/callback' do before(:each) do @adaptor = double(OmniAuth::LDAP::Adaptor, {:uid => 'ping'}) @adaptor.stub(:filter) OmniAuth::LDAP::Adaptor.stub(:new).and_return(@adaptor) end context 'failure' do before(:each) do @adaptor.stub(:bind_as).and_return(false) end it 'should fail with missing_credentials' do post('/auth/ldap/callback', {}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{missing_credentials} end it 'should redirect to error page' do post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) expect(last_response).to be_redirect expect(last_response.headers['Location']).to match('invalid_credentials') expect(last_request.env['omniauth.error'].message).to eq('Invalid credentials for ping') end it 'should redirect to error page when there is exception' do @adaptor.stub(:bind_as).and_throw(Exception.new('connection_error')) post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{ldap_error} end context 'wrong request method' do it 'redirects to error page' do get('/auth/ldap/callback', { username: 'ping', password: 'password' }) expect(last_response).to be_redirect expect(last_response.headers['Location']).to match('invalid_request_method') end end context "when username is not preset" do it 'should redirect to error page' do post('/auth/ldap/callback', {}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{missing_credentials} end end context "when username is empty" do it 'should redirect to error page' do post('/auth/ldap/callback', {:username => ""}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{missing_credentials} end end context "when username is present" do context "and password is not preset" do it 'should redirect to error page' do post('/auth/ldap/callback', {:username => "ping"}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{missing_credentials} end end context "and password is empty" do it 'should redirect to error page' do post('/auth/ldap/callback', {:username => "ping", :password => ""}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{missing_credentials} end end end context "when username and password are present" do context "and bind on LDAP server failed" do it 'should redirect to error page' do post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) expect(last_response).to be_redirect expect(last_response.headers['Location']).to match('invalid_credentials') expect(last_request.env['omniauth.error'].message).to eq('Invalid credentials for ping') end context 'and filter is set' do it 'should bind with filter' do @adaptor.stub(:filter).and_return('uid=%{username}') Net::LDAP::Filter.should_receive(:construct).with('uid=ping') post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) expect(last_response).to be_redirect expect(last_response.headers['Location']).to match('invalid_credentials') expect(last_request.env['omniauth.error'].message).to eq('Invalid credentials for ping') end end end context "and communication with LDAP server caused an exception" do before :each do @adaptor.stub(:bind_as).and_throw(Exception.new('connection_error')) end it 'should redirect to error page' do post('/auth/ldap/callback', {:username => "ping", :password => "password"}) last_response.should be_redirect last_response.headers['Location'].should =~ %r{ldap_error} end end end end context 'success' do let(:auth_hash){ last_request.env['omniauth.auth'] } before(:each) do @adaptor.stub(:filter) @adaptor.stub(:bind_as).and_return(Net::LDAP::Entry.from_single_ldif_string( %Q{dn: cn=ping, dc=intridea, dc=com mail: ping@intridea.com givenname: Ping sn: Yu telephonenumber: 555-555-5555 mobile: 444-444-4444 uid: ping title: dev address: k street l: Washington st: DC co: U.S.A postofficebox: 20001 wwwhomepage: www.intridea.com jpegphoto: http://www.intridea.com/ping.jpg description: omniauth-ldap } )) end it 'should not redirect to error page' do post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) last_response.should_not be_redirect end context 'and filter is set' do it 'should bind with filter' do @adaptor.stub(:filter).and_return('uid=%{username}') Net::LDAP::Filter.should_receive(:construct).with('uid=ping') post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) last_response.should_not be_redirect end end it 'should map user info to Auth Hash' do post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) auth_hash.uid.should == 'cn=ping, dc=intridea, dc=com' auth_hash.info.email.should == 'ping@intridea.com' auth_hash.info.first_name.should == 'Ping' auth_hash.info.last_name.should == 'Yu' auth_hash.info.phone.should == '555-555-5555' auth_hash.info.mobile.should == '444-444-4444' auth_hash.info.nickname.should == 'ping' auth_hash.info.title.should == 'dev' auth_hash.info.location.should == 'k street, Washington, DC, U.S.A 20001' auth_hash.info.url.should == 'www.intridea.com' auth_hash.info.image.should == 'http://www.intridea.com/ping.jpg' auth_hash.info.description.should == 'omniauth-ldap' end end context 'alternate fields' do let(:auth_hash){ last_request.env['omniauth.auth'] } before(:each) do @adaptor.stub(:filter) @adaptor.stub(:bind_as).and_return(Net::LDAP::Entry.from_single_ldif_string( %Q{dn: cn=ping, dc=intridea, dc=com userprincipalname: ping@intridea.com givenname: Ping sn: Yu telephonenumber: 555-555-5555 mobile: 444-444-4444 uid: ping title: dev address: k street l: Washington st: DC co: U.S.A postofficebox: 20001 wwwhomepage: www.intridea.com jpegphoto: http://www.intridea.com/ping.jpg description: omniauth-ldap } )) end it 'should map user info to Auth Hash' do post('/auth/ldap/callback', {:username => 'ping', :password => 'password'}) auth_hash.uid.should == 'cn=ping, dc=intridea, dc=com' auth_hash.info.email.should == 'ping@intridea.com' auth_hash.info.first_name.should == 'Ping' auth_hash.info.last_name.should == 'Yu' auth_hash.info.phone.should == '555-555-5555' auth_hash.info.mobile.should == '444-444-4444' auth_hash.info.nickname.should == 'ping' auth_hash.info.title.should == 'dev' auth_hash.info.location.should == 'k street, Washington, DC, U.S.A 20001' auth_hash.info.url.should == 'www.intridea.com' auth_hash.info.image.should == 'http://www.intridea.com/ping.jpg' auth_hash.info.description.should == 'omniauth-ldap' end end end end gitlab_omniauth-ldap-2.2.0/spec/omniauth-ldap/0000755000004100000410000000000014256370317021333 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/spec/omniauth-ldap/adaptor_spec.rb0000644000004100000410000003073514256370317024334 0ustar www-datawww-datarequire 'spec_helper' describe OmniAuth::LDAP::Adaptor do describe 'initialize' do it 'should throw exception when must have field is not set' do #[:host, :port, :encryption, :bind_dn] lambda { OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain'})}.should raise_error(ArgumentError) end it 'should not throw an error if hosts is set but host and port are not' do expect { described_class.new( hosts: [['192.168.1.145', 389], ['192.168.1.146', 389]], encryption: 'plain', base: 'dc=example,dc=com', uid: 'uid' ) }.not_to raise_error(ArgumentError) end it 'should throw exception when encryption method is not supported' do lambda { OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'myplain', uid: 'uid', port: 389, base: 'dc=com'})}.should raise_error(OmniAuth::LDAP::Adaptor::ConfigurationError) end it 'should setup ldap connection with anonymous' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.should_not == nil adaptor.connection.host.should == '192.168.1.145' adaptor.connection.port.should == 389 adaptor.connection.base.should == 'dc=intridea, dc=com' adaptor.connection.instance_variable_get('@auth').should == {:method => :anonymous, :username => nil, :password => nil} end it 'should setup ldap connection with simple' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password'}) adaptor.connection.should_not == nil adaptor.connection.host.should == '192.168.1.145' adaptor.connection.port.should == 389 adaptor.connection.base.should == 'dc=intridea, dc=com' adaptor.connection.instance_variable_get('@auth').should == {:method => :simple, :username => 'bind_dn', :password => 'password'} adaptor.connection.instance_variable_get('@encryption').should == nil end it 'should setup ldap connection with sasl-md5' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', try_sasl: true, sasl_mechanisms: ["DIGEST-MD5"], bind_dn: 'bind_dn', password: 'password'}) adaptor.connection.should_not == nil adaptor.connection.host.should == '192.168.1.145' adaptor.connection.port.should == 389 adaptor.connection.base.should == 'dc=intridea, dc=com' adaptor.connection.instance_variable_get('@auth')[:method].should == :sasl adaptor.connection.instance_variable_get('@auth')[:mechanism].should == 'DIGEST-MD5' adaptor.connection.instance_variable_get('@auth')[:initial_credential].should == '' adaptor.connection.instance_variable_get('@auth')[:challenge_response].should_not be_nil end it 'should setup ldap connection with sasl-gss' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', try_sasl: true, sasl_mechanisms: ["GSS-SPNEGO"], bind_dn: 'bind_dn', password: 'password'}) adaptor.connection.should_not == nil adaptor.connection.host.should == '192.168.1.145' adaptor.connection.port.should == 389 adaptor.connection.base.should == 'dc=intridea, dc=com' adaptor.connection.instance_variable_get('@auth')[:method].should == :sasl adaptor.connection.instance_variable_get('@auth')[:mechanism].should == 'GSS-SPNEGO' adaptor.connection.instance_variable_get('@auth')[:initial_credential].should =~ /^NTLMSSP/ adaptor.connection.instance_variable_get('@auth')[:challenge_response].should_not be_nil end it 'sets up a connection with the proper host and port' do adapter = described_class.new( host: '192.168.1.145', encryption: 'plain', base: 'dc=example,dc=com', port: 3890, uid: 'uid' ) expect(adapter.connection.host).to eq('192.168.1.145') expect(adapter.connection.port).to eq(3890) expect(adapter.connection.hosts).to be_nil end it 'sets up a connection with a enumerable pairs of hosts' do adapter = described_class.new( hosts: [['192.168.1.145', 636], ['192.168.1.146', 636]], encryption: 'plain', base: 'dc=example,dc=com', uid: 'uid' ) expect(adapter.connection.host).to eq('127.0.0.1') expect(adapter.connection.port).to eq(389) expect(adapter.connection.hosts).to match_array([['192.168.1.145', 636], ['192.168.1.146', 636]]) end context 'when encryption is plain' do it 'should set encryption to nil' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should eq(nil) end end context 'when encryption is ssl' do it 'should set the encryption method to simple_tls' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include method: :simple_tls end context 'when disable_verify_certificates is not specified' do it 'should set the encryption tls_options to OpenSSL default params' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS end end context 'when disable_verify_certificates is true' do it 'should set the encryption tls_options verify_mode explicitly to verify none' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', disable_verify_certificates: true, base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } end end context 'when disable_verify_certificates is false' do it 'should set the encryption tls_options to OpenSSL default params' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', disable_verify_certificates: false, base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS end end context 'when tls_options are specified' do it 'should pass the values along with defaults' do cert = OpenSSL::X509::Certificate.new key = OpenSSL::PKey::RSA.new adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', base: 'dc=intridea, dc=com', port: 636, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password', tls_options: { ca_file: '/etc/ca.pem', ssl_version: 'TLSv1_2', cert: cert, key: key }}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge(ca_file: '/etc/ca.pem', ssl_version: 'TLSv1_2', cert: cert, key: key) end it 'does not pass nil or blank values' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', base: 'dc=intridea, dc=com', port: 636, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password', tls_options: { ca_file: nil, ssl_version: ' ' }}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS end end # DEPRECATED context 'when ca_file is specified' do it 'should set the encryption tls_options ca_file' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', base: 'dc=intridea, dc=com', port: 636, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password', ca_file: '/etc/ca.pem'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge(ca_file: '/etc/ca.pem') end end # DEPRECATED context 'when ssl_version is specified' do it 'should overwrite the encryption tls_options ssl_version' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'ssl', base: 'dc=intridea, dc=com', port: 636, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password', ssl_version: 'TLSv1_2'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.merge(ssl_version: 'TLSv1_2') end end end context 'when encryption is tls' do it 'should set the encryption method to start_tls' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'tls', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include method: :start_tls end context 'when disable_verify_certificates is not specified' do it 'should set the encryption tls_options to OpenSSL default params' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'tls', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS end end context 'when disable_verify_certificates is true' do it 'should set the encryption tls_options verify_mode explicitly to verify none' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'tls', disable_verify_certificates: true, base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } end end context 'when disable_verify_certificates is false' do it 'should set the encryption tls_options to OpenSSL default params' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'tls', disable_verify_certificates: false, base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS end end end context 'when method is set instead of encryption' do it 'should set the encryption method for backwards-compatibility' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", method: 'tls', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName'}) adaptor.connection.instance_variable_get('@encryption').should include method: :start_tls end end end describe 'bind_as' do let(:args) { {:filter => Net::LDAP::Filter.eq('sAMAccountName', 'username'), :password => 'password', :size => 1} } let(:rs) { Struct.new(:dn).new('new dn') } it 'should bind simple' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.126", encryption: 'plain', base: 'dc=score, dc=local', port: 389, uid: 'sAMAccountName', bind_dn: 'bind_dn', password: 'password'}) adaptor.connection.should_receive(:open).and_yield(adaptor.connection) adaptor.connection.should_receive(:search).with(args).and_return([rs]) adaptor.connection.should_receive(:bind).with({:username => 'new dn', :password => args[:password], :method => :simple}).and_return(true) adaptor.bind_as(args).should == rs end it 'should bind sasl' do adaptor = OmniAuth::LDAP::Adaptor.new({host: "192.168.1.145", encryption: 'plain', base: 'dc=intridea, dc=com', port: 389, uid: 'sAMAccountName', try_sasl: true, sasl_mechanisms: ["GSS-SPNEGO"], bind_dn: 'bind_dn', password: 'password'}) adaptor.connection.should_receive(:open).and_yield(adaptor.connection) adaptor.connection.should_receive(:search).with(args).and_return([rs]) adaptor.connection.should_receive(:bind).and_return(true) adaptor.bind_as(args).should == rs end end end gitlab_omniauth-ldap-2.2.0/spec/spec_helper.rb0000644000004100000410000000047114256370317021411 0ustar www-datawww-data$:.unshift File.expand_path('..', __FILE__) $:.unshift File.expand_path('../../lib', __FILE__) require 'rspec' require 'rack/test' require 'omniauth' require 'omniauth-ldap' RSpec.configure do |config| config.include Rack::Test::Methods config.extend OmniAuth::Test::StrategyMacros, :type => :strategy end gitlab_omniauth-ldap-2.2.0/.gitignore0000644000004100000410000000004514256370317017626 0ustar www-datawww-data.project .tags coverage Gemfile.lock gitlab_omniauth-ldap-2.2.0/gitlab_omniauth-ldap.gemspec0000644000004100000410000000225614256370317023275 0ustar www-datawww-data# -*- encoding: utf-8 -*- require File.expand_path('../lib/omniauth-ldap/version', __FILE__) Gem::Specification.new do |gem| gem.authors = ["Ping Yu"] gem.email = ["ping@intridea.com"] gem.description = %q{A LDAP strategy for OmniAuth.} gem.summary = %q{A LDAP strategy for OmniAuth.} gem.homepage = "https://gitlab.com/gitlab-org/omniauth-ldap" gem.license = "MIT" gem.add_runtime_dependency 'omniauth', '>= 1.3', '< 3' gem.add_runtime_dependency 'net-ldap', '~> 0.16' gem.add_runtime_dependency 'pyu-ruby-sasl', '>= 0.0.3.3', '< 0.1' gem.add_runtime_dependency 'rubyntlm', '~> 0.5' gem.add_development_dependency 'rspec', '>= 2.12' gem.add_development_dependency 'pry', '>= 0.9' gem.add_development_dependency 'rake', '>= 10.0' gem.add_development_dependency 'rack-test', '>= 0.6' gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } gem.files = `git ls-files`.split("\n") gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") gem.name = "gitlab_omniauth-ldap" gem.require_paths = ["lib"] gem.version = OmniAuth::LDAP::VERSION end gitlab_omniauth-ldap-2.2.0/Rakefile0000644000004100000410000000025614256370317017307 0ustar www-datawww-data#!/usr/bin/env rake require "bundler/gem_tasks" require 'rspec/core/rake_task' desc 'Default: run specs.' task :default => :spec desc "Run specs" RSpec::Core::RakeTask.new gitlab_omniauth-ldap-2.2.0/lib/0000755000004100000410000000000014256370317016405 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/lib/omniauth/0000755000004100000410000000000014256370317020231 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/lib/omniauth/strategies/0000755000004100000410000000000014256370317022403 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/lib/omniauth/strategies/ldap.rb0000644000004100000410000000760314256370317023656 0ustar www-datawww-datarequire 'omniauth' module OmniAuth module Strategies class LDAP include OmniAuth::Strategy InvalidCredentialsError = Class.new(StandardError) @@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 :disable_verify_certificates, false option :ca_file, nil option :ssl_version, nil # use OpenSSL default if nil 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!(:invalid_request_method) unless valid_request_method? return fail!(:missing_credentials) if missing_credentials? begin @ldap_user_info = @adaptor.bind_as(:filter => filter(@adaptor), :size => 1, :password => request['password']) unless @ldap_user_info return fail!(:invalid_credentials, InvalidCredentialsError.new("Invalid credentials for #{request['username']}")) end @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? username = Net::LDAP::Filter.escape(@options[:name_proc].call(request['username'])) Net::LDAP::Filter.construct(adaptor.filter % { username: username }) else Net::LDAP::Filter.equals(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 valid_request_method? request.env['REQUEST_METHOD'] == 'POST' end 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' gitlab_omniauth-ldap-2.2.0/lib/omniauth-ldap/0000755000004100000410000000000014256370317021147 5ustar www-datawww-datagitlab_omniauth-ldap-2.2.0/lib/omniauth-ldap/version.rb0000644000004100000410000000007614256370317023164 0ustar www-datawww-datamodule OmniAuth module LDAP VERSION = "2.2.0" end end gitlab_omniauth-ldap-2.2.0/lib/omniauth-ldap/adaptor.rb0000644000004100000410000001720714256370317023135 0ustar www-datawww-data#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 = [ :hosts, :host, :port, :encryption, :disable_verify_certificates, :bind_dn, :password, :try_sasl, :sasl_mechanisms, :uid, :base, :allow_anonymous, :filter, :tls_options, # Deprecated :method, :ca_file, :ssl_version ] # A list of needed keys. Possible alternatives are specified using sub-lists. MUST_HAVE_KEYS = [ :base, [:encryption, :method], # :method is deprecated [:hosts, :host], [:hosts, :port], [:uid, :filter] ] ENCRYPTION_METHOD = { :simple_tls => :simple_tls, :start_tls => :start_tls, :plain => nil, # Deprecated. This mapping aimed to be user-friendly, but only caused # confusion. Better to pass-through the actual `Net::LDAP` encryption type. :ssl => :simple_tls, :tls => :start_tls, } 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 config = { base: @base, hosts: @hosts, host: @host, port: @port, encryption: encryption_options } @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 @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 encryption_options translated_method = translate_method return nil unless translated_method { method: translated_method, tls_options: tls_options(translated_method) } end def translate_method method = @encryption || @method method ||= "plain" normalized_method = method.to_s.downcase.to_sym unless ENCRYPTION_METHOD.has_key?(normalized_method) available_methods = ENCRYPTION_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 ENCRYPTION_METHOD[normalized_method] end def tls_options(translated_method) return {} if translated_method == nil # (plain) options = default_options if @tls_options # Prevent blank config values from overwriting SSL defaults configured_options = sanitize_hash_values(@tls_options) configured_options = symbolize_hash_keys(configured_options) options.merge!(configured_options) end # Retain backward compatibility until deprecated configs are removed. options[:ca_file] = @ca_file if @ca_file options[:ssl_version] = @ssl_version if @ssl_version options 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 private def default_options if @disable_verify_certificates # It is important to explicitly set verify_mode for two reasons: # 1. The behavior of OpenSSL is undefined when verify_mode is not set. # 2. The net-ldap gem implementation verifies the certificate hostname # unless verify_mode is set to VERIFY_NONE. { verify_mode: OpenSSL::SSL::VERIFY_NONE } else OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.dup end end # Removes keys that have blank values # # This gem may not always be in the context of Rails so we # do this rather than `.blank?`. def sanitize_hash_values(hash) hash.delete_if do |_, value| value.nil? || (value.is_a?(String) && value !~ /\S/) end end def symbolize_hash_keys(hash) hash.keys.each do |key| hash[key.to_sym] = hash[key] end hash end end end end gitlab_omniauth-ldap-2.2.0/lib/omniauth-ldap.rb0000644000004100000410000000014414256370317021473 0ustar www-datawww-datarequire "omniauth-ldap/version" require "omniauth-ldap/adaptor" require 'omniauth/strategies/ldap' gitlab_omniauth-ldap-2.2.0/Guardfile0000644000004100000410000000036214256370317017465 0ustar www-datawww-dataguard 'rspec', :version => 2 do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } end guard 'bundler' do watch('Gemfile') watch(/^.+\.gemspec/) end gitlab_omniauth-ldap-2.2.0/Gemfile0000644000004100000410000000004614256370317017132 0ustar www-datawww-datasource 'http://rubygems.org' gemspec gitlab_omniauth-ldap-2.2.0/CHANGELOG0000644000004100000410000000047514256370317017057 0ustar www-datawww-data## 2.1.1 - Add a String check to `tls_options` sanitization to allow other objects ## 2.1.0 - Expose `:tls_options` SSL configuration option. Deprecate :ca_file, :ssl_version ## 2.0.4 - Improve log message when invalid credentials are used ## 2.0.3 - Protects against wrong request method call to callback gitlab_omniauth-ldap-2.2.0/.gitlab-ci.yml0000644000004100000410000000036114256370317020273 0ustar www-datawww-datadefault: image: "ruby:${RUBY_VERSION}" stages: - test .test-template: &test before_script: - bundle install script: - bundle exec rake spec rspec: parallel: matrix: - RUBY_VERSION: [ "2.7", "3.0" ] <<: *test