diff --git a/requirements.txt b/requirements.txt
index f0cc71715..7f5f5f2cc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -39,3 +39,4 @@ python-dateutil>=2.8.0
 SQLAlchemy>=1.3.13
 Flask-Security-Too>=3.0.0
 sshtunnel>=0.1.4
+ldap3>=2.5.1
diff --git a/web/config.py b/web/config.py
index f508f8fc6..3343f4b6d 100644
--- a/web/config.py
+++ b/web/config.py
@@ -488,6 +488,65 @@ MASTER_PASSWORD_REQUIRED = True
 ##########################################################################
 ENHANCED_COOKIE_PROTECTION = True
 
+##########################################################################
+# External Authentication Sources
+##########################################################################
+
+# Default setting is internal
+# External Supported Sources: ldap
+# Multiple authentication can be achieved by setting this parameter to
+# ['ldap', 'internal']. pgAdmin will authenticate the user with ldap first,
+# in case of failure internal authentication will be done.
+
+AUTHENTICATION_SOURCES = ['internal']
+
+##########################################################################
+# LDAP Configuration
+##########################################################################
+
+# After ldap authentication, user will be added into the SQLite database
+# automatically, if set to True.
+# Set it to False, if user should not be added automatically,
+# in this case Admin has to add the user manually in the SQLite database.
+
+LDAP_AUTO_CREATE_USER = True
+
+# Connection timeout
+LDAP_CONNECTION_TIMEOUT = 10
+
+# Server connection details (REQUIRED)
+# example: ldap://<ip-address>:<port> or ldap://<hostname>:<port>
+LDAP_SERVER_URI = 'ldap://<ip-address>:<port>'
+
+# BaseDN (REQUIRED)
+# AD example:
+# (&(objectClass=user)(memberof=CN=MYGROUP,CN=Users,dc=example,dc=com))
+# OpenLDAP example: CN=Users,dc=example,dc=com
+LDAP_BASE_DN = '<Base-DN>'
+
+# The LDAP attribute containing user names. In OpenLDAP, this may be 'uid'
+# whilst in AD, 'sAMAccountName' might be appropriate. (REQUIRED)
+LDAP_USERNAME_ATTRIBUTE = '<User-id>'
+
+# Search ldap for further authentication
+LDAP_SEARCH_BASE_DN = '<Search-Base-DN>'
+
+# Filter string for the user search.
+# For OpenLDAP, '(cn=*)' may well be enough.
+# For AD, you might use '(objectClass=user)' (REQUIRED)
+LDAP_SEARCH_FILTER = '(objectclass=*)'
+
+# Search scope for users (one of BASE, LEVEL or SUBTREE)
+LDAP_SEARCH_SCOPE = 'SUBTREE'
+
+# Use TLS? If the URI scheme is ldaps://, this is ignored.
+LDAP_USE_STARTTLS = False
+
+# TLS/SSL certificates. Specify if required, otherwise leave empty
+LDAP_CA_CERT_FILE = ''
+LDAP_CERT_FILE = ''
+LDAP_KEY_FILE = ''
+
 ##########################################################################
 # Local config settings
 ##########################################################################
diff --git a/web/migrations/versions/7fedf8531802_.py b/web/migrations/versions/7fedf8531802_.py
new file mode 100644
index 000000000..89401686a
--- /dev/null
+++ b/web/migrations/versions/7fedf8531802_.py
@@ -0,0 +1,51 @@
+
+"""empty message
+
+Revision ID: 7fedf8531802
+Revises: aff1436e3c8c
+Create Date: 2020-02-26 11:24:54.353288
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from pgadmin.model import db
+
+# revision identifiers, used by Alembic.
+revision = '7fedf8531802'
+down_revision = 'aff1436e3c8c'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+
+    db.engine.execute("ALTER TABLE user RENAME TO user_old")
+
+    db.engine.execute("""
+        CREATE TABLE user (
+            id INTEGER NOT NULL,
+            username VARCHAR(256) NOT NULL,
+            email VARCHAR(256),
+            password VARCHAR(256),
+            active BOOLEAN NOT NULL,
+            confirmed_at DATETIME,
+            masterpass_check VARCHAR(256),
+            auth_source VARCHAR(256) NOT NULL DEFAULT 'internal',
+            PRIMARY KEY (id),
+            UNIQUE (username, auth_source),
+            CHECK (active IN (0, 1))
+        );
+        """)
+
+    db.engine.execute("""
+        INSERT INTO user (
+            id, username, email, password, active, confirmed_at, masterpass_check
+        ) SELECT
+            id, email, email, password, active, confirmed_at, masterpass_check
+        FROM user_old""")
+
+    db.engine.execute("DROP TABLE user_old")
+
+
+def downgrade():
+    pass
diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py
index 81ef6c396..45d33b72d 100644
--- a/web/pgAdmin4.py
+++ b/web/pgAdmin4.py
@@ -160,6 +160,18 @@ if 'PGADMIN_INT_KEY' in globals():
 else:
     app.PGADMIN_INT_KEY = ''
 
+# Authentication sources
+app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
+app.PGADMIN_SUPPORTED_AUTH_SOURCE = ['internal', 'ldap']
+
+if len(config.AUTHENTICATION_SOURCES) > 0:
+    app.PGADMIN_EXTERNAL_AUTH_SOURCE = config.AUTHENTICATION_SOURCES[0]
+else:
+    app.PGADMIN_EXTERNAL_AUTH_SOURCE = app.PGADMIN_DEFAULT_AUTH_SOURCE
+
+app.logger.debug(
+    "Authentication Source: %s" % app.PGADMIN_DEFAULT_AUTH_SOURCE)
+
 # Output a startup message if we're not under the runtime and startup.
 # If we're under WSGI, we don't need to worry about this
 if __name__ == '__main__':
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index 820c8015a..5ca2ae67a 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -38,7 +38,7 @@ from datetime import timedelta
 from pgadmin.setup import get_version, set_version
 from pgadmin.utils.ajax import internal_server_error
 from pgadmin.utils.csrf import pgCSRFProtect
-
+from pgadmin import authenticate
 
 # If script is running under python3, it will not have the xrange function
 # defined
@@ -398,6 +398,7 @@ def create_app(app_name=None):
     # Load all available server drivers
     ##########################################################################
     driver.init_app(app)
+    authenticate.init_app(app)
 
     ##########################################################################
     # Register language to the preferences after login
diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py
new file mode 100644
index 000000000..63f524e23
--- /dev/null
+++ b/web/pgadmin/authenticate/__init__.py
@@ -0,0 +1,156 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""A blueprint module implementing the Authentication."""
+
+import flask
+import pickle
+from flask import current_app, flash
+from flask_babelex import gettext
+from flask_security import current_user
+from flask_security.views import _security, _ctx
+from flask_security.utils import config_value, get_post_logout_redirect
+from flask import session
+
+import config
+from pgadmin.utils import PgAdminModule
+from .registry import AuthSourceRegistry
+
+MODULE_NAME = 'authenticate'
+
+
+class AuthenticateModule(PgAdminModule):
+    def get_exposed_url_endpoints(self):
+        return ['authenticate.login']
+
+
+blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
+
+
+@blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
+def login():
+    """
+    Entry point for all the authentication sources.
+    The user input will be validated and authenticated.
+    """
+    form = _security.login_form()
+    auth_obj = AuthSourceManager(form, config.AUTHENTICATION_SOURCES)
+    session['_auth_source_manager_obj'] = None
+
+    # Validate the user
+    if not auth_obj.validate():
+        for field in form.errors:
+            for error in form.errors[field]:
+                flash(error, 'warning')
+            return flask.redirect(get_post_logout_redirect())
+
+    # Authenticate the user
+    status, msg = auth_obj.authenticate()
+    if status:
+        # Login the user
+        status, msg = auth_obj.login()
+        if not status:
+            flash(gettext(msg), 'danger')
+            return flask.redirect(get_post_logout_redirect())
+
+        session['_auth_source_manager_obj'] = auth_obj.as_dict()
+        return flask.redirect('/')
+
+    flash(gettext(msg), 'danger')
+    return flask.redirect(get_post_logout_redirect())
+
+
+class AuthSourceManager():
+    """This class will manage all the authentication sources.
+     """
+    def __init__(self, form, sources):
+        self.form = form
+        self.auth_sources = sources
+        self.source = None
+        self.source_friendly_name = None
+
+    def as_dict(self):
+        """
+        Returns the dictionary object representing this object.
+        """
+
+        res = dict()
+        res['source_friendly_name'] = self.source_friendly_name
+        res['auth_sources'] = self.auth_sources
+
+        return res
+
+    def set_source(self, source):
+        self.source = source
+
+    @property
+    def get_source(self):
+        return self.source
+
+    def set_source_friendly_name(self, name):
+        self.source_friendly_name = name
+
+    @property
+    def get_source_friendly_name(self):
+        return self.source_friendly_name
+
+    def validate(self):
+        """Validate through all the sources."""
+        for src in self.auth_sources:
+            source = get_auth_sources(src)
+            if source.validate(self.form):
+                return True
+        return False
+
+    def authenticate(self):
+        """Authenticate through all the sources."""
+        status = False
+        msg = None
+        for src in self.auth_sources:
+            source = get_auth_sources(src)
+            status, msg = source.authenticate(self.form)
+            if status:
+                self.set_source(source)
+                return status, msg
+        return status, msg
+
+    def login(self):
+        status, msg = self.source.login(self.form)
+        if status:
+            self.set_source_friendly_name(self.source.get_friendly_name())
+        return status, msg
+
+
+def get_auth_sources(type):
+    """Get the authenticated source object from the registry"""
+
+    auth_sources = getattr(current_app, '_pgadmin_auth_sources', None)
+
+    if auth_sources is None or not isinstance(auth_sources, dict):
+        auth_sources = dict()
+
+    if type in auth_sources:
+        return auth_sources[type]
+
+    auth_source = AuthSourceRegistry.create(type)
+
+    if auth_source is not None:
+        auth_sources[type] = auth_source
+        setattr(current_app, '_pgadmin_auth_sources', auth_sources)
+
+    return auth_source
+
+
+def init_app(app):
+    auth_sources = dict()
+
+    setattr(app, '_pgadmin_auth_sources', auth_sources)
+    AuthSourceRegistry.load_auth_sources()
+
+    return auth_sources
diff --git a/web/pgadmin/authenticate/internal.py b/web/pgadmin/authenticate/internal.py
new file mode 100644
index 000000000..62032f4e2
--- /dev/null
+++ b/web/pgadmin/authenticate/internal.py
@@ -0,0 +1,98 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Implements Internal Authentication"""
+
+import six
+from flask import current_app
+from flask_security import login_user
+from abc import abstractmethod, abstractproperty
+from flask_babelex import gettext
+
+from .registry import AuthSourceRegistry
+from pgadmin.model import User
+
+
+@six.add_metaclass(AuthSourceRegistry)
+class BaseAuthentication(object):
+
+    DEFAULT_MSG = {
+        'USER_DOES_NOT_EXIST': 'Specified user does not exist',
+        'LOGIN_FAILED': 'Login failed',
+        'EMAIL_NOT_PROVIDED': 'Email/Username not provided',
+        'PASSWORD_NOT_PROVIDED': 'Password not provided'
+    }
+
+    @abstractproperty
+    def get_friendly_name(cls):
+        pass
+
+    @abstractmethod
+    def authenticate(cls):
+        pass
+
+    def validate(self, form):
+        username = form.data['email']
+        password = form.data['password']
+
+        if username is None or username == '':
+            form.email.errors = list(form.email.errors)
+            form.email.errors.append(gettext(
+                self.messages('EMAIL_NOT_PROVIDED')))
+            return False
+        if password is None or password == '':
+            form.password.errors = list(form.password.errors)
+            form.password.errors.append(
+                self.messages('PASSWORD_NOT_PROVIDED'))
+            return False
+
+        return True
+
+    def login(self, form):
+        username = form.data['email']
+        user = getattr(form, 'user', None)
+
+        if user is None:
+            user = User.query.filter_by(username=username).first()
+
+        if user is None:
+            current_app.logger.exception(
+                self.messages('USER_DOES_NOT_EXIST'))
+            return False, self.messages('USER_DOES_NOT_EXIST')
+
+        # Login user through flask_security
+        status = login_user(user)
+        if not status:
+            current_app.logger.exception(self.messages('LOGIN_FAILED'))
+            return False, self.messages('LOGIN_FAILED')
+        return True, None
+
+    def messages(self, msg_key):
+        return self.DEFAULT_MSG[msg_key] if msg_key in self.DEFAULT_MSG\
+            else None
+
+
+class InternalAuthentication(BaseAuthentication):
+
+    def get_friendly_name(cls):
+        return gettext("internal")
+
+    def validate(self, form):
+        """User validation"""
+
+        # Flask security validation
+        return form.validate_on_submit()
+
+    def authenticate(self, form):
+        username = form.data['email']
+        user = getattr(form, 'user',
+                       User.query.filter_by(username=username).first())
+        if user and user.is_authenticated and form.validate_on_submit():
+            return True, None
+        return False, self.messages('USER_DOES_NOT_EXIST')
diff --git a/web/pgadmin/authenticate/ldap.py b/web/pgadmin/authenticate/ldap.py
new file mode 100644
index 000000000..1e83e2b62
--- /dev/null
+++ b/web/pgadmin/authenticate/ldap.py
@@ -0,0 +1,183 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""A blueprint module implementing the ldap authentication."""
+
+import ssl
+import config
+from ldap3 import Connection, Server, Tls, ALL, ALL_ATTRIBUTES
+from ldap3.core.exceptions import LDAPSocketOpenError, LDAPBindError,\
+    LDAPInvalidScopeError, LDAPAttributeError, LDAPInvalidFilterError,\
+    LDAPStartTLSError
+from flask_babelex import gettext
+
+from .internal import BaseAuthentication
+from pgadmin.model import User, ServerGroup, db, Role
+from flask_security import login_user
+from flask import current_app
+from pgadmin.tools.user_management import create_user
+
+try:
+    from urllib.parse import urlparse
+except ImportError:
+    from urlparse import urlparse
+
+
+class LDAPAuthentication(BaseAuthentication):
+    """Ldap Authentication Class"""
+
+    def get_friendly_name(self):
+        return gettext("ldap")
+
+    def authenticate(self, form):
+        self.username = form.data['email']
+        self.password = form.data['password']
+
+        status, msg = self.connect()
+
+        if not status:
+            return status, msg
+
+        status, user_email = self.search_ldap_user()
+
+        if not status:
+            return status, user_email
+
+        return self.__auto_create_user(user_email)
+
+    def connect(self):
+        """Setup the connection to the LDAP server and authenticate the user.
+        """
+
+        # Parse the server URI
+        uri = getattr(config, 'LDAP_SERVER_URI', None)
+
+        if uri:
+            uri = urlparse(uri)
+
+        # Create the TLS configuration object if required
+        tls = None
+
+        if type(uri) == str:
+            return False, "LDAP configuration error: Set the proper LDAP URI."
+
+        if uri.scheme == 'ldaps' or config.LDAP_USE_STARTTLS:
+
+            ca_cert_file = getattr(config, 'LDAP_CA_CERT_FILE', None)
+            cert_file = getattr(config, 'LDAP_CERT_FILE', None)
+            key_file = getattr(config, 'LDAP_KEY_FILE', None)
+            cert_validate = ssl.CERT_NONE
+
+            if ca_cert_file and cert_file and key_file:
+                cert_validate = ssl.CERT_REQUIRED
+
+            tls = Tls(
+                local_private_key_file=key_file,
+                local_certificate_file=cert_file,
+                validate=cert_validate,
+                version=ssl.PROTOCOL_TLSv1,
+                ca_certs_file=ca_cert_file)
+
+        # Create the server object
+        server = Server(uri.hostname,
+                        port=uri.port,
+                        use_ssl=(uri.scheme == 'ldaps'),
+                        get_info=ALL,
+                        tls=tls,
+                        connect_timeout=config.LDAP_CONNECTION_TIMEOUT)
+
+        # Create the connection
+        try:
+            user_dn = "{0}={1},{2}".format(config.LDAP_USERNAME_ATTRIBUTE,
+                                           self.username,
+                                           config.LDAP_BASE_DN
+                                           )
+            self.conn = Connection(server,
+                                   user=user_dn,
+                                   password=self.password,
+                                   auto_bind=True
+                                   )
+
+        except LDAPSocketOpenError as e:
+            current_app.logger.exception(
+                "Error connecting to the LDAP server: %s\n" % e)
+            return False, "Error connecting to the LDAP server:" \
+                          " %s\n" % e.args[0]
+        except LDAPBindError as e:
+            current_app.logger.exception(
+                "Error binding to the LDAP server.")
+            return False, "Error binding to the LDAP server."
+        except Exception as e:
+            current_app.logger.exception(
+                "Error connecting to the LDAP server: %s\n" % e)
+            return False, "Error connecting to the LDAP server:" \
+                          " %s\n" % e.args[0]
+
+        # Enable TLS if STARTTLS is configured
+        if not uri.scheme == 'ldaps' and config.LDAP_USE_STARTTLS:
+            try:
+                self.conn.start_tls()
+            except LDAPStartTLSError as e:
+                current_app.logger.exception(
+                    "Error starting TLS: %s\n" % e)
+                return False, "Error starting TLS: %s\n" % e.args[0]
+
+        return True, None
+
+    def __auto_create_user(self, user_email):
+        """Add the ldap user to the internal SQLite database."""
+        if config.LDAP_AUTO_CREATE_USER:
+            user = User.query.filter_by(
+                username=self.username).first()
+            if user is None:
+                return create_user({
+                    'username': self.username,
+                    'email': user_email,
+                    'role': 2,
+                    'active': True,
+                    'auth_source': 'ldap'
+                })
+
+        return True, None
+
+    def search_ldap_user(self):
+        """Get a list of users from the LDAP server based on config
+         search criteria."""
+        try:
+            self.conn.search(search_base=config.LDAP_SEARCH_BASE_DN,
+                             search_filter=config.LDAP_SEARCH_FILTER,
+                             search_scope=config.LDAP_SEARCH_SCOPE,
+                             attributes=ALL_ATTRIBUTES
+                             )
+
+        except LDAPInvalidScopeError as e:
+            current_app.logger.exception(
+                "Error searching the LDAP directory: %s\n" % e)
+            return False, "Error searching the LDAP directory:" \
+                          " %s\n" % e.args[0]
+        except LDAPAttributeError as e:
+            current_app.logger.exception("Error searching the LDAP directory:"
+                                         " %s\n" % e)
+            return False, "Error searching the LDAP directory:" \
+                          " %s\n" % e.args[0]
+        except LDAPInvalidFilterError as e:
+            current_app.logger.exception(
+                "Error searching the LDAP directory: %s\n" % e)
+            return False, "Error searching the LDAP directory:" \
+                          " %s\n" % e.args[0]
+
+        users = []
+        for entry in self.conn.entries:
+            user_email = None
+            if config.LDAP_USERNAME_ATTRIBUTE in entry and self.username == \
+                    entry[config.LDAP_USERNAME_ATTRIBUTE].value:
+                if 'mail' in entry:
+                    user_email = entry['mail'].value
+                return True, user_email
+        return False, None
diff --git a/web/pgadmin/authenticate/registry.py b/web/pgadmin/authenticate/registry.py
new file mode 100644
index 000000000..905f55643
--- /dev/null
+++ b/web/pgadmin/authenticate/registry.py
@@ -0,0 +1,65 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""External Authentication Registry."""
+
+
+from flask_babelex import gettext
+from abc import ABCMeta
+
+
+def _decorate_cls_name(module_name):
+    length = len(__package__) + 1
+
+    if len(module_name) > length and module_name.startswith(__package__):
+        return module_name[length:]
+
+    return module_name
+
+
+class AuthSourceRegistry(ABCMeta):
+    registry = None
+    auth_sources = dict()
+
+    def __init__(cls, name, bases, d):
+
+        # Register this type of auth_sources, based on the module name
+        # Avoid registering the BaseAuthentication itself
+
+        AuthSourceRegistry.registry[_decorate_cls_name(d['__module__'])] = cls
+        ABCMeta.__init__(cls, name, bases, d)
+
+    @classmethod
+    def create(cls, name, **kwargs):
+
+        if name in AuthSourceRegistry.auth_sources:
+            return AuthSourceRegistry.auth_sources[name]
+
+        if name in AuthSourceRegistry.registry:
+            AuthSourceRegistry.auth_sources[name] = \
+                (AuthSourceRegistry.registry[name])(**kwargs)
+            return AuthSourceRegistry.auth_sources[name]
+
+        raise NotImplementedError(
+            gettext(
+                "Authentication source '{0}' has not been implemented."
+            ).format(name)
+        )
+
+    @classmethod
+    def load_auth_sources(cls):
+        # Initialize the registry only if it has not yet been initialized
+        if AuthSourceRegistry.registry is None:
+            AuthSourceRegistry.registry = dict()
+
+        from importlib import import_module
+        from werkzeug.utils import find_modules
+
+        for module_name in find_modules(__package__, True):
+            module = import_module(module_name)
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 30af3e11b..862490820 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -45,6 +45,7 @@ from pgadmin.browser.register_browser_preferences import \
 from pgadmin.utils.master_password import validate_master_password, \
     set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
     set_crypt_key, process_masterpass_disabled
+from pgadmin.model import User
 
 try:
     import urllib.request as urlreq
@@ -580,12 +581,24 @@ def index():
 
                 flash(msg, 'warning')
 
+    auth_only_internal = False
+    auth_source = []
+
+    if config.SERVER_MODE:
+        if len(config.AUTHENTICATION_SOURCES) == 1\
+                and 'internal' in config.AUTHENTICATION_SOURCES:
+            auth_only_internal = True
+        auth_source = session['_auth_source_manager_obj'][
+            'source_friendly_name']
+
     response = Response(render_template(
         MODULE_NAME + "/index.html",
-        username=current_user.email,
+        username=current_user.username,
+        auth_source=auth_source,
         is_admin=current_user.has_role("Administrator"),
         logout_url=_get_logout_url(),
-        _=gettext
+        _=gettext,
+        auth_only_internal=auth_only_internal
     ))
 
     # Set the language cookie after login, so next time the user will have that
@@ -994,43 +1007,60 @@ if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE:
             form = form_class()
 
         if form.validate_on_submit():
-            try:
-                send_reset_password_instructions(form.user)
-            except SOCKETErrorException as e:
-                # Handle socket errors which are not covered by SMTPExceptions.
-                logging.exception(str(e), exc_info=True)
-                flash(gettext(u'SMTP Socket error: {}\n'
-                              u'Your password has not been changed.'
-                              ).format(e),
-                      'danger')
-                has_error = True
-            except (SMTPConnectError, SMTPResponseException,
-                    SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
-                    SMTPException, SMTPAuthenticationError, SMTPSenderRefused,
-                    SMTPRecipientsRefused) as e:
-
-                # Handle smtp specific exceptions.
-                logging.exception(str(e), exc_info=True)
-                flash(gettext(u'SMTP error: {}\n'
-                              u'Your password has not been changed.'
-                              ).format(e),
-                      'danger')
-                has_error = True
-            except Exception as e:
-                # Handle other exceptions.
-                logging.exception(str(e), exc_info=True)
-                flash(gettext(u'Error: {}\n'
-                              u'Your password has not been changed.'
-                              ).format(e),
+            # Check the Authentication source of the User
+            user = User.query.filter_by(
+                email=form.data['email'],
+                auth_source=current_app.PGADMIN_DEFAULT_AUTH_SOURCE
+            ).first()
+
+            if user is None:
+                # If the user is not an internal user, raise the exception
+                flash(gettext('Your account is authenticated using an '
+                              'external {} source. '
+                              'Please contact the administrators of this '
+                              'service if you need to reset your password.'
+                              ).format(form.user.auth_source),
                       'danger')
                 has_error = True
+            if not has_error:
+                try:
+                    send_reset_password_instructions(form.user)
+                except SOCKETErrorException as e:
+                    # Handle socket errors which are not
+                    # covered by SMTPExceptions.
+                    logging.exception(str(e), exc_info=True)
+                    flash(gettext(u'SMTP Socket error: {}\n'
+                                  u'Your password has not been changed.'
+                                  ).format(e),
+                          'danger')
+                    has_error = True
+                except (SMTPConnectError, SMTPResponseException,
+                        SMTPServerDisconnected, SMTPDataError, SMTPHeloError,
+                        SMTPException, SMTPAuthenticationError,
+                        SMTPSenderRefused, SMTPRecipientsRefused) as e:
+
+                    # Handle smtp specific exceptions.
+                    logging.exception(str(e), exc_info=True)
+                    flash(gettext(u'SMTP error: {}\n'
+                                  u'Your password has not been changed.'
+                                  ).format(e),
+                          'danger')
+                    has_error = True
+                except Exception as e:
+                    # Handle other exceptions.
+                    logging.exception(str(e), exc_info=True)
+                    flash(gettext(u'Error: {}\n'
+                                  u'Your password has not been changed.'
+                                  ).format(e),
+                          'danger')
+                    has_error = True
 
             if request.json is None and not has_error:
                 do_flash(*get_message('PASSWORD_RESET_REQUEST',
                                       email=form.user.email))
 
         if request.json and not has_error:
-            return _render_json(form, include_user=False)
+            return default_render_json(form, include_user=False)
 
         return _security.render_template(
             config_value('FORGOT_PASSWORD_TEMPLATE'),
diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html
index 682c23d65..b389b9574 100644
--- a/web/pgadmin/browser/templates/browser/index.html
+++ b/web/pgadmin/browser/templates/browser/index.html
@@ -142,6 +142,7 @@ window.onload = function(e){
                 <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"
                    role="button" aria-expanded="false" id="navbar-user"></a>
                 <ul class="dropdown-menu dropdown-menu-right" role="menu">
+                    {% if auth_only_internal %}
                     <li>
                         <a class="dropdown-item" href="#" onclick="pgAdmin.Browser.UserManagement.change_password(
                           '{{ url_for('browser.change_password') }}'
@@ -150,6 +151,7 @@ window.onload = function(e){
                         </a>
                     </li>
                     <li class="dropdown-divider"></li>
+                    {% endif %}
                     {% if is_admin %}
                     <li><a class="dropdown-item" href="#" onclick="pgAdmin.Browser.UserManagement.show_users()">{{ _('Users') }}</a></li>
                     <li class="dropdown-divider"></li>
diff --git a/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro b/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro
index 72ec97e59..eded8b68a 100644
--- a/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro
+++ b/web/pgadmin/browser/templates/browser/macros/gravatar_icon.macro
@@ -4,5 +4,5 @@ we will not associate our application with Gravatar module which will make
 'gravatar' filter unavailable in Jinja templates
 ###########################################################################}
 {% macro PREPARE_HTML() -%}
-'<img src = "{{ username | gravatar }}" width = "18" height = "18" alt = "Gravatar image for {{ username }}" > {{ username }} <span class="caret"></span>';
+'<img src = "{{ username | gravatar }}" width = "18" height = "18" alt = "Gravatar image for {{ username }}" > {{ username }} ({{auth_source}}) <span class="caret"></span>';
 {%- endmacro %}
diff --git a/web/pgadmin/browser/tests/test_change_password.py b/web/pgadmin/browser/tests/test_change_password.py
index 04c49a23e..fb86e4dfd 100644
--- a/web/pgadmin/browser/tests/test_change_password.py
+++ b/web/pgadmin/browser/tests/test_change_password.py
@@ -95,6 +95,7 @@ class ChangePasswordTestCase(BaseTestGenerator):
             response = self.tester.post(
                 '/user_management/user/',
                 data=json.dumps(dict(
+                    username=self.username,
                     email=self.username,
                     newPassword=self.password,
                     confirmPassword=self.password,
diff --git a/web/pgadmin/browser/tests/test_ldap_login.py b/web/pgadmin/browser/tests/test_ldap_login.py
new file mode 100644
index 000000000..2f59dfff6
--- /dev/null
+++ b/web/pgadmin/browser/tests/test_ldap_login.py
@@ -0,0 +1,88 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import config as app_config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression.test_setup import config_data
+
+
+class LDAPLoginTestCase(BaseTestGenerator):
+    """
+    This class checks ldap login functionality
+    by validating different scenarios.
+    """
+
+    scenarios = [
+        ('LDAP Authentication', dict(
+            config_key_param='ldap',
+            is_gravtar_image_check=False)),
+        ('LDAP With SSL Authentication', dict(
+            config_key_param='ldap_with_ssl',
+            is_gravtar_image_check=False)),
+        ('LDAP With TLS Authentication', dict(
+            config_key_param='ldap_with_tls',
+            is_gravtar_image_check=False)),
+    ]
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        We need to logout the test client
+        as we are testing ldap login scenarios.
+        """
+        cls.tester.logout()
+
+    def setUp(self):
+        if type(config_data['ldap_config']) is list and\
+                len(config_data['ldap_config']) > 0 and\
+                self.config_key_param in config_data['ldap_config'][0]:
+            ldap_config = config_data['ldap_config'][0][self.config_key_param]
+
+            app_config.AUTHENTICATION_SOURCES = ['ldap']
+            app_config.LDAP_AUTO_CREATE_USER = True
+            app_config.LDAP_SERVER_URI = ldap_config['uri']
+            app_config.LDAP_BASE_DN = ldap_config['base_dn']
+            app_config.LDAP_USERNAME_ATTRIBUTE = ldap_config[
+                'username_atr']
+            app_config.LDAP_SEARCH_BASE_DN = ldap_config[
+                'search_base_dn']
+            app_config.LDAP_SEARCH_FILTER = ldap_config['search_filter']
+            app_config.LDAP_USE_STARTTLS = ldap_config['use_starttls']
+            app_config.LDAP_CA_CERT_FILE = ldap_config['ca_cert_file']
+            app_config.LDAP_CERT_FILE = ldap_config['cert_file']
+            app_config.LDAP_KEY_FILE = ldap_config['key_file']
+        else:
+            self.skipTest(
+                "LDAP config not set."
+            )
+
+    def runTest(self):
+        """This function checks login functionality."""
+        username = config_data['pgAdmin4_ldap_credentials']['login_username']
+        password = config_data['pgAdmin4_ldap_credentials']['login_password']
+
+        res = self.tester.login(username, password, True)
+
+        respdata = 'Gravatar image for %s' %\
+                   config_data['pgAdmin4_ldap_credentials']['login_username']
+        self.assertTrue(respdata in res.data.decode('utf8'))
+
+    def tearDown(self):
+        self.tester.logout()
+
+    @classmethod
+    def tearDownClass(cls):
+        """
+        We need to again login the test client as soon as test scenarios
+        finishes.
+        """
+        cls.tester.logout()
+        app_config.AUTHENTICATION_SOURCES = ['internal']
+        utils.login_tester_account(cls.tester)
diff --git a/web/pgadmin/browser/tests/test_ldap_with_mocking.py b/web/pgadmin/browser/tests/test_ldap_with_mocking.py
new file mode 100644
index 000000000..90385242c
--- /dev/null
+++ b/web/pgadmin/browser/tests/test_ldap_with_mocking.py
@@ -0,0 +1,84 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import sys
+import config as app_config
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from regression.test_setup import config_data
+from pgadmin.authenticate.registry import AuthSourceRegistry
+
+if sys.version_info < (3, 3):
+    from mock import patch
+else:
+    from unittest.mock import patch
+
+
+class LDAPLoginMockTestCase(BaseTestGenerator):
+    """
+    This class checks ldap login functionality by mocking
+    ldap connection and ldap search functionality.
+    """
+
+    scenarios = [
+        ('LDAP Authentication with Auto Create User', dict(
+            auth_source=['ldap'],
+            auto_create_user=True,
+            username='ldap_user',
+            password='ldap_pass')),
+        ('LDAP Authentication without Auto Create User', dict(
+            auth_source=['ldap'],
+            auto_create_user=False,
+            username='ldap_user',
+            password='ldap_pass')),
+        ('LDAP + Internal Authentication', dict(
+            auth_source=['ldap', 'internal'],
+            auto_create_user=False,
+            username=config_data[
+                'pgAdmin4_login_credentials']['login_username'],
+            password=config_data[
+                'pgAdmin4_login_credentials']['login_password']
+        ))
+    ]
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        We need to logout the test client as we are testing
+        ldap login scenarios.
+        """
+        cls.tester.logout()
+
+    def setUp(self):
+        app_config.AUTHENTICATION_SOURCES = self.auth_source
+        app_config.LDAP_AUTO_CREATE_USER = self.auto_create_user
+
+    @patch.object(AuthSourceRegistry.registry['ldap'], 'connect',
+                  return_value=[True, "Done"])
+    @patch.object(AuthSourceRegistry.registry['ldap'], 'search_ldap_user',
+                  return_value=[True, ''])
+    def runTest(self, conn_mock_obj, search_mock_obj):
+        """This function checks ldap login functionality."""
+
+        res = self.tester.login(self.username, self.password, True)
+        respdata = 'Gravatar image for %s' % self.username
+        self.assertTrue(respdata in res.data.decode('utf8'))
+
+    def tearDown(self):
+        self.tester.logout()
+
+    @classmethod
+    def tearDownClass(cls):
+        """
+        We need to again login the test client as soon as test scenarios
+        finishes.
+        """
+        cls.tester.logout()
+        app_config.AUTHENTICATION_SOURCES = ['internal']
+        utils.login_tester_account(cls.tester)
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index f588e401c..e3af660b0 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
 #
 ##########################################################################
 
-SCHEMA_VERSION = 24
+SCHEMA_VERSION = 25
 
 ##########################################################################
 #
@@ -66,13 +66,15 @@ class User(db.Model, UserMixin):
     """Define a user object"""
     __tablename__ = 'user'
     id = db.Column(db.Integer, primary_key=True)
-    email = db.Column(db.String(256), unique=True, nullable=False)
+    email = db.Column(db.String(256), nullable=True)
+    username = db.Column(db.String(64), unique=True, nullable=False)
     password = db.Column(db.String(256))
     active = db.Column(db.Boolean(), nullable=False)
     confirmed_at = db.Column(db.DateTime())
     masterpass_check = db.Column(db.String(256))
     roles = db.relationship('Role', secondary=roles_users,
                             backref=db.backref('users', lazy='dynamic'))
+    auth_source = db.Column(db.String(16), unique=True, nullable=False)
 
 
 class Setting(db.Model):
diff --git a/web/pgadmin/templates/security/fields.html b/web/pgadmin/templates/security/fields.html
index efb126b2e..c505da366 100644
--- a/web/pgadmin/templates/security/fields.html
+++ b/web/pgadmin/templates/security/fields.html
@@ -9,3 +9,14 @@
     {% endif %}
 </div>
 {% endmacro %}
+{% macro render_username_with_errors(field, type) %}
+<div class="form-group mb-3 {% if field.errors %} has-error{% endif %}">
+    <input class="form-control" placeholder="{{ field.label.text }} / Username" name="{{ field.name }}"
+           type="{% if type %}{{ type }}{% else %}{{ field.type }}{% endif %}" autofocus>
+    {% if field.errors %}
+    {% for error in field.errors %}
+    <span class="form-text">{{ error }}</span>
+    {% endfor %}
+    {% endif %}
+</div>
+{% endmacro %}
diff --git a/web/pgadmin/templates/security/login_user.html b/web/pgadmin/templates/security/login_user.html
index 7515c2c2a..2e92d7b12 100644
--- a/web/pgadmin/templates/security/login_user.html
+++ b/web/pgadmin/templates/security/login_user.html
@@ -7,10 +7,10 @@
 {% block panel_title %}{{ _('Login') }}{% endblock %}
 {% block panel_body %}
 {% if config.SERVER_MODE %}
-<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
+<form action="{{ url_for('authenticate.login') }}" method="POST" name="login_user_form">
     {{ login_user_form.hidden_tag() }}
     {% set user_language = request.cookies.get('PGADMIN_LANGUAGE') or 'en' %}
-    {{ render_field_with_errors(login_user_form.email, "text") }}
+    {{ render_username_with_errors(login_user_form.email, "text") }}
     {{ render_field_with_errors(login_user_form.password, "password") }}
     <button class="btn btn-primary btn-block btn-login" type="submit" value="{{ _('Login') }}">{{ _('Login') }}</button>
     <div class="form-group row mb-3 c user-language">
diff --git a/web/pgadmin/templates/security/panel.html b/web/pgadmin/templates/security/panel.html
index 7de1d9d90..1452de8ca 100644
--- a/web/pgadmin/templates/security/panel.html
+++ b/web/pgadmin/templates/security/panel.html
@@ -1,5 +1,5 @@
 {% extends "base.html" %}
-{% from "security/fields.html" import render_field_with_errors %}
+{% from "security/fields.html" import render_field_with_errors, render_username_with_errors %}
 {% block body %}
 <div class="container-fluid h-100 login_page">
     {% if config.LOGIN_BANNER is defined and config.LOGIN_BANNER != "" %}
diff --git a/web/pgadmin/tools/user_management/__init__.py b/web/pgadmin/tools/user_management/__init__.py
index 55365173f..4f1602cdc 100644
--- a/web/pgadmin/tools/user_management/__init__.py
+++ b/web/pgadmin/tools/user_management/__init__.py
@@ -74,7 +74,8 @@ class UserManagementModule(PgAdminModule):
             'user_management.roles', 'user_management.role',
             'user_management.update_user', 'user_management.delete_user',
             'user_management.create_user', 'user_management.users',
-            'user_management.user', current_app.login_manager.login_view
+            'user_management.user', current_app.login_manager.login_view,
+            'user_management.auth_sources', 'user_management.auth_sources'
         ]
 
 
@@ -100,7 +101,7 @@ def validate_user(data):
         else:
             raise Exception(_("Passwords do not match."))
 
-    if 'email' in data and data['email'] != "":
+    if 'email' in data and data['email'] and data['email'] != "":
         if email_filter.match(data['email']):
             new_data['email'] = data['email']
         else:
@@ -112,6 +113,12 @@ def validate_user(data):
     if 'active' in data and data['active'] != "":
         new_data['active'] = data['active']
 
+    if 'username' in data and data['username'] != "":
+        new_data['username'] = data['username']
+
+    if 'auth_source' in data and data['auth_source'] != "":
+        new_data['auth_source'] = data['auth_source']
+
     return new_data
 
 
@@ -140,6 +147,7 @@ def script():
 @pgCSRFProtect.exempt
 @login_required
 def current_user_info():
+
     return Response(
         response=render_template(
             "user_management/js/current_user.js",
@@ -148,13 +156,14 @@ def current_user_info():
             user_id=current_user.id,
             email=current_user.email,
             name=(
-                current_user.email.split('@')[0] if config.SERVER_MODE is True
+                current_user.username.split('@')[0] if config.SERVER_MODE is True
                 else 'postgres'
             ),
             allow_save_password='true' if config.ALLOW_SAVE_PASSWORD
             else 'false',
             allow_save_tunnel_password='true'
             if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false',
+            auth_sources=config.AUTHENTICATION_SOURCES,
         ),
         status=200,
         mimetype="application/javascript"
@@ -180,9 +189,11 @@ def user(uid):
         u = User.query.get(uid)
 
         res = {'id': u.id,
+               'username': u.username,
                'email': u.email,
                'active': u.active,
-               'role': u.roles[0].id
+               'role': u.roles[0].id,
+               'auth_source': u.auth_source
                }
     else:
         users = User.query.all()
@@ -190,9 +201,11 @@ def user(uid):
         users_data = []
         for u in users:
             users_data.append({'id': u.id,
+                               'username': u.username,
                                'email': u.email,
                                'active': u.active,
-                               'role': u.roles[0].id
+                               'role': u.roles[0].id,
+                               'auth_source': u.auth_source
                                })
 
         res = users_data
@@ -215,11 +228,29 @@ def create():
         request.data, encoding='utf-8'
     )
 
-    for f in ('email', 'role', 'active', 'newPassword', 'confirmPassword'):
+    status, res = create_user(data)
+
+    if not status:
+        return internal_server_error(errormsg=res)
+
+    return ajax_response(
+        response=res,
+        status=200
+    )
+
+
+def create_user(data):
+    if 'auth_source' in data and data['auth_source'] != 'internal':
+        req_params = ('username', 'role', 'active', 'auth_source')
+    else:
+        req_params = ('email', 'role', 'active', 'newPassword',
+                      'confirmPassword')
+
+    for f in req_params:
         if f in data and data[f] != '':
             continue
         else:
-            return bad_request(errormsg=_("Missing field: '{0}'".format(f)))
+            return False, _("Missing field: '{0}'".format(f))
 
     try:
         new_data = validate_user(data)
@@ -228,13 +259,23 @@ def create():
             new_data['roles'] = [Role.query.get(new_data['roles'])]
 
     except Exception as e:
-        return bad_request(errormsg=_(str(e)))
+        return False, str(e)
 
     try:
-        usr = User(email=new_data['email'],
+
+        username = new_data['username'] if 'username' in new_data \
+            else new_data['email']
+        email = new_data['email'] if 'email' in new_data else None
+        password = new_data['password'] if 'password' in new_data else None
+        auth_source = new_data['auth_source'] if 'auth_source' in new_data \
+            else current_app.PGADMIN_DEFAULT_AUTH_SOURCE
+
+        usr = User(username=username,
+                   email=email,
                    roles=new_data['roles'],
                    active=new_data['active'],
-                   password=new_data['password'])
+                   password=password,
+                   auth_source=auth_source)
         db.session.add(usr)
         db.session.commit()
         # Add default server group for new user.
@@ -242,18 +283,15 @@ def create():
         db.session.add(server_group)
         db.session.commit()
     except Exception as e:
-        return internal_server_error(errormsg=str(e))
+        return False, str(e)
 
-    res = {'id': usr.id,
-           'email': usr.email,
-           'active': usr.active,
-           'role': usr.roles[0].id
-           }
-
-    return ajax_response(
-        response=res,
-        status=200
-    )
+    return True, {
+        'id': usr.id,
+        'username': usr.username,
+        'email': usr.email,
+        'active': usr.active,
+        'role': usr.roles[0].id
+    }
 
 
 @blueprint.route(
@@ -337,9 +375,11 @@ def update(uid):
         db.session.commit()
 
         res = {'id': usr.id,
+               'username': usr.username,
                'email': usr.email,
                'active': usr.active,
-               'role': usr.roles[0].id
+               'role': usr.roles[0].id,
+               'auth_source': usr.auth_source
                }
 
         return ajax_response(
@@ -384,3 +424,17 @@ def role(rid):
         response=res,
         status=200
     )
+
+
+@blueprint.route(
+    '/auth_sources/', methods=['GET'], endpoint='auth_sources'
+)
+def auth_sources():
+    sources = []
+    for source in current_app.PGADMIN_SUPPORTED_AUTH_SOURCE:
+        sources.append({'label': source, 'value': source})
+
+    return ajax_response(
+        response=sources,
+        status=200
+    )
diff --git a/web/pgadmin/tools/user_management/static/js/user_management.js b/web/pgadmin/tools/user_management/static/js/user_management.js
index 2b1ed1727..0e1108f52 100644
--- a/web/pgadmin/tools/user_management/static/js/user_management.js
+++ b/web/pgadmin/tools/user_management/static/js/user_management.js
@@ -9,12 +9,12 @@
 
 define([
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore', 'pgadmin.alertifyjs',
-  'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node',
+  'pgadmin.browser', 'backbone', 'backgrid', 'backform', 'pgadmin.browser.node', 'pgadmin.backform',
   'pgadmin.user_management.current_user',
   'backgrid.select.all', 'backgrid.filter',
 ], function(
   gettext, url_for, $, _, alertify, pgBrowser, Backbone, Backgrid, Backform,
-  pgNode, userInfo
+  pgNode, pgBackform, userInfo
 ) {
 
   // if module is already initialized, refer to that.
@@ -24,6 +24,8 @@ define([
 
   var USERURL = url_for('user_management.users'),
     ROLEURL = url_for('user_management.roles'),
+    SOURCEURL = url_for('user_management.auth_sources'),
+    AUTH_ONLY_INTERNAL = (userInfo['auth_sources'].length  == 1 && userInfo['auth_sources'].includes('internal')) ? true : false,
     userFilter = function(collection) {
       return (new Backgrid.Extension.ClientSideFilter({
         collection: collection,
@@ -33,6 +35,41 @@ define([
       }));
     };
 
+  // Integer Cell for Columns Length and Precision
+  var PasswordDepCell = Backgrid.Extension.PasswordDepCell =
+    Backgrid.Extension.PasswordCell.extend({
+      initialize: function() {
+        Backgrid.Extension.PasswordCell.prototype.initialize.apply(this, arguments);
+        Backgrid.Extension.DependentCell.prototype.initialize.apply(this, arguments);
+      },
+      dependentChanged: function () {
+        this.$el.empty();
+        var model = this.model,
+          column = this.column,
+          editable = this.column.get('editable'),
+          is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
+
+        if (is_editable){ this.$el.addClass('editable'); }
+        else { this.$el.removeClass('editable'); }
+
+        this.delegateEvents();
+        return this;
+      },
+      render: function() {
+        Backgrid.NumberCell.prototype.render.apply(this, arguments);
+
+        var model = this.model,
+          column = this.column,
+          editable = this.column.get('editable'),
+          is_editable = _.isFunction(editable) ? !!editable.apply(column, [model]) : !!editable;
+
+        if (is_editable){ this.$el.addClass('editable'); }
+        else { this.$el.removeClass('editable'); }
+        return this;
+      },
+      remove: Backgrid.Extension.DependentCell.prototype.remove,
+    });
+
   pgBrowser.UserManagement = {
     init: function() {
       if (this.initialized)
@@ -235,20 +272,67 @@ define([
     // Callback to draw User Management Dialog.
     show_users: function() {
       if (!userInfo['is_admin']) return;
-      var Roles = [];
+      var Roles = [],
+        Sources = [];
 
       var UserModel = pgBrowser.Node.Model.extend({
           idAttribute: 'id',
           urlRoot: USERURL,
           defaults: {
             id: undefined,
+            username: undefined,
             email: undefined,
             active: true,
             role: undefined,
             newPassword: undefined,
             confirmPassword: undefined,
+            auth_source: 'internal',
+            authOnlyInternal: AUTH_ONLY_INTERNAL,
           },
           schema: [{
+            id: 'username',
+            label: gettext('Username'),
+            type: 'text',
+            cell: Backgrid.Extension.StringDepCell,
+            cellHeaderClasses: 'width_percent_30',
+            deps: ['id'],
+            editable: function(m) {
+              if (m.get('authOnlyInternal')) return false;
+              return true;
+            },
+            disabled: false,
+          }, {
+            id: 'auth_source',
+            label: gettext('Authentication Source'),
+            type: 'text',
+            control: 'Select2',
+            url: url_for('user_management.auth_sources'),
+            cellHeaderClasses: 'width_percent_30',
+            visible: function(m) {
+              if (m.get('authOnlyInternal')) return false;
+              return true;
+            },
+            disabled: false,
+            cell: 'Select2',
+            select2: {
+              allowClear: false,
+              openOnEnter: false,
+              first_empty: false,
+            },
+            options: function() {
+              return Sources;
+            },
+            editable: function(m) {
+              if (m instanceof Backbone.Collection) {
+                return true;
+              }
+              if (m.isNew() && !m.get('authOnlyInternal')) {
+                return true;
+              } else {
+                return false;
+              }
+            },
+          }, {
             id: 'email',
             label: gettext('Email'),
             type: 'text',
@@ -256,6 +340,8 @@ define([
             cellHeaderClasses: 'width_percent_30',
             deps: ['id'],
             editable: function(m) {
+              if (!m.get('authOnlyInternal')) return true;
+
               if (m instanceof Backbone.Collection) {
                 return false;
               }
@@ -328,23 +414,38 @@ define([
             type: 'password',
             disabled: false,
             control: 'input',
-            cell: 'password',
+            cell: PasswordDepCell,
             cellHeaderClasses: 'width_percent_20',
+            deps: ['auth_source'],
+            editable: function(m) {
+              if (m.get('auth_source') == 'internal') {
+                return true;
+              } else {
+                return false;
+              }
+            },
           }, {
             id: 'confirmPassword',
             label: gettext('Confirm password'),
             type: 'password',
             disabled: false,
             control: 'input',
-            cell: 'password',
+            cell: PasswordDepCell,
             cellHeaderClasses: 'width_percent_20',
+            editable: function(m) {
+              if (m.get('auth_source') == 'internal') {
+                return true;
+              } else {
+                return false;
+              }
+            },
           }],
           validate: function() {
             var errmsg = null,
               changedAttrs = this.changed || {},
               email_filter = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
 
-            if (('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
+            if (this.get('auth_source') == 'internal' && ('email' in changedAttrs || !this.isNew()) && (_.isUndefined(this.get('email')) ||
                 _.isNull(this.get('email')) ||
                 String(this.get('email')).replace(/^\s+|\s+$/g, '') == '')) {
               errmsg = gettext('Email address cannot be empty.');
@@ -358,9 +459,8 @@ define([
               this.errorModel.set('email', errmsg);
               return errmsg;
             } else if (!!this.get('email') && this.collection.where({
-              'email': this.get('email'),
+              'email': this.get('email'), 'auth_source': 'internal',
             }).length > 1) {
-
               errmsg = gettext('The email address %s already exists.',
                 this.get('email')
               );
@@ -385,111 +485,113 @@ define([
               this.errorModel.unset('role');
             }
 
-            if (this.isNew()) {
-              // Password is compulsory for new user.
-              if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
-                  _.isNull(this.get('newPassword')) ||
-                  this.get('newPassword') == '')) {
-
-                errmsg = gettext('Password cannot be empty for user %s.',
-                  (this.get('email') || '')
-                );
+            if (this.get('auth_source') == 'internal') {
+              if (this.isNew()) {
+                // Password is compulsory for new user.
+                if ('newPassword' in changedAttrs && (_.isUndefined(this.get('newPassword')) ||
+                    _.isNull(this.get('newPassword')) ||
+                    this.get('newPassword') == '')) {
 
-                this.errorModel.set('newPassword', errmsg);
-                return errmsg;
-              } else if (!_.isUndefined(this.get('newPassword')) &&
-                !_.isNull(this.get('newPassword')) &&
-                this.get('newPassword').length < 6) {
+                  errmsg = gettext('Password cannot be empty for user %s.',
+                    (this.get('email') || '')
+                  );
 
-                errmsg = gettext('Password must be at least 6 characters for user %s.',
-                  (this.get('email') || '')
-                );
+                  this.errorModel.set('newPassword', errmsg);
+                  return errmsg;
+                } else if (!_.isUndefined(this.get('newPassword')) &&
+                  !_.isNull(this.get('newPassword')) &&
+                  this.get('newPassword').length < 6) {
 
-                this.errorModel.set('newPassword', errmsg);
-                return errmsg;
-              } else {
-                this.errorModel.unset('newPassword');
-              }
+                  errmsg = gettext('Password must be at least 6 characters for user %s.',
+                    (this.get('email') || '')
+                  );
 
-              if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
-                  _.isNull(this.get('confirmPassword')) ||
-                  this.get('confirmPassword') == '')) {
+                  this.errorModel.set('newPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('newPassword');
+                }
 
-                errmsg = gettext('Confirm Password cannot be empty for user %s.',
-                  (this.get('email') || '')
-                );
+                if ('confirmPassword' in changedAttrs && (_.isUndefined(this.get('confirmPassword')) ||
+                    _.isNull(this.get('confirmPassword')) ||
+                    this.get('confirmPassword') == '')) {
 
-                this.errorModel.set('confirmPassword', errmsg);
-                return errmsg;
-              } else {
-                this.errorModel.unset('confirmPassword');
-              }
+                  errmsg = gettext('Confirm Password cannot be empty for user %s.',
+                    (this.get('email') || '')
+                  );
 
-              if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
-                this.get('newPassword') != this.get('confirmPassword')) {
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('confirmPassword');
+                }
 
-                errmsg = gettext('Passwords do not match for user %s.',
-                  (this.get('email') || '')
-                );
+                if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
+                  this.get('newPassword') != this.get('confirmPassword')) {
 
-                this.errorModel.set('confirmPassword', errmsg);
-                return errmsg;
-              } else {
-                this.errorModel.unset('confirmPassword');
-              }
+                  errmsg = gettext('Passwords do not match for user %s.',
+                    (this.get('email') || '')
+                  );
 
-            } else {
-              if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
-                  this.get('newPassword') == '') &&
-                ((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
-                  this.get('confirmPassword') == ''))) {
-
-                this.errorModel.unset('newPassword');
-                if (this.get('newPassword') == '') {
-                  this.set({
-                    'newPassword': undefined,
-                  });
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('confirmPassword');
                 }
 
-                this.errorModel.unset('confirmPassword');
-                if (this.get('confirmPassword') == '') {
-                  this.set({
-                    'confirmPassword': undefined,
-                  });
-                }
-              } else if (!_.isUndefined(this.get('newPassword')) &&
-                !_.isNull(this.get('newPassword')) &&
-                !this.get('newPassword') == '' &&
-                this.get('newPassword').length < 6) {
+              } else {
+                if ((_.isUndefined(this.get('newPassword')) || _.isNull(this.get('newPassword')) ||
+                    this.get('newPassword') == '') &&
+                  ((_.isUndefined(this.get('confirmPassword')) || _.isNull(this.get('confirmPassword')) ||
+                    this.get('confirmPassword') == ''))) {
+
+                  this.errorModel.unset('newPassword');
+                  if (this.get('newPassword') == '') {
+                    this.set({
+                      'newPassword': undefined,
+                    });
+                  }
 
-                errmsg = gettext('Password must be at least 6 characters for user %s.',
-                  (this.get('email') || '')
-                );
+                  this.errorModel.unset('confirmPassword');
+                  if (this.get('confirmPassword') == '') {
+                    this.set({
+                      'confirmPassword': undefined,
+                    });
+                  }
+                } else if (!_.isUndefined(this.get('newPassword')) &&
+                  !_.isNull(this.get('newPassword')) &&
+                  !this.get('newPassword') == '' &&
+                  this.get('newPassword').length < 6) {
 
-                this.errorModel.set('newPassword', errmsg);
-                return errmsg;
-              } else if (_.isUndefined(this.get('confirmPassword')) ||
-                _.isNull(this.get('confirmPassword')) ||
-                this.get('confirmPassword') == '') {
+                  errmsg = gettext('Password must be at least 6 characters for user %s.',
+                    (this.get('email') || '')
+                  );
 
-                errmsg = gettext('Confirm Password cannot be empty for user %s.',
-                  (this.get('email') || '')
-                );
+                  this.errorModel.set('newPassword', errmsg);
+                  return errmsg;
+                } else if (_.isUndefined(this.get('confirmPassword')) ||
+                  _.isNull(this.get('confirmPassword')) ||
+                  this.get('confirmPassword') == '') {
 
-                this.errorModel.set('confirmPassword', errmsg);
-                return errmsg;
-              } else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
-                this.get('newPassword') != this.get('confirmPassword')) {
+                  errmsg = gettext('Confirm Password cannot be empty for user %s.',
+                    (this.get('email') || '')
+                  );
 
-                errmsg = gettext('Passwords do not match for user %s.',
-                  (this.get('email') || '')
-                );
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else if (!!this.get('newPassword') && !!this.get('confirmPassword') &&
+                  this.get('newPassword') != this.get('confirmPassword')) {
 
-                this.errorModel.set('confirmPassword', errmsg);
-                return errmsg;
-              } else {
-                this.errorModel.unset('newPassword');
-                this.errorModel.unset('confirmPassword');
+                  errmsg = gettext('Passwords do not match for user %s.',
+                    (this.get('email') || '')
+                  );
+
+                  this.errorModel.set('confirmPassword', errmsg);
+                  return errmsg;
+                } else {
+                  this.errorModel.unset('newPassword');
+                  this.errorModel.unset('confirmPassword');
+                }
               }
             }
             return null;
@@ -716,7 +818,10 @@ define([
                   saveUser: function(m) {
                     var d = m.toJSON(true);
 
-                    if (m.isNew() && (!m.get('email') || !m.get('role') ||
+                    if(m.isNew() && m.get('authOnlyInternal') === false &&
+                     (!m.get('username') || !m.get('auth_source') || !m.get('role')) ) {
+                      return false;
+                    } else if (m.isNew() && m.get('authOnlyInternal') === true &&  (!m.get('email') || !m.get('role') ||
                         !m.get('newPassword') || !m.get('confirmPassword') ||
                         m.get('newPassword') != m.get('confirmPassword'))) {
                       // New user model is valid but partially filled so return without saving.
@@ -741,7 +846,7 @@ define([
 
                           m.startNewSession();
                           alertify.success(gettext('User \'%s\' saved.',
-                            m.get('email')
+                            m.get('username')
                           ));
                         },
                         error: function(res, jqxhr) {
@@ -797,6 +902,23 @@ define([
                   }, 100);
                 });
 
+              $.ajax({
+                url: SOURCEURL,
+                method: 'GET',
+                async: false,
+              })
+                .done(function(res) {
+                  Sources = res;
+                })
+                .fail(function() {
+                  setTimeout(function() {
+                    alertify.alert(
+                      gettext('Error'),
+                      gettext('Cannot load user Sources.')
+                    );
+                  }, 100);
+                });
+
               var view = this.view = new Backgrid.Grid({
                 row: UserRow,
                 columns: gridSchema.columns,
diff --git a/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js b/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js
index cfcb77813..c6e210343 100644
--- a/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js
+++ b/web/pgadmin/tools/user_management/templates/user_management/js/current_user.js
@@ -14,6 +14,7 @@ define('pgadmin.user_management.current_user', [], function() {
         'is_admin': {{ is_admin }},
         'name': '{{ name }}',
         'allow_save_password': {{ allow_save_password }},
-        'allow_save_tunnel_password': {{ allow_save_tunnel_password }}
+        'allow_save_tunnel_password': {{ allow_save_tunnel_password }},
+        'auth_sources': {{ auth_sources }}
     }
 });
diff --git a/web/regression/python_test_utils/csrf_test_client.py b/web/regression/python_test_utils/csrf_test_client.py
index bb3f7da70..42ae510b5 100644
--- a/web/regression/python_test_utils/csrf_test_client.py
+++ b/web/regression/python_test_utils/csrf_test_client.py
@@ -109,7 +109,7 @@ class TestClient(testing.FlaskClient):
             csrf_token = self.generate_csrf_token()
 
         res = self.post(
-            '/login', data=dict(
+            '/authenticate/login', data=dict(
                 email=email, password=password,
                 csrf_token=csrf_token,
             ),
@@ -120,5 +120,5 @@ class TestClient(testing.FlaskClient):
         return res
 
     def logout(self):
-        res = self.get('/logout', follow_redirects=False)
+        res = self.get('/logout?next=/browser/', follow_redirects=False)
         self.csrf_token = None
diff --git a/web/regression/runtests.py b/web/regression/runtests.py
index ef5b46328..fcf73a886 100644
--- a/web/regression/runtests.py
+++ b/web/regression/runtests.py
@@ -118,6 +118,11 @@ app.PGADMIN_RUNTIME = True
 if config.SERVER_MODE is True:
     app.PGADMIN_RUNTIME = False
 app.config['WTF_CSRF_ENABLED'] = True
+
+# Authentication sources
+app.PGADMIN_DEFAULT_AUTH_SOURCE = 'internal'
+app.PGADMIN_EXTERNAL_AUTH_SOURCE = 'ldap'
+
 app.test_client_class = TestClient
 test_client = app.test_client()
 test_client.setApp(app)
@@ -195,6 +200,8 @@ def get_test_modules(arguments):
             "browser.tests.test_login",
             "browser.tests.test_logout",
             "browser.tests.test_reset_password",
+            "browser.tests.test_ldap_login",
+            "browser.tests.test_ldap_with_mocking",
         ])
     if arguments['exclude'] is not None:
         exclude_pkgs += arguments['exclude'].split(',')
diff --git a/web/regression/test_config.json.in b/web/regression/test_config.json.in
index 15b133a19..0a151e633 100644
--- a/web/regression/test_config.json.in
+++ b/web/regression/test_config.json.in
@@ -11,6 +11,49 @@
     "login_password": "PASSWORD",
     "login_username": "USER@EXAMPLE.COM"
   },
+  "pgAdmin4_ldap_credentials": {
+    "login_password": "PASSWORD",
+    "login_username": "USERNAME"
+  },
+  "ldap_config": [
+    {
+    "ldap": {
+      "name": "Ldap scenario name"
+      "uri": "ldap://IP-ADDRESS/HOSTNAME:389",
+      "base_dn": "BASE-DN",
+      "search_base_dn": "SEARCH-BASE-DN",
+      "username_atr": "UID",
+      "search_filter": "(objectclass=*)",
+      "use_starttls": false,
+      "ca_cert_file": "",
+      "cert_file": "",
+      "key_file": ""
+    },
+    "ldap_with_ssl": {
+      "name": "Ldap scenario name"
+      "uri": "ldaps://IP-ADDRESS/HOSTNAME:636",
+      "base_dn": "BASE-DN",
+      "search_base_dn": "SEARCH-BASE-DN",
+      "username_atr": "UID",
+      "search_filter": "(objectclass=*)",
+      "use_starttls": false,
+      "ca_cert_file": "",
+      "cert_file": "",
+      "key_file": ""
+    },
+    "ldap_with_tls": {
+      "name": "Ldap scenario name"
+      "uri": "ldap://IP-ADDRESS/HOSTNAME:389",
+      "base_dn": "BASE-DN",
+      "search_base_dn": "SEARCH-BASE-DN",
+      "username_atr": "UID",
+      "search_filter": "(objectclass=*)",
+      "use_starttls": true,
+      "ca_cert_file": "",
+      "cert_file": "",
+      "key_file": ""
+    }
+  }],
   "server_group": 1,
   "server_credentials": [
     {
