diff --git a/core/modules/user/lib/Drupal/user/Controller/UserController.php b/core/modules/user/lib/Drupal/user/Controller/UserController.php
index 864e095..90c6bdf 100644
--- a/core/modules/user/lib/Drupal/user/Controller/UserController.php
+++ b/core/modules/user/lib/Drupal/user/Controller/UserController.php
@@ -7,27 +7,33 @@
 
 namespace Drupal\user\Controller;
 
-use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\RedirectResponse;
 use Symfony\Component\HttpFoundation\Request;
-use Drupal\Core\Controller\ControllerInterface;
 
 /**
  * Controller routines for user routes.
  */
-class UserController implements ControllerInterface {
+class UserController {
 
   /**
-   * Constructs an UserController object.
+   * Returns the user page.
+   *
+   * Displays user profile if user is logged in, or login form for anonymous
+   * users.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse|array
+   *   Returns either a redirect to the user page or the render
+   *   array of the login form.
    */
-  public function __construct() {
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static();
+  public function userPage() {
+    global $user;
+    if ($user->uid) {
+      $url = 'user/' . $user->uid;
+    }
+    else {
+      $url = 'user/login';
+    }
+    return new RedirectResponse(url($url, array('absolute' => TRUE)));
   }
 
   /**
diff --git a/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php b/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php
new file mode 100644
index 0000000..4535fc1
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Form/UserLoginForm.php
@@ -0,0 +1,222 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Form\UserLoginForm.
+ */
+
+namespace Drupal\user\Form;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Controller\ControllerInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\Core\Form\FormInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * @todo.
+ */
+class UserLoginForm implements FormInterface, ControllerInterface {
+
+  /**
+   * @var \Drupal\Core\Config\ConfigFactory
+   */
+  protected $configFactory;
+
+  /**
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * @var \Drupal\Core\Flood\FloodInterface
+   */
+  protected $flood;
+
+  /**
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood service.
+   */
+  function __construct(ConfigFactory $config_factory, FloodInterface $flood) {
+    $this->configFactory = $config_factory;
+    $this->flood = $flood;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('flood')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'user_login_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, array &$form_state, Request $request = NULL) {
+    $this->request = $request;
+    // Display login form:
+    $form['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Username'),
+      '#size' => 60,
+      '#maxlength' => USERNAME_MAX_LENGTH,
+      '#description' => t('Enter your @s username.', array('@s' => $this->configFactory->get('system.site')->get('name'))),
+      '#required' => TRUE,
+      '#attributes' => array(
+        'autocorrect' => 'off',
+        'autocapitalize' => 'off',
+        'spellcheck' => 'false',
+        'autofocus' => 'autofocus',
+      ),
+    );
+
+    $form['pass'] = array(
+      '#type' => 'password',
+      '#title' => t('Password'),
+      '#size' => 60,
+      '#description' => t('Enter the password that accompanies your username.'),
+      '#required' => TRUE,
+    );
+
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
+
+    $form['#validate'][] = array($this, 'validateName');
+    $form['#validate'][] = array($this, 'validateAuthentication');
+    $form['#validate'][] = array($this, 'validateFinal');
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    global $user;
+    $user = user_load($form_state['uid']);
+    $form_state['redirect'] = 'user/' . $user->uid;
+
+    user_login_finalize($form_state);
+  }
+
+  /**
+   * A FAPI validate handler. Sets an error if supplied username has been blocked.
+   */
+  public function validateName(array &$form, array &$form_state) {
+    if (!empty($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) {
+      // Blocked in user administration.
+      form_set_error('name', t('The username %name has not been activated or is blocked.', array('%name' => $form_state['values']['name'])));
+    }
+  }
+
+  /**
+   * A validate handler on the login form. Check supplied username/password
+   * against local users table. If successful, $form_state['uid']
+   * is set to the matching user ID.
+   */
+  public function validateAuthentication(array &$form, array &$form_state) {
+    $password = trim($form_state['values']['pass']);
+    $flood_config = $this->configFactory->get('user.flood');
+    if (!empty($form_state['values']['name']) && !empty($password)) {
+      // Do not allow any login from the current user's IP if the limit has been
+      // reached. Default is 50 failed attempts allowed in one hour. This is
+      // independent of the per-user limit to catch attempts from one IP to log
+      // in to many different user accounts.  We have a reasonably high limit
+      // since there may be only one apparent IP for all users at an institution.
+      if (!$this->flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
+        $form_state['flood_control_triggered'] = 'ip';
+        return;
+      }
+      $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject();
+      if ($account) {
+        if ($flood_config->get('uid_only')) {
+          // Register flood events based on the uid only, so they apply for any
+          // IP address. This is the most secure option.
+          $identifier = $account->uid;
+        }
+        else {
+          // The default identifier is a combination of uid and IP address. This
+          // is less secure but more resistant to denial-of-service attacks that
+          // could lock out all users with public user names.
+          $identifier = $account->uid . '-' . $this->request->getClientIP();
+        }
+        $form_state['flood_control_user_identifier'] = $identifier;
+
+        // Don't allow login if the limit for this user has been reached.
+        // Default is to allow 5 failed attempts every 6 hours.
+        if (!$this->flood->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
+          $form_state['flood_control_triggered'] = 'user';
+          return;
+        }
+      }
+      // We are not limited by flood control, so try to authenticate.
+      // Set $form_state['uid'] as a flag for user_login_final_validate().
+      $form_state['uid'] = user_authenticate($form_state['values']['name'], $password);
+    }
+  }
+
+  /**
+   * The final validation handler on the login form.
+   *
+   * Sets a form error if user has not been authenticated, or if too many
+   * logins have been attempted. This validation function should always
+   * be the last one.
+   */
+  public function validateFinal(array &$form, array &$form_state) {
+    $flood_config = $this->configFactory->get('user.flood');
+    if (empty($form_state['uid'])) {
+      // Always register an IP-based failed login event.
+      $this->flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
+      // Register a per-user failed login event.
+      if (isset($form_state['flood_control_user_identifier'])) {
+        $this->flood->register('user.failed_login_user', $flood_config->get('user_window'), $form_state['flood_control_user_identifier']);
+      }
+
+      if (isset($form_state['flood_control_triggered'])) {
+        if ($form_state['flood_control_triggered'] == 'user') {
+          form_set_error('name', format_plural($flood_config->get('user_limit'), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
+        }
+        else {
+          // We did not find a uid, so the limit is IP-based.
+          form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
+        }
+      }
+      else {
+        form_set_error('name', t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>', array('@password' => url('user/password', array('query' => array('name' => $form_state['values']['name']))))));
+        if (user_load_by_name($form_state['values']['name'])) {
+          watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
+        }
+        else {
+          // If the username entered is not a valid user,
+          // only store the IP address.
+          watchdog('user', 'Login attempt failed from %ip.', array('%ip' => $this->request->getClientIp()));
+        }
+      }
+    }
+    elseif (isset($form_state['flood_control_user_identifier'])) {
+      // Clear past failures for this user so as not to block a user who might
+      // log in and out more than once in an hour.
+      $this->flood->clear('user.failed_login_user', $form_state['flood_control_user_identifier']);
+    }
+  }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php b/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php
index 0c8aa2a..b785aef 100644
--- a/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php
+++ b/core/modules/user/lib/Drupal/user/Plugin/Block/UserLoginBlock.php
@@ -10,6 +10,7 @@
 use Drupal\block\BlockBase;
 use Drupal\Component\Annotation\Plugin;
 use Drupal\Core\Annotation\Translation;
+use Drupal\user\Form\UserLoginForm;
 
 /**
  * Provides a 'User login' block.
@@ -33,7 +34,7 @@ public function access() {
    * {@inheritdoc}
    */
   public function build() {
-    $form = drupal_get_form('user_login_form');
+    $form = drupal_get_form(new UserLoginForm(\Drupal::service('config.factory'), \Drupal::service('flood')));
     unset($form['name']['#attributes']['autofocus']);
     unset($form['name']['#description']);
     unset($form['pass']['#description']);
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 9663af5..712b5f1 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -878,16 +878,13 @@ function user_menu() {
   $items['user'] = array(
     'title' => 'User account',
     'title callback' => 'user_menu_title',
-    'page callback' => 'user_page',
-    'access callback' => TRUE,
-    'file' => 'user.pages.inc',
     'weight' => -10,
+    'route_name' => 'user_page',
     'menu_name' => 'account',
   );
 
   $items['user/login'] = array(
     'title' => 'Log in',
-    'access callback' => 'user_is_anonymous',
     'type' => MENU_DEFAULT_LOCAL_TASK,
   );
   // Other authentication methods may add pages below user/login/.
@@ -1182,44 +1179,6 @@ function user_page_title($account) {
 }
 
 /**
- * Form builder; the main user login form.
- *
- * @ingroup forms
- */
-function user_login_form($form, &$form_state) {
-  // Display login form:
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Username'),
-    '#size' => 60,
-    '#maxlength' => USERNAME_MAX_LENGTH,
-    '#description' => t('Enter your @s username.', array('@s' => config('system.site')->get('name'))),
-    '#required' => TRUE,
-    '#attributes' => array(
-      'autocorrect' => 'off',
-      'autocapitalize' => 'off',
-      'spellcheck' => 'false',
-      'autofocus' => 'autofocus',
-    ),
-  );
-
-  $form['pass'] = array(
-    '#type' => 'password',
-    '#title' => t('Password'),
-    '#size' => 60,
-    '#description' => t('Enter the password that accompanies your username.'),
-    '#required' => TRUE,
-  );
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Log in'));
-
-  $form['#validate'] = user_login_default_validators();
-
-  return $form;
-}
-
-/**
  * Set up a series for validators which check for blocked users,
  * then authenticate against local database, then return an error if
  * authentication fails. Distributed authentication modules are welcome
@@ -1242,109 +1201,6 @@ function user_login_default_validators() {
 }
 
 /**
- * A FAPI validate handler. Sets an error if supplied username has been blocked.
- */
-function user_login_name_validate($form, &$form_state) {
-  if (!empty($form_state['values']['name']) && user_is_blocked($form_state['values']['name'])) {
-    // Blocked in user administration.
-    form_set_error('name', t('The username %name has not been activated or is blocked.', array('%name' => $form_state['values']['name'])));
-  }
-}
-
-/**
- * A validate handler on the login form. Check supplied username/password
- * against local users table. If successful, $form_state['uid']
- * is set to the matching user ID.
- */
-function user_login_authenticate_validate($form, &$form_state) {
-  $password = trim($form_state['values']['pass']);
-  $flood_config = config('user.flood');
-  $flood = Drupal::service('flood');
-  if (!empty($form_state['values']['name']) && !empty($password)) {
-    // Do not allow any login from the current user's IP if the limit has been
-    // reached. Default is 50 failed attempts allowed in one hour. This is
-    // independent of the per-user limit to catch attempts from one IP to log
-    // in to many different user accounts.  We have a reasonably high limit
-    // since there may be only one apparent IP for all users at an institution.
-    if (!$flood->isAllowed('user.failed_login_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
-      $form_state['flood_control_triggered'] = 'ip';
-      return;
-    }
-    $account = db_query("SELECT * FROM {users} WHERE name = :name AND status = 1", array(':name' => $form_state['values']['name']))->fetchObject();
-    if ($account) {
-      if ($flood_config->get('uid_only')) {
-        // Register flood events based on the uid only, so they apply for any
-        // IP address. This is the most secure option.
-        $identifier = $account->uid;
-      }
-      else {
-        // The default identifier is a combination of uid and IP address. This
-        // is less secure but more resistant to denial-of-service attacks that
-        // could lock out all users with public user names.
-        $identifier = $account->uid . '-' . Drupal::request()->getClientIP();
-      }
-      $form_state['flood_control_user_identifier'] = $identifier;
-
-      // Don't allow login if the limit for this user has been reached.
-      // Default is to allow 5 failed attempts every 6 hours.
-      if (!$flood->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
-        $form_state['flood_control_triggered'] = 'user';
-        return;
-      }
-    }
-    // We are not limited by flood control, so try to authenticate.
-    // Set $form_state['uid'] as a flag for user_login_final_validate().
-    $form_state['uid'] = user_authenticate($form_state['values']['name'], $password);
-  }
-}
-
-/**
- * The final validation handler on the login form.
- *
- * Sets a form error if user has not been authenticated, or if too many
- * logins have been attempted. This validation function should always
- * be the last one.
- */
-function user_login_final_validate($form, &$form_state) {
-  $flood_config = config('user.flood');
-  $flood = Drupal::service('flood');
-  if (empty($form_state['uid'])) {
-    // Always register an IP-based failed login event.
-    $flood->register('user.failed_login_ip', $flood_config->get('ip_window'));
-    // Register a per-user failed login event.
-    if (isset($form_state['flood_control_user_identifier'])) {
-      $flood->register('user.failed_login_user', $flood_config->get('user_window'), $form_state['flood_control_user_identifier']);
-    }
-
-    if (isset($form_state['flood_control_triggered'])) {
-      if ($form_state['flood_control_triggered'] == 'user') {
-        form_set_error('name', format_plural($flood_config->get('user_limit'), 'Sorry, there has been more than one failed login attempt for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', 'Sorry, there have been more than @count failed login attempts for this account. It is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
-      }
-      else {
-        // We did not find a uid, so the limit is IP-based.
-        form_set_error('name', t('Sorry, too many failed login attempts from your IP address. This IP address is temporarily blocked. Try again later or <a href="@url">request a new password</a>.', array('@url' => url('user/password'))));
-      }
-    }
-    else {
-      form_set_error('name', t('Sorry, unrecognized username or password. <a href="@password">Have you forgotten your password?</a>', array('@password' => url('user/password', array('query' => array('name' => $form_state['values']['name']))))));
-      if (user_load_by_name($form_state['values']['name'])) {
-        watchdog('user', 'Login attempt failed for %user.', array('%user' => $form_state['values']['name']));
-      }
-      else {
-        // If the username entered is not a valid user,
-        // only store the IP address.
-        watchdog('user', 'Login attempt failed from %ip.', array('%ip' => Drupal::request()->getClientIp()));
-      }
-    }
-  }
-  elseif (isset($form_state['flood_control_user_identifier'])) {
-    // Clear past failures for this user so as not to block a user who might
-    // log in and out more than once in an hour.
-    $flood->clear('user.failed_login_user', $form_state['flood_control_user_identifier']);
-  }
-}
-
-/**
  * Try to validate the user's login credentials locally.
  *
  * @param $name
@@ -1406,19 +1262,6 @@ function user_login_finalize(&$edit = array()) {
 }
 
 /**
- * Submit handler for the login form. Load $user object and perform standard login
- * tasks. The user is then redirected to the My Account page. Setting the
- * destination in the query string overrides the redirect.
- */
-function user_login_form_submit($form, &$form_state) {
-  global $user;
-  $user = user_load($form_state['uid']);
-  $form_state['redirect'] = 'user/' . $user->uid;
-
-  user_login_finalize($form_state);
-}
-
-/**
  * Implements hook_user_login().
  */
 function user_user_login($account) {
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 26fb040..ffd16eb 100644
--- a/core/modules/user/user.pages.inc
+++ b/core/modules/user/user.pages.inc
@@ -398,19 +398,3 @@ function user_cancel_confirm($account, $timestamp = 0, $hashed_pass = '') {
   }
   throw new AccessDeniedHttpException();
 }
-
-/**
- * Access callback for path /user.
- *
- * Displays user profile if user is logged in, or login form for anonymous
- * users.
- */
-function user_page() {
-  global $user;
-  if ($user->uid) {
-    return new RedirectResponse(url('user/' . $user->uid, array('absolute' => TRUE)));
-  }
-  else {
-    return drupal_get_form('user_login_form');
-  }
-}
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index b48c808..90baba8 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -68,3 +68,17 @@ user_role_delete:
     _form: '\Drupal\user\Form\UserRoleDelete'
   requirements:
     _entity_access: user_role.delete
+
+user_page:
+  pattern: '/user'
+  defaults:
+    _content: '\Drupal\user\Controller\UserController::userPage'
+  requirements:
+    _access: 'TRUE'
+
+user_login:
+  pattern: '/user/login'
+  defaults:
+    _form: '\Drupal\user\Form\UserLoginForm'
+  requirements:
+    _access: 'TRUE'
