diff --git a/docs/rbtools/rbt/commands/login.rst b/docs/rbtools/rbt/commands/login.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3a1742087007f18e708e220f1eb706a5d1830b79
--- /dev/null
+++ b/docs/rbtools/rbt/commands/login.rst
@@ -0,0 +1,15 @@
+.. rbt-command:: rbtools.commands.login.Login
+
+=====
+login
+=====
+
+:command:`rbt login` will prompt for credentials, log the user into
+Review Board, and save a session cookie in :file:`.rbtools-cookies`.
+
+To log the user in without prompting, the :option:`--username` and
+:option:`--password` options can be provided.
+
+
+.. rbt-command-usage::
+.. rbt-command-options::
diff --git a/docs/rbtools/rbt/commands/logout.rst b/docs/rbtools/rbt/commands/logout.rst
new file mode 100644
index 0000000000000000000000000000000000000000..80aff6489818f1acf367481b089a1a79dbdb047c
--- /dev/null
+++ b/docs/rbtools/rbt/commands/logout.rst
@@ -0,0 +1,12 @@
+.. rbt-command:: rbtools.commands.logout.Logout
+
+======
+logout
+======
+
+:command:`rbt logout` will log the user out of their RBTools API session
+on Review Board and delete the session cookie from :file:`.rbtools-cookies`.
+
+
+.. rbt-command-usage::
+.. rbt-command-options::
diff --git a/rbtools/api/client.py b/rbtools/api/client.py
index 117a6d9e6492a41289e6227fbe98f71d7a3f3b64..0077f6d360d1d4405390363c5149edc4764256a0 100644
--- a/rbtools/api/client.py
+++ b/rbtools/api/client.py
@@ -25,3 +25,6 @@ class RBClient(object):
 
     def login(self, *args, **kwargs):
         return self._transport.login(*args, **kwargs)
+
+    def logout(self, *args, **kwargs):
+        return self._transport.logout(*args, **kwargs)
diff --git a/rbtools/api/request.py b/rbtools/api/request.py
index 92e2e6f742fefcf6186e86b63013290fb2c548d4..d7ed2d766a713241518f481d9a3f8871736f7f8a 100644
--- a/rbtools/api/request.py
+++ b/rbtools/api/request.py
@@ -409,22 +409,23 @@ class ReviewBoardServer(object):
         except IOError:
             pass
 
-        if session:
-            parsed_url = urlparse(url)
-            # Get the cookie domain from the url. If the domain
-            # does not contain a '.' (e.g. 'localhost'), we assume
-            # it is a local domain and suffix it (See RFC 2109).
-            domain = parsed_url[1].partition(':')[0]  # Remove Port.
-            if domain.count('.') < 1:
-                domain = '%s.local' % domain
+        # Get the cookie domain from the url. If the domain
+        # does not contain a '.' (e.g. 'localhost'), we assume
+        # it is a local domain and suffix it (See RFC 2109).
+        parsed_url = urlparse(url)
+        self.domain = parsed_url[1].partition(':')[0]  # Remove Port.
+
+        if self.domain.count('.') < 1:
+            self.domain = '%s.local' % self.domain
 
+        if session:
             cookie = Cookie(
                 version=0,
                 name=RB_COOKIE_NAME,
                 value=session,
                 port=None,
                 port_specified=False,
-                domain=domain,
+                domain=self.domain,
                 domain_specified=True,
                 domain_initial_dot=True,
                 path=parsed_url[2],
@@ -485,6 +486,14 @@ class ReviewBoardServer(object):
         """Reset the user information"""
         self.preset_auth_handler.reset(username, password)
 
+    def logout(self):
+        """Logs the user out of the session."""
+        self.preset_auth_handler.reset(None, None)
+        self.make_request(HttpRequest('%ssession/' % self.url,
+                                      method='DELETE'))
+        self.cookie_jar.clear(self.domain)
+        self.cookie_jar.save()
+
     def process_error(self, http_status, data):
         """Processes an error, raising an APIError with the information."""
         try:
diff --git a/rbtools/api/transport/__init__.py b/rbtools/api/transport/__init__.py
index ef241aca9e5b7d81d41339a78f31c683ba98e750..86522085b19e3be8b8eea96b6bf1f1b99d28330e 100644
--- a/rbtools/api/transport/__init__.py
+++ b/rbtools/api/transport/__init__.py
@@ -37,6 +37,15 @@ class Transport(object):
         """
         raise NotImplementedError
 
+    def logout(self):
+        """Logs out of a Review Board session on the server.
+
+        The transport should override this method and provide a way
+        to reset the username and password which will be populated
+        in the next request.
+        """
+        raise NotImplementedError
+
     def execute_request_method(self, method, *args, **kwargs):
         """Execute a method and carry out the returned HttpRequest."""
         return method(*args, **kwargs)
diff --git a/rbtools/api/transport/sync.py b/rbtools/api/transport/sync.py
index 17ef02bfa8f62bfc9575493ce130e4d1d9430836..ffa1eab85c9fc9e0ba783384708871c55339076e 100644
--- a/rbtools/api/transport/sync.py
+++ b/rbtools/api/transport/sync.py
@@ -55,6 +55,9 @@ class SyncTransport(Transport):
     def login(self, username, password):
         self.server.login(username, password)
 
+    def logout(self):
+        self.server.logout()
+
     def execute_request_method(self, method, *args, **kwargs):
         request = method(*args, **kwargs)
 
diff --git a/rbtools/commands/login.py b/rbtools/commands/login.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f0d0b761024eabe99c2f29a9e82d5b47683960a
--- /dev/null
+++ b/rbtools/commands/login.py
@@ -0,0 +1,44 @@
+from __future__ import print_function, unicode_literals
+
+import logging
+
+from rbtools.commands import Command
+from rbtools.utils.users import get_authenticated_session
+
+
+class Login(Command):
+    """Logs into a Review Board server.
+
+    The user will be prompted for a username and password, unless otherwise
+    passed on the command line, allowing the user to log in and save a
+    session cookie without needing to be in a repository or posting to
+    the server.
+
+    If the user is already logged in, this won't do anything.
+    """
+    name = 'login'
+    author = 'The Review Board Project'
+    option_list = [
+        Command.server_options,
+    ]
+
+    def main(self):
+        """Run the command."""
+        server_url = self.get_server_url(None, None)
+        api_client, api_root = self.get_api(server_url)
+
+        session = api_root.get_session(expand='user')
+        was_authenticated = session.authenticated
+
+        if not was_authenticated:
+            session = get_authenticated_session(api_client, api_root,
+                                                auth_required=True,
+                                                session=session)
+
+        if session.authenticated:
+            if not was_authenticated or (self.options.username and
+                                         self.options.password):
+                logging.info('Successfully logged in to Review Board.')
+            else:
+                logging.info('You are already logged in to Review Board at %s',
+                             api_client.url)
diff --git a/rbtools/commands/logout.py b/rbtools/commands/logout.py
new file mode 100644
index 0000000000000000000000000000000000000000..36fe017159aa51a18a309018252d6e64588b317e
--- /dev/null
+++ b/rbtools/commands/logout.py
@@ -0,0 +1,34 @@
+from __future__ import print_function, unicode_literals
+
+import logging
+
+from rbtools.commands import Command
+
+
+class Logout(Command):
+    """Logs out of a Review Board server.
+
+    The session cookie will be removed into from the .rbtools-cookies
+    file. The next RBTools command you run will then prompt for credentials.
+    """
+    name = 'logout'
+    author = 'The Review Board Project'
+    option_list = [
+        Command.server_options,
+    ]
+
+    def main(self):
+        """Run the command."""
+        server_url = self.get_server_url(None, None)
+        api_client, api_root = self.get_api(server_url)
+
+        session = api_root.get_session(expand='user')
+
+        if session.authenticated:
+            api_client.logout()
+
+            logging.info('You are now logged out of Review Board at %s',
+                         api_client.url)
+        else:
+            logging.info('You are already logged out of Review Board at %s',
+                         api_client.url)
diff --git a/rbtools/utils/users.py b/rbtools/utils/users.py
index 1d8f7308ac7755459b350ad1aded5a9f5e56dc5d..5e1c67c61b651fac3f47123587a53062bb7b6305 100644
--- a/rbtools/utils/users.py
+++ b/rbtools/utils/users.py
@@ -4,36 +4,47 @@ import getpass
 import logging
 import sys
 
-from six.moves import input
+from six.moves import input, range
 
 from rbtools.api.errors import AuthorizationError
 from rbtools.commands import CommandError
 
 
-def get_authenticated_session(api_client, api_root, auth_required=False):
+def get_authenticated_session(api_client, api_root, auth_required=False,
+                              session=None, num_retries=3):
     """Return an authenticated session.
 
     None will be returned if the user is not authenticated, unless the
     'auth_required' parameter is True, in which case the user will be prompted
     to login.
     """
-    session = api_root.get_session(expand='user')
+    if not session:
+        session = api_root.get_session(expand='user')
 
     if not session.authenticated:
         if not auth_required:
             return None
 
-        logging.warning('You are not authenticated with the Review Board '
-                        'server at %s, please login.' % api_client.url)
-        sys.stderr.write('Username: ')
-        username = input()
-        password = getpass.getpass(b'Password: ')
-        api_client.login(username, password)
-
-        try:
-            session = session.get_self()
-        except AuthorizationError:
-            raise CommandError('You are not authenticated.')
+        logging.info('Please log in to the Review Board server at %s',
+                     api_client.url)
+
+        for i in range(num_retries):
+            sys.stderr.write('Username: ')
+            username = input()
+            password = getpass.getpass(b'Password: ')
+            api_client.login(username, password)
+
+            try:
+                session = session.get_self()
+                break
+            except AuthorizationError:
+                sys.stderr.write('\n')
+
+                if i < num_retries - 1:
+                    logging.error('The username or password was incorrect. '
+                                  'Please try again.')
+                else:
+                    raise CommandError('Unable to log in to Review Board.')
 
     return session
 
diff --git a/setup.py b/setup.py
index 41e9f0ae744bd4c3ecb1ada9e911eef57a886ec7..0427aef662b23cc703e5a9ba16e898cb6b93d5bc 100755
--- a/setup.py
+++ b/setup.py
@@ -76,6 +76,8 @@ rb_commands = [
     'diff = rbtools.commands.diff:Diff',
     'land = rbtools.commands.land:Land',
     'list-repo-types = rbtools.commands.list_repo_types:ListRepoTypes',
+    'login = rbtools.commands.login:Login',
+    'logout = rbtools.commands.logout:Logout',
     'patch = rbtools.commands.patch:Patch',
     'post = rbtools.commands.post:Post',
     'publish = rbtools.commands.publish:Publish',
