diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index 79e9746e85e3837535ae900b51d492a6d58e7754_cGFyYW1pa28vX19pbml0X18ucHk=..4df3c07d8edb620b9b7751d0a547a0b13e7960f1_cGFyYW1pa28vX19pbml0X18ucHk= 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -24,6 +24,7 @@
     SSHClient, MissingHostKeyPolicy, AutoAddPolicy, RejectPolicy,
     WarningPolicy,
 )
+from paramiko.authenticator import Authenticator
 from paramiko.auth_handler import AuthHandler
 from paramiko.ssh_gss import GSSAuth, GSS_AUTH_AVAILABLE, GSS_EXCEPTIONS
 from paramiko.channel import Channel, ChannelFile
diff --git a/paramiko/authenticator.py b/paramiko/authenticator.py
new file mode 100644
index 0000000000000000000000000000000000000000..4df3c07d8edb620b9b7751d0a547a0b13e7960f1_cGFyYW1pa28vYXV0aGVudGljYXRvci5weQ==
--- /dev/null
+++ b/paramiko/authenticator.py
@@ -0,0 +1,74 @@
+"""
+High level authentication logic module.
+
+Largely replaces what used to be implemented solely within `SSHClient` and its
+``_auth()`` method.
+
+Technically, this module's main API member - `Authenticator` - sits below
+`SSHClient` (meaning it can be used by non-Client-based code) and above
+`Transport` (which provides the bare auth SSH message 'levers' only.)
+
+.. note::
+    This is not to be confused with the `paramiko.auth_handler` module, which
+    sits *below* (or within) `Transport`, handling the low level guts of
+    submitting authentication protocol messages and awaiting their responses.
+"""
+
+class Authenticator(object):
+    """
+    Wraps a `Transport` and uses it to authenticate or die trying.
+
+    Lifecycle is relatively straightforward:
+
+    - Instantiate with a handle onto a `Transport` object. This object must
+      already have been prepared for authentication by calling
+      `Transport.start_client`.
+    - Call the instance's `authenticate_with_kwargs` method with as many or few
+      auth-source keyword arguments as needed, which will:
+        - attempt to authenticate in a documented order of preference
+        - if successful, return an `AuthenticationResult`
+        - if unsuccessful or if additional auth factors are required, raise an
+          `AuthenticationException` (or subclass thereof) which will exhibit a
+          ``.result`` attribute whose value is an `AuthenticationResult`.
+        - either way, the point is that the caller will have access to an
+          `AuthenticationResult` object exposing the various auth sources
+          tried, what order they were tried in, and what the result was.
+        - see API docs for `authenticate` for further details.
+    - Alternately, for tighter control of which auth sources are tried and in
+      what order, call `authenticate` directly (it's what implements the guts
+      of `authenticate_with_kwargs`) which foregoes most kwargs in lieu of an
+      iterable containing `AuthSource` objects.
+    """
+    def __init__(self, transport):
+        # TODO: probably sanity check transport state and bail early if it's
+        # not ready.
+        # TODO: consider adding some more of SSHClient.connect (optionally, if
+        # the caller didn't already do these things) like the call to
+        # .start_client; then update lifecycle in docstring.
+        pass
+
+    def authenticate_with_kwargs(self, lots_o_kwargs_here):
+        # Basically SSHClient._auth signature...then calls
+        # sources_from_kwargs() and stuffs result into authenticate()?
+        # TODO: at the start, just copypasta/tweak SSHClient._auth so the
+        # break-up is tested; THEN move to the newer cleaner shit?
+        # TODO: this is probably a good spot to reject the
+        # password-as-passphrase bit; accept distinct kwargs and require
+        # SSHClient to implement the fallback on its end.
+        pass
+
+    def authenticate(self, username, *sources):
+        # TODO: define AuthSource (maybe rename...lol), should be lightweight,
+        # pairing an auth type with some value or iterable of values
+        # TODO: implement cleaner version of SSHClient._auth, somehow, that
+        # handles multi-factor auth much better than the current shite
+        # trickledown. (Be very TDD here...! Perhaps wait until single-source
+        # tests all pass first, then can ensure they continue to do so?)
+        pass
+
+    def sources_from_kwargs(self, kwargs):
+        # TODO: **kwargs? whatever, this is mostly internal
+        # TODO: this should implement, and document, the current (and/or then
+        # desired) way that a pile of kwargs becomes an ordered set of
+        # attempted auths...
+        pass
diff --git a/tests/conftest.py b/tests/conftest.py
index 79e9746e85e3837535ae900b51d492a6d58e7754_dGVzdHMvY29uZnRlc3QucHk=..4df3c07d8edb620b9b7751d0a547a0b13e7960f1_dGVzdHMvY29uZnRlc3QucHk= 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,7 +4,7 @@
 import threading
 
 import pytest
-from paramiko import RSAKey, SFTPServer, SFTP, Transport
+from paramiko import RSAKey, SFTPServer, SFTP, Transport, Authenticator
 
 from ._loop import LoopSocket
 from ._stub_sftp import StubServer, StubSFTPServer
@@ -62,6 +62,16 @@
     sockc.close()
 
 
+@pytest.fixture
+def authn(trans):
+    """
+    Yields an `Authenticator` wrapping a `Transport` (from `trans`.)
+    """
+    # TODO: call start_client() on the trans too? or push that into
+    # `Authenticator` itself?
+    yield Authenticator(trans)
+
+
 def make_sftp_folder():
     """
     Ensure expected target temp folder exists on the remote end.
diff --git a/tests/test_util.py b/tests/test_util.py
index 79e9746e85e3837535ae900b51d492a6d58e7754_dGVzdHMvdGVzdF91dGlsLnB5..4df3c07d8edb620b9b7751d0a547a0b13e7960f1_dGVzdHMvdGVzdF91dGlsLnB5 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -73,6 +73,7 @@
             Agent
             AgentKey
             AuthenticationException
+            Authenticator
             AutoAddPolicy
             BadAuthenticationType
             BufferedFile