diff --git a/helios_auth/auth_systems/__init__.py b/helios_auth/auth_systems/__init__.py
index 5a0e9233ba4df89067688283e90649aef4f1ae70..aaaddb56b6b5a29d4a414b4c34b2ba4a11278085 100644
--- a/helios_auth/auth_systems/__init__.py
+++ b/helios_auth/auth_systems/__init__.py
@@ -2,6 +2,9 @@
 AUTH_SYSTEMS = {}
 
 import twitter, password, cas, facebook, google, yahoo, linkedin, clever
+import ldapauth
+
+
 AUTH_SYSTEMS['twitter'] = twitter
 AUTH_SYSTEMS['linkedin'] = linkedin
 AUTH_SYSTEMS['password'] = password
@@ -10,6 +13,7 @@ AUTH_SYSTEMS['facebook'] = facebook
 AUTH_SYSTEMS['google'] = google
 AUTH_SYSTEMS['yahoo'] = yahoo
 AUTH_SYSTEMS['clever'] = clever
+AUTH_SYSTEMS['ldap'] = ldapauth
 
 # not ready
 #import live
diff --git a/helios_auth/auth_systems/ldapauth.py b/helios_auth/auth_systems/ldapauth.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e201f94ce2fef763c8891ea3ca57240fac76fb9
--- /dev/null
+++ b/helios_auth/auth_systems/ldapauth.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+"""
+LDAP Authentication
+Author : shirlei@gmail.com
+Version: 1.0
+Requires libldap2-dev
+django-auth-ldap 1.2.7
+LDAP authentication relies on django-auth-ldap (http://pythonhosted.org/django-auth-ldap/),
+which considers that "Authenticating against an external source is swell, but Django’s
+auth module is tightly bound to a user model. When a user logs in, we have to create a model
+object to represent them in the database."
+Helios, originally, does not rely on default django user model. Discussion about that can be
+found in:
+https://groups.google.com/forum/#!topic/helios-voting/nRHFAbAHTNA
+That considered, using a django plugin for ldap authentication, in order to not reinvent the
+wheel seems ok, since it does not alter anything on original helios user model, it is just
+for authentication purposes.
+However, two installed_apps that are added when you first create a django project, which were
+commented out in helios settings, need to be made available now:
+django.contrib.auth
+django.contrib.contenttypes'
+This will enable the native django authentication support on what django-auth-ldap is build upon.
+Further reference on
+https://docs.djangoproject.com/en/1.8/topics/auth/
+"""
+
+from django import forms
+from django.conf import settings
+from django.core.mail import send_mail
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+
+
+from helios_auth.auth_systems.ldapbackend import backend
+
+
+# some parameters to indicate that status updating is possible
+STATUS_UPDATES = False
+
+
+LOGIN_MESSAGE = "Log in with my LDAP Account"
+
+class LoginForm(forms.Form):
+    username = forms.CharField(max_length=250)
+    password = forms.CharField(widget=forms.PasswordInput(), max_length=100)
+
+
+def ldap_login_view(request):
+    from helios_auth.view_utils import render_template
+    from helios_auth.views import after
+
+    error = None
+
+    if request.method == "GET":
+            form = LoginForm()
+    else:
+            form = LoginForm(request.POST)
+
+            request.session['auth_system_name'] = 'ldap'
+
+            if request.POST.has_key('return_url'):
+                request.session['auth_return_url'] = request.POST.get('return_url')
+
+            if form.is_valid():
+                username = form.cleaned_data['username'].strip()
+                password = form.cleaned_data['password'].strip()
+
+                auth = backend.CustomLDAPBackend()
+                user = auth.authenticate(username, password)
+
+                if user:
+                    request.session['ldap_user']  = {
+                        'user_id': user.email,
+                        'name': user.first_name + ' ' + user.last_name,
+                    }
+                    return HttpResponseRedirect(reverse(after))
+                else:
+                    error = 'Bad Username or Password'
+
+    return render_template(request, 'password/login', {
+            'form': form,
+            'error': error,
+            'enabled_auth_systems': settings.AUTH_ENABLED_AUTH_SYSTEMS,
+        })
+
+
+def get_user_info_after_auth(request):
+    return {
+       'type': 'ldap',
+       'user_id' : request.session['ldap_user']['user_id'],
+       'name': request.session['ldap_user']['name'],
+       'info': {'email': request.session['ldap_user']['user_id']},
+       'token': None
+    }
+
+
+def get_auth_url(request, redirect_url = None):
+    return reverse(ldap_login_view)
+
+
+def send_message(user_id, name, user_info, subject, body):
+    send_mail(subject, body, settings.SERVER_EMAIL, ["%s <%s>" % (name, user_id)], fail_silently=False)
+
+
+def check_constraint(constraint, user_info):
+    """
+    for eligibility
+    """
+    pass
+
+def can_create_election(user_id, user_info):
+  return True
+
diff --git a/helios_auth/auth_systems/ldapbackend/__init__.py b/helios_auth/auth_systems/ldapbackend/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/helios_auth/auth_systems/ldapbackend/backend.py b/helios_auth/auth_systems/ldapbackend/backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..5d3060120b0dc860e61d942a2a29715fa11ca999
--- /dev/null
+++ b/helios_auth/auth_systems/ldapbackend/backend.py
@@ -0,0 +1,28 @@
+from django.core.exceptions import ImproperlyConfigured
+
+from django_auth_ldap.backend import LDAPBackend
+from django_auth_ldap.config import LDAPSearch
+from django_auth_ldap.backend import populate_user
+
+
+class CustomLDAPBackend(LDAPBackend):
+    def authenticate(self, username, password):
+        """
+        Some ldap servers allow anonymous search, returning just a subset
+        of user attributes. So here we re-perform search after user is authenticated,
+        in order to populate other user attributes.
+        For now, just in cases where AUTH_LDAP_BIND_PASSWORD is empty
+        """
+        user =  super(CustomLDAPBackend, self).authenticate(username, password)
+
+        if user and self.settings.BIND_PASSWORD == '' :
+            search = self.settings.USER_SEARCH
+            if search is None:
+                raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.')
+            results = search.execute(user.ldap_user.connection, {'user': user.username})
+            if results is not None and len(results) == 1:
+                (user.ldap_user._user_dn, user.ldap_user.user_attrs) = results[0]
+                user.ldap_user._load_user_attrs()
+                user.ldap_user._populate_user_from_attributes()
+                user.save()
+        return user
diff --git a/helios_auth/media/login-icons/ldap.png b/helios_auth/media/login-icons/ldap.png
new file mode 100644
index 0000000000000000000000000000000000000000..86f7807cbcf9f8d5add2c9450a1cf74b30ff1cb6
Binary files /dev/null and b/helios_auth/media/login-icons/ldap.png differ
diff --git a/helios_auth/tests.py b/helios_auth/tests.py
index f07f309fb5176007ca67a25ce14c712efc33af35..b6cb5ddd6ceb77ba0e13dfdac783fa16dcb08e99 100644
--- a/helios_auth/tests.py
+++ b/helios_auth/tests.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 """
 Unit Tests for Auth Systems
 """
@@ -13,6 +14,7 @@ from django.test import TestCase
 from django.core import mail
 
 from auth_systems import AUTH_SYSTEMS
+from helios_auth import ENABLED_AUTH_SYSTEMS
 
 class UserModelTests(unittest.TestCase):
 
@@ -25,10 +27,10 @@ class UserModelTests(unittest.TestCase):
         """
         for auth_system, auth_system_module in AUTH_SYSTEMS.iteritems():
             models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name':'Foo Bar'})
-            
+
             def double_insert():
                 models.User.objects.create(user_type = auth_system, user_id = 'foobar', info={'name': 'Foo2 Bar'})
-                
+
             self.assertRaises(IntegrityError, double_insert)
             transaction.rollback()
 
@@ -55,7 +57,7 @@ class UserModelTests(unittest.TestCase):
             assert(hasattr(auth_system_module, 'can_create_election'))
             if auth_system != 'clever':
                 assert(auth_system_module.can_create_election('foobar', {}))
-        
+
 
     def test_status_update(self):
         """
@@ -117,7 +119,7 @@ class UserBlackboxTests(TestCase):
 
     def test_logout(self):
         response = self.client.post(reverse(views.logout), follow=True)
-        
+
         self.assertContains(response, "not logged in")
         self.assertNotContains(response, "Foobar User")
 
@@ -128,3 +130,197 @@ class UserBlackboxTests(TestCase):
         self.assertEquals(len(mail.outbox), 1)
         self.assertEquals(mail.outbox[0].subject, "testing subject")
         self.assertEquals(mail.outbox[0].to[0], "\"Foobar User\" <foobar-test@adida.net>")
+
+
+'''
+    tests for LDAP auth module.
+    Much of the code below was get or inspired on django-auth-ldap package.
+    See site-packages/django_ldap_auth/tests.py
+'''
+import ldap
+
+import auth_systems.ldapauth as ldap_views
+from mockldap import MockLdap
+from django_auth_ldap.backend import LDAPSettings
+from django_auth_ldap.config import LDAPSearch
+from helios_auth.auth_systems.ldapbackend.backend import CustomLDAPBackend
+try:
+    from django.test.utils import override_settings
+except ImportError:
+    override_settings = lambda *args, **kwargs: (lambda v: v)
+
+
+class TestSettings(LDAPSettings):
+    """
+    A replacement for backend.LDAPSettings that does not load settings
+    from django.conf.
+    """
+    def __init__(self, **kwargs):
+        for name, default in self.defaults.items():
+            value = kwargs.get(name, default)
+            setattr(self, name, value)
+
+
+class LDAPAuthTests(TestCase):
+    """
+    These tests uses mockldap 0.2.7 https://pypi.python.org/pypi/mockldap/
+    """
+
+    top = ("o=test", {"o": "test"})
+    people = ("ou=people,o=test", {"ou": "people"})
+    groups = ("ou=groups,o=test", {"ou": "groups"})
+
+    alice = ("uid=alice,ou=people,o=test", {
+        "uid": ["alice"],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "uidNumber": ["1000"],
+        "gidNumber": ["1000"],
+        "givenName": ["Alice"],
+        "sn": ["Adams"],
+        "mail": ["alice@example.com"]
+    })
+    bob = ("uid=bob,ou=people,o=test", {
+        "uid": ["bob"],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "uidNumber": ["1001"],
+        "gidNumber": ["50"],
+        "givenName": ["Robert"],
+        "sn": ["Barker"],
+        "mail": ["bob@example.com"]
+    })
+    john = ("uid=john,ou=people,o=test", {
+        "uid": ["john"],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "uidNumber": ["1002"],
+        "gidNumber": ["60"],
+        "givenName": ["Robert"],
+        "sn": ["Doe"]
+    })
+
+    directory = dict([top, people, groups, alice, bob, john])
+
+    def _init_settings(self, **kwargs):
+        self.backend.settings = TestSettings(**kwargs)
+
+    @classmethod
+    def setUpClass(cls):
+        cls.mockldap = MockLdap(cls.directory)
+
+    @classmethod
+    def tearDownClass(cls):
+        del cls.mockldap
+
+    def setUp(self):
+        self.mockldap.start()
+        self.ldapobj = self.mockldap['ldap://localhost']
+
+        self.backend = CustomLDAPBackend()
+        self.backend.ldap  # Force global configuration
+
+    def tearDown(self):
+        self.mockldap.stop()
+        del self.ldapobj
+
+    def test_backend_login(self):
+        """ Test authentication usign correct username/password """
+        if 'ldap' in ENABLED_AUTH_SYSTEMS:
+            self._init_settings(
+                BIND_DN='uid=bob,ou=people,o=test',
+                BIND_PASSWORD='password',
+                USER_SEARCH=LDAPSearch(
+                    "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+                )
+            )
+            user = self.backend.authenticate(username='alice', password='password')
+            self.assertTrue(user is not None)
+
+    def test_backend_bad_login(self):
+        """ Test authentication using incorrect username/password"""
+        if 'ldap' in ENABLED_AUTH_SYSTEMS:
+            self._init_settings(
+                BIND_DN='uid=bob,ou=people,o=test',
+                BIND_PASSWORD='password',
+                USER_SEARCH=LDAPSearch(
+                    "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+                )
+            )
+            user = self.backend.authenticate(username='maria', password='password')
+            self.assertTrue(user is None)
+
+    @override_settings(AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test',
+        AUTH_LDAP_BIND_PASSWORD='password',AUTH_LDAP_USER_SEARCH=LDAPSearch(
+            "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+        )
+    )
+    def test_ldap_view_login_with_bind_credentials(self):
+        """ Test if authenticates using the auth system login view """
+        if 'ldap' in ENABLED_AUTH_SYSTEMS:
+            response = self.client.post(reverse(ldap_views.ldap_login_view), {
+                'username' : 'bob',
+                'password': 'password'
+            }, follow=True)
+            self.assertEqual(self.client.session['user']['name'], 'Robert Barker')
+            self.assertEqual(self.client.session['user']['type'], 'ldap')
+            self.assertEqual(self.client.session['user']['info']['email'],'bob@example.com')
+
+    @override_settings(AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test',
+        AUTH_LDAP_BIND_PASSWORD='password',AUTH_LDAP_USER_SEARCH=LDAPSearch(
+            "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+        )
+    )
+    def test_ldap_view_login_with_bad_password(self):
+        """ Test if given a wrong password the user can't login """
+        if 'ldap' in ENABLED_AUTH_SYSTEMS:
+            response = self.client.post(reverse(ldap_views.ldap_login_view), {
+                'username' : 'john',
+                'password': 'passworddd'
+            }, follow=True)
+            self.assertEqual(response.status_code, 200)
+            self.assertFalse(self.client.session.has_key('user'))
+
+    def test_ldap_view_login_anonymous_bind(self):
+        """
+        Test anonymous search/bind
+        See https://pythonhosted.org/django-auth-ldap/authentication.html#search-bind
+        """
+        self._init_settings(
+            BIND_PASSWORD='',
+            USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn','email':'mail'},
+            USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+            )
+        )
+        user = self.backend.authenticate(username='alice', password='password')
+        self.assertEqual(user.username, 'alice')
+        self.assertEqual(user.first_name, 'Alice')
+        self.assertEqual(user.last_name, 'Adams')
+        self.assertEqual(user.email,'alice@example.com')
+
+    def test_ldap_bind_as_user(self):
+        """
+        Test direct bind
+        See https://pythonhosted.org/django-auth-ldap/authentication.html#direct-bind
+        """
+        self._init_settings(
+            USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn','email':'mail'},
+            BIND_AS_AUTHENTICATING_USER=True,
+            USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+            )
+        )
+        user = self.backend.authenticate(username='alice', password='password')
+        self.assertEqual(user.username, 'alice')
+        self.assertEqual(user.first_name, 'Alice')
+        self.assertEqual(user.last_name, 'Adams')
+        self.assertEqual(user.email,'alice@example.com')
+
+    def test_logout(self):
+        """ test logging out using the auth system logout view """
+        if 'ldap' in ENABLED_AUTH_SYSTEMS:
+            response = self.client.post(reverse(views.logout), follow=True)
+            self.assertContains(response, "not logged in")
+            self.assertNotContains(response, "alice")
diff --git a/helios_auth/urls.py b/helios_auth/urls.py
index e4dca398503d1e2f802fd4811330b66b9020a1a9..810d4f8f9e054bd413687e1cbde6af05c34f9bf7 100644
--- a/helios_auth/urls.py
+++ b/helios_auth/urls.py
@@ -9,6 +9,8 @@ from django.conf.urls import *
 from views import *
 from auth_systems.password import password_login_view, password_forgotten_view
 from auth_systems.twitter import follow_view
+from auth_systems.ldapauth import ldap_login_view
+
 
 urlpatterns = patterns('',
     # basic static stuff
@@ -28,4 +30,7 @@ urlpatterns = patterns('',
 
     # twitter
     (r'^twitter/follow', follow_view),
+
+    #ldap
+    (r'^ldap/login', ldap_login_view),
 )
diff --git a/requirements.txt b/requirements.txt
index 2cc4eb872cc1522a3d333dceee03caf92aa9d8d9..3d493a5ad09c5e9870ff631ff1f5ad48956cc60a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,3 +22,5 @@ boto==2.27.0
 django-ses==0.6.0
 validate_email==1.2
 oauth2client==1.2
+django-auth-ldap==1.2.7
+mockldap==0.2.7
diff --git a/settings.py b/settings.py
index 1e20e2ffde7d3a297097d5b2c4f4e9d3b2ad73e7..1a04cce9ad3ef66d87051b64723181439dc9f9bc 100644
--- a/settings.py
+++ b/settings.py
@@ -1,6 +1,7 @@
-
+import ldap
 import os, json
 
+from django_auth_ldap.config import LDAPSearch, GroupOfNamesType
 # a massive hack to see if we're testing, in which case we use different settings
 import sys
 TESTING = 'test' in sys.argv
@@ -134,8 +135,8 @@ TEMPLATE_DIRS = (
 )
 
 INSTALLED_APPS = (
-#    'django.contrib.auth',
-#    'django.contrib.contenttypes',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
     'djangosecure',
     'django.contrib.sessions',
     #'django.contrib.sites',
@@ -201,7 +202,7 @@ HELIOS_VOTERS_EMAIL = True
 HELIOS_PRIVATE_DEFAULT = False
 
 # authentication systems enabled
-#AUTH_ENABLED_AUTH_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo']
+#AUTH_ENABLED_AUTH_SYSTEMS = ['password','facebook','twitter', 'google', 'yahoo','ldap']
 AUTH_ENABLED_AUTH_SYSTEMS = get_from_env('AUTH_ENABLED_AUTH_SYSTEMS', 'google').split(",")
 AUTH_DEFAULT_AUTH_SYSTEM = get_from_env('AUTH_DEFAULT_AUTH_SYSTEM', None)
 
@@ -237,6 +238,25 @@ CAS_ELIGIBILITY_REALM = get_from_env('CAS_ELIGIBILITY_REALM', "")
 CLEVER_CLIENT_ID = get_from_env('CLEVER_CLIENT_ID', "")
 CLEVER_CLIENT_SECRET = get_from_env('CLEVER_CLIENT_SECRET', "")
 
+# ldap
+# see configuration example at https://pythonhosted.org/django-auth-ldap/example.html
+AUTH_LDAP_SERVER_URI = "ldap://localhost" # replace by your Ldap URI
+AUTH_LDAP_BIND_DN = "cn=read-only-admin,dc=example,dc=com"
+AUTH_LDAP_BIND_PASSWORD = "password"
+AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com",
+    ldap.SCOPE_SUBTREE, "(uid=%(user)s)"
+)
+# Populate the Django user from the LDAP directory.
+AUTH_LDAP_USER_ATTR_MAP = {
+    "first_name": "givenName",
+    "last_name": "sn",
+    "email": "mail",
+}
+
+AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True
+
+AUTH_LDAP_ALWAYS_UPDATE_USER = False
+
 # email server
 EMAIL_HOST = get_from_env('EMAIL_HOST', 'localhost')
 EMAIL_PORT = int(get_from_env('EMAIL_PORT', "2525"))