diff --git a/docs/manual/extending/auth-backends.rst b/docs/manual/extending/auth-backends.rst
index 4b8e45472cc8c736bd1b2a4dc8d5de398e0b2c73..5620f0a88b1b6327d15fecd1b13d1d78b1869c37 100644
--- a/docs/manual/extending/auth-backends.rst
+++ b/docs/manual/extending/auth-backends.rst
@@ -29,6 +29,7 @@ An Authentication Backend class is a simple class inheriting from
 :py:class:`reviewboard.accounts.backends.AuthBackend`. It must set the
 following attributes:
 
+* :py:attr:`backend_id`
 * :py:attr:`name`
 
 And can optionally set the following attributes:
@@ -51,6 +52,14 @@ We'll go into each function and attribute in detail.
 
 .. py:class:: reviewboard.accounts.backends.AuthBackend
 
+.. py:attribute:: backend_id
+
+   This is the ID used for registering and looking up the authentication
+   backend.
+
+   This ID needs to be unique, and therefore should include some
+   vendor-specific prefix.
+
 .. py:attribute:: name
 
    This is the human-readable name of the authentication backend. This is what
@@ -58,7 +67,7 @@ We'll go into each function and attribute in detail.
 
 .. py:attribute:: login_instructions
 
-   If set, this string is displayed under the name on the login page.
+   If set, this string is displayed on the login page.
 
 .. py:attribute:: settings_form
 
@@ -281,7 +290,7 @@ Disabling Fields
 It can be useful to disable fields based on different conditions, such as
 a missing Python module. In this case, you can disable any fields in the
 form and provide an inline message by setting the
-:py:attr:`disabled_fields` and :py:`disabled_reasons` attributes during
+:py:attr:`disabled_fields` and :py:attr:`disabled_reasons` attributes during
 :py:meth:`load`.
 
 Both of these attributes are dictionaries mapping from a field name to a
@@ -338,8 +347,25 @@ For example::
 Packaging
 =========
 
-Authentication backends should be packaged as a standard Python egg module.
-Generally, this looks something like::
+Using Extensions
+----------------
+
+As of Review Board 2.0, authentication backends should be provided by
+extensions, using :ref:`auth-backend-hook`. This allows the authentication
+backends to be easily added or removed.
+
+
+Using Entry Points
+------------------
+
+When extensions are, for some reason, not an ideal option, you can instead
+fall back on using Python entry point registration. This is required
+if your authentication backend needs to work on versions of Review Board
+prior to 2.0.
+
+For entry point registration, your authentication backends will need to be
+packaged as a standard Python egg module. Generally, this looks something
+like::
 
     setup.py
     myauth/__init__.py
diff --git a/docs/manual/extending/extensions/hooks/auth-backend-hook.rst b/docs/manual/extending/extensions/hooks/auth-backend-hook.rst
new file mode 100644
index 0000000000000000000000000000000000000000..99e7ce27a601b57b4050a9e18ac866f3d6bbf7c0
--- /dev/null
+++ b/docs/manual/extending/extensions/hooks/auth-backend-hook.rst
@@ -0,0 +1,53 @@
+.. _auth-backend-hook:
+
+===============
+AuthBackendHook
+===============
+
+.. versionadded:: 2.0
+
+:py:class:`reviewboard.extensions.hooks.AuthBackendHook` allows extensions to
+register new authentication backends, which can be used to integrate with
+databases or servers to handle authentication, user lookup, and profile
+manipulation.
+
+Extensions must provide a subclass of
+:py:class:`reviewboard.accounts.backends.AuthBackend`, and pass it as a
+parameter to :py:class:`AuthBackendHook`. Each class must provide
+:py:attr:`backend_id` and :py:attr:`name` attributes, and must implement
+:py:meth:`authenticate` and :py:meth:`get_or_create_user` methods.
+
+
+Example
+=======
+
+.. code-block:: python
+
+    from reviewboard.accounts.backends import AuthBackend
+    from reviewboard.extensions.base import Extension
+    from reviewboard.extensions.hooks import AuthBackendHook
+
+
+    class SampleAuthBackend(AuthBackend):
+        backend_id = 'myvendor_sample_auth'
+        name = 'Sample Authentication'
+
+        def authenticate(self, username, password):
+            if username == 'superuser' and password = 's3cr3t':
+                return self.get_or_create_user(username, password=password)
+
+            return None
+
+        def get_or_create_user(self, username, request=None, password=None):
+            user, is_new = User.objects.get_or_create(username=username)
+
+            if is_new:
+                user.set_unusable_password()
+                user.save()
+
+            return user
+
+
+      class SampleExtension(Extension):
+          def initialize(self):
+              AuthBackendHook(self, SampleAuthBackend)
diff --git a/docs/manual/extending/extensions/hooks/index.rst b/docs/manual/extending/extensions/hooks/index.rst
index 464cf10fd22765d7d01b21a16cb47872e12c094f..dd6d9727be9d3af0c092c50d80b55ecb363b1eaa 100644
--- a/docs/manual/extending/extensions/hooks/index.rst
+++ b/docs/manual/extending/extensions/hooks/index.rst
@@ -16,6 +16,7 @@ The following hooks are available for use by extensions.
 .. toctree::
    :maxdepth: 1
 
+   auth-backend-hook
    account-pages-hook
    account-page-forms-hook
    action-hooks
diff --git a/reviewboard/accounts/backends.py b/reviewboard/accounts/backends.py
index 5c1a8bcd26c55795953abf7b2730c4230e385a38..90ced77e4f9be76b4427ce0167c44c8156989d60 100644
--- a/reviewboard/accounts/backends.py
+++ b/reviewboard/accounts/backends.py
@@ -15,6 +15,7 @@ from django.contrib.auth import hashers
 from django.utils import six
 from django.utils.translation import ugettext_lazy as _
 from djblets.db.query import get_object_or_none
+from djblets.siteconfig.models import SiteConfiguration
 try:
     from ldap.filter import filter_format
 except ImportError:
@@ -29,8 +30,10 @@ from reviewboard.accounts.models import LocalSiteProfile
 from reviewboard.site.models import LocalSite
 
 
-_auth_backends = []
+_registered_auth_backends = {}
+_enabled_auth_backends = []
 _auth_backend_setting = None
+_populated = False
 
 
 INVALID_USERNAME_CHAR_REGEX = re.compile(r'[^\w.@+-]')
@@ -38,6 +41,7 @@ INVALID_USERNAME_CHAR_REGEX = re.compile(r'[^\w.@+-]')
 
 class AuthBackend(object):
     """The base class for Review Board authentication backends."""
+    backend_id = None
     name = None
     settings_form = None
     supports_anonymous_user = True
@@ -114,6 +118,7 @@ class StandardAuthBackend(AuthBackend, ModelBackend):
     handle authentication against locally added users and handle
     LocalSite-based permissions for all configurations.
     """
+    backend_id = 'builtin'
     name = _('Standard Registration')
     settings_form = StandardAuthSettingsForm
     supports_registration = True
@@ -224,6 +229,7 @@ class StandardAuthBackend(AuthBackend, ModelBackend):
 
 class NISBackend(AuthBackend):
     """Authenticate against a user on an NIS server."""
+    backend_id = 'nis'
     name = _('NIS')
     settings_form = NISSettingsForm
     login_instructions = \
@@ -286,6 +292,7 @@ class NISBackend(AuthBackend):
 
 class LDAPBackend(AuthBackend):
     """Authenticate against a user on an LDAP server."""
+    backend_id = 'ldap'
     name = _('LDAP')
     settings_form = LDAPSettingsForm
     login_instructions = \
@@ -453,6 +460,7 @@ class LDAPBackend(AuthBackend):
 
 class ActiveDirectoryBackend(AuthBackend):
     """Authenticate a user against an Active Directory server."""
+    backend_id = 'ad'
     name = _('Active Directory')
     settings_form = ActiveDirectorySettingsForm
     login_instructions = \
@@ -660,6 +668,7 @@ class X509Backend(AuthBackend):
     browser. This backend relies on the X509AuthMiddleware to extract a
     username field from the client certificate.
     """
+    backend_id = 'x509'
     name = _('X.509 Public Key')
     settings_form = X509SettingsForm
     supports_change_password = True
@@ -704,6 +713,39 @@ class X509Backend(AuthBackend):
         return user
 
 
+def _populate_defaults():
+    """Populates the default list of authentication backends."""
+    global _populated
+
+    if not _populated:
+        _populated = True
+
+        # Always ensure that the standard built-in auth backend is included.
+        register_auth_backend(StandardAuthBackend)
+
+        entrypoints = \
+            pkg_resources.iter_entry_points('reviewboard.auth_backends')
+
+        for entry in entrypoints:
+            try:
+                cls = entry.load()
+
+                # All backends should include an ID, but we need to handle
+                # legacy modules.
+                if not cls.backend_id:
+                    logging.warning('The authentication backend %r did '
+                                    'not provide a backend_id attribute. '
+                                    'Setting it to the entrypoint name "%s".',
+                                    cls, entry.name)
+                    cls.backend_id = entry.name
+
+                register_auth_backend(cls)
+            except Exception as e:
+                logging.error('Error loading authentication backend %s: %s'
+                              % (entry.name, e),
+                              exc_info=1)
+
+
 def get_registered_auth_backends():
     """Returns all registered Review Board authentication backends.
 
@@ -711,30 +753,77 @@ def get_registered_auth_backends():
     third parties that have properly registered with the
     "reviewboard.auth_backends" entry point.
     """
-    # Always ensure that the standard built-in auth backend is included.
-    yield "builtin", StandardAuthBackend
+    _populate_defaults()
+
+    return six.itervalues(_registered_auth_backends)
+
+
+def get_registered_auth_backend(backend_id):
+    """Returns the authentication backends with the specified ID.
+
+    If the authentication backend could not be found, this will return None.
+    """
+    _populate_defaults()
+
+    try:
+        return _registered_auth_backends[backend_id]
+    except KeyError:
+        return None
 
-    for entry in pkg_resources.iter_entry_points('reviewboard.auth_backends'):
-        try:
-            yield entry.name, entry.load()
-        except Exception as e:
-            logging.error('Error loading authentication backend %s: %s'
-                          % (entry.name, e),
-                          exc_info=1)
 
+def register_auth_backend(backend_cls):
+    """Registers an authentication backend.
 
-def get_auth_backends():
+    This backend will appear in the list of available backends.
+
+    The backend class must have a backend_id attribute set, and can only
+    be registerd once. A KeyError will be thrown if attempting to register
+    a second time.
+    """
+    _populate_defaults()
+
+    backend_id = backend_cls.backend_id
+
+    if not backend_id:
+        raise KeyError('The backend_id attribute must be set on %r'
+                       % backend_cls)
+
+    if backend_id in _registered_auth_backends:
+        raise KeyError('"%s" is already a registered auth backend'
+                       % backend_id)
+
+    _registered_auth_backends[backend_id] = backend_cls
+
+
+def unregister_auth_backend(backend_cls):
+    """Unregisters a previously registered authentication backend."""
+    _populate_defaults()
+
+    backend_id = backend_cls.backend_id
+
+    if backend_id not in _registered_auth_backends:
+        logging.error('Failed to unregister unknown authentication '
+                      'backend "%s".',
+                      backend_id)
+        raise KeyError('"%s" is not a registered authentication backend'
+                       % backend_id)
+
+    del _registered_auth_backends[backend_id]
+
+
+def get_enabled_auth_backends():
     """Returns all authentication backends being used by Review Board.
 
     The returned list contains every authentication backend that Review Board
     will try, in order.
     """
-    global _auth_backends
+    global _enabled_auth_backends
     global _auth_backend_setting
 
-    if (not _auth_backends or
-            _auth_backend_setting != settings.AUTHENTICATION_BACKENDS):
-        _auth_backends = []
+    if (not _enabled_auth_backends or
+        _auth_backend_setting != settings.AUTHENTICATION_BACKENDS):
+        _enabled_auth_backends = []
+
         for backend in get_backends():
             if not isinstance(backend, AuthBackend):
                 warn('Authentication backends should inherit from '
@@ -752,8 +841,14 @@ def get_auth_backends():
                              "from AuthBackend." % (field, backend.__class__))
                         setattr(backend, field, False)
 
-            _auth_backends.append(backend)
+            _enabled_auth_backends.append(backend)
 
         _auth_backend_setting = settings.AUTHENTICATION_BACKENDS
 
-    return _auth_backends
+    return _enabled_auth_backends
+
+
+def set_enabled_auth_backend(backend_id):
+    """Sets the authentication backend to be used."""
+    siteconfig = SiteConfiguration.objects.get_current()
+    siteconfig.set('auth_backend', backend_id)
diff --git a/reviewboard/accounts/context_processors.py b/reviewboard/accounts/context_processors.py
index 539a61b276bf0d784fac636f883446b60a021531..460d225f782930ab125cc9607dda8a82347170e3 100644
--- a/reviewboard/accounts/context_processors.py
+++ b/reviewboard/accounts/context_processors.py
@@ -1,12 +1,12 @@
 from __future__ import unicode_literals
 
-from reviewboard.accounts.backends import get_auth_backends
+from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.accounts.models import Profile
 
 
 def auth_backends(request):
     return {
-        'auth_backends': get_auth_backends(),
+        'auth_backends': get_enabled_auth_backends(),
     }
 
 
diff --git a/reviewboard/accounts/forms/pages.py b/reviewboard/accounts/forms/pages.py
index 03c32e7f180f0a6c881c06304535b4c7005bb3dc..229cecaa21cbde9cb4ecb15193391d4eefd60c9a 100644
--- a/reviewboard/accounts/forms/pages.py
+++ b/reviewboard/accounts/forms/pages.py
@@ -11,7 +11,7 @@ from django.utils.translation import ugettext_lazy as _
 from djblets.forms.fields import TimeZoneField
 from djblets.siteconfig.models import SiteConfiguration
 
-from reviewboard.accounts.backends import get_auth_backends
+from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.reviews.models import Group
 from reviewboard.site.urlresolvers import local_site_reverse
 
@@ -130,7 +130,7 @@ class AccountSettingsForm(AccountPageForm):
         required=False)
 
     def is_visible(self):
-        backend = get_auth_backends()[0]
+        backend = get_enabled_auth_backends()[0]
 
         return backend.supports_change_password
 
@@ -179,7 +179,7 @@ class ChangePasswordForm(AccountPageForm):
         widget=widgets.PasswordInput())
 
     def clean_old_password(self):
-        backend = get_auth_backends()[0]
+        backend = get_enabled_auth_backends()[0]
 
         password = self.cleaned_data['old_password']
 
@@ -196,7 +196,7 @@ class ChangePasswordForm(AccountPageForm):
         return p2
 
     def save(self):
-        backend = get_auth_backends()[0]
+        backend = get_enabled_auth_backends()[0]
         backend.update_password(self.user, self.cleaned_data['password1'])
         self.user.save()
 
@@ -231,7 +231,7 @@ class ProfileForm(AccountPageForm):
             'profile_private': self.profile.is_private,
         })
 
-        backend = get_auth_backends()[0]
+        backend = get_enabled_auth_backends()[0]
 
         if not backend.supports_change_name:
             del self.fields['first_name']
@@ -241,7 +241,7 @@ class ProfileForm(AccountPageForm):
             del self.fields['email']
 
     def save(self):
-        backend = get_auth_backends()[0]
+        backend = get_enabled_auth_backends()[0]
 
         if not backend.supports_change_name:
             self.user.first_name = self.cleaned_data['first_name']
diff --git a/reviewboard/accounts/views.py b/reviewboard/accounts/views.py
index a54c44ca9e52c3719ba7d1b560bc45295180a25d..eeba96c5b703f46ef6ba8e512c49d89af74fbf6b 100644
--- a/reviewboard/accounts/views.py
+++ b/reviewboard/accounts/views.py
@@ -7,7 +7,7 @@ from django.shortcuts import render
 from djblets.auth.views import register
 from djblets.siteconfig.models import SiteConfiguration
 
-from reviewboard.accounts.backends import get_auth_backends
+from reviewboard.accounts.backends import get_enabled_auth_backends
 from reviewboard.accounts.forms.registration import RegistrationForm
 from reviewboard.accounts.models import Profile
 from reviewboard.accounts.pages import get_page_classes
@@ -20,7 +20,7 @@ def account_register(request, next_url='dashboard'):
     on the authentication type the user has configured.
     """
     siteconfig = SiteConfiguration.objects.get_current()
-    auth_backends = get_auth_backends()
+    auth_backends = get_enabled_auth_backends()
 
     if (auth_backends[0].supports_registration and
             siteconfig.get("auth_enable_registration")):
diff --git a/reviewboard/admin/forms.py b/reviewboard/admin/forms.py
index 8cb04e07d3e1c032fb539bc2e39ce280364b14b4..462f48d2ea9d9c8b8a98b299e7d8178551d7d933 100644
--- a/reviewboard/admin/forms.py
+++ b/reviewboard/admin/forms.py
@@ -345,7 +345,9 @@ class AuthenticationSettingsForm(SiteSettingsForm):
         backend_choices = []
         builtin_auth_choice = None
 
-        for backend_id, backend in get_registered_auth_backends():
+        for backend in get_registered_auth_backends():
+            backend_id = backend.backend_id
+
             try:
                 if backend.settings_form:
                     if cur_auth_backend == backend_id:
diff --git a/reviewboard/admin/siteconfig.py b/reviewboard/admin/siteconfig.py
index 6796186fab9248ee3627965bee7507ae118b76d4..76f286a5561d08b3e829613f9e023f9b0d14dac8 100644
--- a/reviewboard/admin/siteconfig.py
+++ b/reviewboard/admin/siteconfig.py
@@ -43,7 +43,7 @@ from djblets.siteconfig.django_settings import (apply_django_settings,
 from djblets.siteconfig.models import SiteConfiguration
 from haystack import connections
 
-from reviewboard.accounts.backends import get_registered_auth_backends
+from reviewboard.accounts.backends import get_registered_auth_backend
 from reviewboard.admin.checks import get_can_enable_syntax_highlighting
 from reviewboard.signals import site_settings_loaded
 
@@ -255,9 +255,8 @@ def load_site_config():
     apply_setting("ADMIN_MEDIA_PREFIX", None, settings.STATIC_URL + "admin/")
 
     # Set the auth backends
-    auth_backend_map = dict(get_registered_auth_backends())
     auth_backend_id = siteconfig.settings.get("auth_backend", "builtin")
-    builtin_backend_obj = auth_backend_map['builtin']
+    builtin_backend_obj = get_registered_auth_backend('builtin')
     builtin_backend = "%s.%s" % (builtin_backend_obj.__module__,
                                  builtin_backend_obj.__name__)
 
@@ -273,14 +272,15 @@ def load_site_config():
 
         if builtin_backend not in custom_backends:
             settings.AUTHENTICATION_BACKENDS += (builtin_backend,)
-    elif auth_backend_id != "builtin" and auth_backend_id in auth_backend_map:
-        backend = auth_backend_map[auth_backend_id]
-
-        settings.AUTHENTICATION_BACKENDS = \
-            ("%s.%s" % (backend.__module__, backend.__name__),
-             builtin_backend)
     else:
-        settings.AUTHENTICATION_BACKENDS = (builtin_backend,)
+        backend = get_registered_auth_backend(auth_backend_id)
+
+        if backend and backend is not builtin_backend_obj:
+            settings.AUTHENTICATION_BACKENDS = \
+                ("%s.%s" % (backend.__module__, backend.__name__),
+                 builtin_backend)
+        else:
+            settings.AUTHENTICATION_BACKENDS = (builtin_backend,)
 
     # Set the storage backend
     storage_backend = siteconfig.settings.get('storage_backend', 'builtin')
diff --git a/reviewboard/extensions/hooks.py b/reviewboard/extensions/hooks.py
index 032b34d908e5c47f8eb9786530ebd427aaaee549..bf2509f5b6f16ece04059516f2286dbcedcb7429 100644
--- a/reviewboard/extensions/hooks.py
+++ b/reviewboard/extensions/hooks.py
@@ -5,6 +5,8 @@ from djblets.extensions.hooks import (DataGridColumnsHook, ExtensionHook,
                                       ExtensionHookPoint, SignalHook,
                                       TemplateHook, URLHook)
 
+from reviewboard.accounts.backends import (register_auth_backend,
+                                           unregister_auth_backend)
 from reviewboard.accounts.pages import (get_page_class,
                                         register_account_page_class,
                                         unregister_account_page_class)
@@ -18,6 +20,28 @@ from reviewboard.reviews.ui.base import register_ui, unregister_ui
 
 
 @six.add_metaclass(ExtensionHookPoint)
+class AuthBackendHook(ExtensionHook):
+    """A hook for registering an authentication backend.
+
+    Authentication backends control user authentication, registration, and
+    user lookup, and user data manipulation.
+
+    This hook takes the class of an authentication backend that should
+    be made available to the server.
+    """
+    def __init__(self, extension, backend_cls):
+        super(AuthBackendHook, self).__init__(extension)
+
+        self.backend_cls = backend_cls
+        register_auth_backend(backend_cls)
+
+    def shutdown(self):
+        super(AuthBackendHook, self).shutdown()
+
+        unregister_auth_backend(self.backend_cls)
+
+
+@six.add_metaclass(ExtensionHookPoint)
 class AccountPagesHook(ExtensionHook):
     """A hook for adding new pages to the My Account page.
 
diff --git a/reviewboard/reviews/fields.py b/reviewboard/reviews/fields.py
index 153286ecc41a82ba9c6380634bde1f1106f5c45f..c0399b851b347d61ee674148da93b418c04ff91b 100644
--- a/reviewboard/reviews/fields.py
+++ b/reviewboard/reviews/fields.py
@@ -655,7 +655,7 @@ def get_review_request_field(field_id):
 
 
 def register_review_request_fieldset(fieldset):
-    """Registeres a custom review request fieldset.
+    """Registers a custom review request fieldset.
 
     A fieldset ID is considered unique and can only be registered once. A
     KeyError will be thrown if attempting to register a second time.
@@ -681,7 +681,7 @@ def register_review_request_fieldset(fieldset):
 
 
 def unregister_review_request_fieldset(fieldset):
-    """Unregisteres a previously registered review request fieldset."""
+    """Unregisters a previously registered review request fieldset."""
     _populate_defaults()
 
     fieldset_id = fieldset.fieldset_id
