diff --git a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
index 01f3620..3accdbf 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ExceptionJsonSubscriber.php
@@ -28,14 +28,23 @@ protected static function getPriority() {
   }
 
   /**
+   * Handles a 400 error for JSON.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on400(GetResponseForExceptionEvent $event) {
+    $this->setEventResponse($event, Response::HTTP_BAD_REQUEST);
+  }
+
+  /**
    * Handles a 403 error for JSON.
    *
    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
    *   The event to process.
    */
   public function on403(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_FORBIDDEN);
-    $event->setResponse($response);
+    $this->setEventResponse($event, Response::HTTP_FORBIDDEN);
   }
 
   /**
@@ -45,8 +54,7 @@ public function on403(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on404(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_NOT_FOUND);
-    $event->setResponse($response);
+    $this->setEventResponse($event, Response::HTTP_NOT_FOUND);
   }
 
   /**
@@ -56,8 +64,7 @@ public function on404(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on405(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(array('message' => $event->getException()->getMessage()), Response::HTTP_METHOD_NOT_ALLOWED);
-    $event->setResponse($response);
+    $this->setEventResponse($event, Response::HTTP_METHOD_NOT_ALLOWED);
   }
 
   /**
@@ -67,7 +74,19 @@ public function on405(GetResponseForExceptionEvent $event) {
    *   The event to process.
    */
   public function on406(GetResponseForExceptionEvent $event) {
-    $response = new JsonResponse(['message' => $event->getException()->getMessage()], Response::HTTP_NOT_ACCEPTABLE);
+    $this->setEventResponse($event, Response::HTTP_NOT_ACCEPTABLE);
+  }
+
+  /**
+   * Sets the Response for the exception event.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The current exception event.
+   * @param int $status
+   *   The HTTP status code to set for the response.
+   */
+  protected function setEventResponse(GetResponseForExceptionEvent $event, $status) {
+    $response = new JsonResponse(['message' => $event->getException()->getMessage()], $status);
     $event->setResponse($response);
   }
 
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index c510ab1..f3c3137 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -64,3 +64,13 @@ services:
     class: Drupal\serialization\EntityResolver\TargetIdResolver
     tags:
       - { name: entity_resolver}
+  serialization.exception.default_http:
+    class: Drupal\serialization\EventSubscriber\ExceptionHttpSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@serializer', '%serializer.formats%']
+  serialization.user_last_access_subscriber:
+    class: Drupal\serialization\EventSubscriber\UserRouteAlterSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@serializer', '%serializer.formats%']
diff --git a/core/modules/serialization/src/EventSubscriber/ExceptionHttpSubscriber.php b/core/modules/serialization/src/EventSubscriber/ExceptionHttpSubscriber.php
new file mode 100644
index 0000000..9b4cba2
--- /dev/null
+++ b/core/modules/serialization/src/EventSubscriber/ExceptionHttpSubscriber.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\serialization\EventSubscriber;
+
+use Drupal\Core\EventSubscriber\HttpExceptionSubscriberBase;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Default handling for HTTP errors.
+ */
+class ExceptionHttpSubscriber extends HttpExceptionSubscriberBase {
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The available serialization formats.
+   *
+   * @var array
+   */
+  protected $serializerFormats = array();
+
+  /**
+   * ExceptionHttpSubscriber constructor.
+   *
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer service.
+   * @param array $serializer_formats
+   *   The available serializer formats.
+   */
+  public function __construct(SerializerInterface $serializer, array $serializer_formats) {
+    $this->serializer = $serializer;
+    $this->serializerFormats = $serializer_formats;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getHandledFormats() {
+    return $this->serializerFormats;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getPriority() {
+    // This will fire after the most common HTML handler, since HTML requests
+    // are still more common than HTTP requests.
+    return -75;
+  }
+
+  /**
+   * Handles a 400 error for HTTP.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on400(GetResponseForExceptionEvent $event) {
+    $this->setEventResponse($event, Response::HTTP_BAD_REQUEST);
+  }
+
+  /**
+   * Handles a 403 error for HTTP.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on403(GetResponseForExceptionEvent $event) {
+    $this->setEventResponse($event, Response::HTTP_FORBIDDEN);
+  }
+
+  /**
+   * Handles a 404 error for HTTP.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on404(GetResponseForExceptionEvent $event) {
+    $this->setEventResponse($event, Response::HTTP_NOT_FOUND);
+  }
+
+  /**
+   * Handles a 405 error for HTTP.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on405(GetResponseForExceptionEvent $event) {
+    $this->setEventResponse($event, Response::HTTP_METHOD_NOT_ALLOWED);
+  }
+
+  /**
+   * Handles a 406 error for HTTP.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The event to process.
+   */
+  public function on406(GetResponseForExceptionEvent $event) {
+    $this->setEventResponse($event, Response::HTTP_NOT_ACCEPTABLE);
+  }
+
+  /**
+   * Sets the Response for the exception event.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
+   *   The current exception event.
+   * @param int $status
+   *   The HTTP status code to set for the response.
+   */
+  protected function setEventResponse(GetResponseForExceptionEvent $event, $status) {
+    $format = $event->getRequest()->getRequestFormat();
+    $content = ['message' => $event->getException()->getMessage()];
+    $encoded_content = $this->serializer->serialize($content, $format);
+    $response = new Response($encoded_content, $status);
+    $event->setResponse($response);
+  }
+
+}
diff --git a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php
new file mode 100644
index 0000000..dd6e2a7
--- /dev/null
+++ b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\serialization\EventSubscriber;
+
+
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\Serializer\SerializerInterface;
+
+/**
+ * Alters user authentication routes to add formats supported by this module.
+ */
+class UserRouteAlterSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The available serialization formats.
+   *
+   * @var array
+   */
+  protected $serializerFormats = array();
+
+  /**
+   * UserRouteAlterSubscriber constructor.
+   *
+   * @param \Symfony\Component\Serializer\SerializerInterface $serializer
+   *   The serializer service.
+   * @param array $serializer_formats
+   *   The available serializer formats.
+   */
+  public function __construct(SerializerInterface $serializer, array $serializer_formats) {
+    $this->serializer = $serializer;
+    $this->serializerFormats = $serializer_formats;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[RoutingEvents::ALTER][] = 'onRoutingRouteAlterAddFormats';
+    return $events;
+  }
+
+  /**
+   * Adds supported formats to the user authentication http routes.
+   *
+   * @param \Drupal\Core\Routing\RouteBuildEvent $event
+   *   The event to process.
+   */
+  public function onRoutingRouteAlterAddFormats(RouteBuildEvent $event) {
+    $route_names = [
+      'user.login_status.http',
+      'user.login.http',
+      'user.logout.http',
+    ];
+    $routes = $event->getRouteCollection();
+    foreach ($route_names as $route_name) {
+      $route = $routes->get($route_name);
+      $formats = explode('|', $route->getRequirement('_format'));
+      $formats = array_unique($formats + $this->serializerFormats);
+      $route->setRequirement('_format', implode('|', $formats));
+    }
+  }
+
+}
diff --git a/core/modules/user/src/Controller/UserAuthenticationController.php b/core/modules/user/src/Controller/UserAuthenticationController.php
new file mode 100644
index 0000000..3027fb9
--- /dev/null
+++ b/core/modules/user/src/Controller/UserAuthenticationController.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Drupal\user\Controller;
+
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\user\UserAuthInterface;
+use Drupal\user\UserInterface;
+use Drupal\user\UserStorageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Provides controllers for login, login status and logout.
+ */
+class UserAuthenticationController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * String sent in responses, to describe the user as being logged in.
+   *
+   * @var string
+   */
+  const LOGGED_IN = 1;
+
+  /**
+   * String sent in responses, to describe the user as being logged out.
+   *
+   * @var string
+   */
+  const LOGGED_OUT = 0;
+
+  /**
+   * The flood controller.
+   *
+   * @var \Drupal\Core\Flood\FloodInterface
+   */
+  protected $flood;
+
+  /**
+   * The user storage.
+   *
+   * @var \Drupal\user\UserStorageInterface
+   */
+  protected $userStorage;
+
+  /**
+   * The CSRF token generator.
+   *
+   * @var \Drupal\Core\Access\CsrfTokenGenerator
+   */
+  protected $csrfToken;
+
+  /**
+   * The user authentication.
+   *
+   * @var \Drupal\user\UserAuthInterface
+   */
+  protected $userAuth;
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The available serialization formats.
+   *
+   * @var array
+   */
+  protected $serializerFormats = array();
+
+  /**
+   * Constructs a new UserAuthenticationController object.
+   *
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood controller.
+   * @param \Drupal\user\UserStorageInterface $user_storage
+   *   The user storage.
+   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
+   *   The CSRF token generator.
+   * @param \Drupal\user\UserAuthInterface $user_auth
+   *   The user authentication.
+   * @param \Symfony\Component\Serializer\Serializer $serializer
+   *   The serializer.
+   * @param array $serializer_formats
+   *   The available serialization formats.
+   */
+  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, Serializer $serializer, array $serializer_formats) {
+    $this->flood = $flood;
+    $this->userStorage = $user_storage;
+    $this->csrfToken = $csrf_token;
+    $this->userAuth = $user_auth;
+    $this->serializer = $serializer;
+    $this->serializerFormats = $serializer_formats;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    if ($container->hasParameter('serializer.formats') && $container->has('serializer')) {
+      $serializer = $container->get('serializer');
+      $formats = $container->getParameter('serializer.formats');
+    }
+    else {
+      $formats = ['json'];
+      $encoders = [new JsonEncoder()];
+      $serializer = new Serializer([], $encoders);
+    }
+
+    return new static(
+      $container->get('flood'),
+      $container->get('entity_type.manager')->getStorage('user'),
+      $container->get('csrf_token'),
+      $container->get('user.auth'),
+      $serializer,
+      $formats
+    );
+  }
+
+  /**
+   * Logs in a user.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   Returns a response which contains the ID and CSRF token.
+   */
+  public function login(Request $request) {
+    $format = $this->getRequestFormat($request);
+
+    $content = $request->getContent();
+    $credentials = $this->serializer->decode($content, $format);
+    if (!isset($credentials['name']) && !isset($credentials['pass'])) {
+      throw new BadRequestHttpException('Missing credentials.');
+    }
+
+    if (!isset($credentials['name'])) {
+      throw new BadRequestHttpException('Missing credentials.name.');
+    }
+    if (!isset($credentials['pass'])) {
+      throw new BadRequestHttpException('Missing credentials.pass.');
+    }
+
+    if (!$this->isFloodBlocked()) {
+      throw new BadRequestHttpException('Blocked.');
+    }
+
+    if ($this->userIsBlocked($credentials['name'])) {
+      throw new BadRequestHttpException('The user has not been activated or is blocked.');
+    }
+
+    if ($uid = $this->userAuth->authenticate($credentials['name'], $credentials['pass'])) {
+      /** @var \Drupal\user\UserInterface $user */
+      $user = $this->userStorage->load($uid);
+      $this->userLoginFinalize($user);
+
+      // Send basic metadata about the logged in user.
+      $response_data = [];
+      if ($user->get('uid')->access('view')) {
+        $response_data['current_user']['uid'] = $user->id();
+      }
+      if ($user->get('roles')->access('view')) {
+        $response_data['current_user']['roles'] = $user->getRoles();
+      }
+      if ($user->get('name')->access('view')) {
+        $response_data['current_user']['name'] = $user->getAccountName();
+      }
+      $response_data['csrf_token'] = $this->csrfToken->get('rest');
+
+      $encoded_response_data = $this->serializer->encode($response_data, $format);
+      return new Response($encoded_response_data);
+    }
+
+    $this->flood->register('rest.login_cookie', $this->configFactory->get('user.flood')->get('user_window'));
+    throw new BadRequestHttpException('Sorry, unrecognized username or password.');
+  }
+
+  /**
+   * Verifies if the user is blocked.
+   *
+   * @param string $name
+   *   The username.
+   *
+   * @return bool
+   *   Returns TRUE if the user is blocked, otherwise FALSE.
+   */
+  protected function userIsBlocked($name) {
+    return user_is_blocked($name);
+  }
+
+  /**
+   * Finalizes the user login.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user.
+   */
+  protected function userLoginFinalize(UserInterface $user) {
+    user_login_finalize($user);
+  }
+
+  /**
+   * Checks for flooding.
+   *
+   * @return bool
+   *   TRUE if the user is allowed to proceed, FALSE otherwise.
+   */
+  protected function isFloodBlocked() {
+    $config = $this->config('user.flood');
+    $limit = $config->get('user_limit');
+    $interval = $config->get('user_window');
+
+    return $this->flood->isAllowed('rest.login_cookie', $limit, $interval);
+  }
+
+  /**
+   * Logs out a user.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   */
+  public function logout() {
+    $this->userLogout();
+    return new Response(NULL, 204);
+  }
+
+  /**
+   * Logs the user out.
+   */
+  protected function userLogout() {
+    user_logout();
+  }
+
+  /**
+   * Checks whether a user is logged in or not.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response.
+   */
+  public function loginStatus() {
+    if ($this->currentUser()->isAuthenticated()) {
+      $response = new Response(self::LOGGED_IN);
+    }
+    else {
+      $response = new Response(self::LOGGED_OUT);
+    }
+    $response->headers->set('Content-Type', 'text/plain');
+    return $response;
+  }
+
+  /**
+   * Gets the format of the current request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return string
+   *   The format of the request.
+   */
+  protected function getRequestFormat(Request $request) {
+    $format = $request->getRequestFormat();
+    if (!in_array($format, $this->serializerFormats)) {
+      throw new BadRequestHttpException("Unrecognized format: $format.");
+    }
+    return $format;
+  }
+
+}
diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
new file mode 100644
index 0000000..5e5d8a5
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace Drupal\Tests\user\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\user\Controller\UserAuthenticationController;
+use GuzzleHttp\Cookie\CookieJar;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Tests login via direct HTTP.
+ *
+ * @group user
+ */
+class UserLoginHttpTest extends BrowserTestBase {
+
+  /**
+   * The cookie jar.
+   *
+   * @var \GuzzleHttp\Cookie\CookieJar
+   */
+  protected $cookies;
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->cookies = new CookieJar();
+
+    $this->serializer = new Serializer([], [new JsonEncoder()]);
+  }
+
+  /**
+   * Executes a login HTTP request.
+   *
+   * @param string $name
+   *   The username.
+   * @param string $pass
+   *   The user password.
+   * @param string $format
+   *   The format to use to make the request.
+   *
+   * @return \Psr\Http\Message\ResponseInterface The HTTP response.
+   *   The HTTP response.
+   */
+  protected function loginRequest($name, $pass, $format) {
+    $user_login_url = Url::fromRoute('user.login.http')
+      ->setRouteParameter('_format', $format)
+      ->setAbsolute();
+
+    $request_body = [];
+    if (isset($name)) {
+      $request_body['name'] = $name;
+    }
+    if (isset($pass)) {
+      $request_body['pass'] = $pass;
+    }
+
+    $result = \Drupal::httpClient()->post($user_login_url->toString(), [
+      'body' => $this->encode($request_body, $format),
+      'headers' => [
+        'Accept' => "application/$format",
+      ],
+      'http_errors' => FALSE,
+      'cookies' => $this->cookies,
+    ]);
+    return $result;
+  }
+
+  /**
+   * Tests user session life cycle.
+   */
+  public function testLogin() {
+    $account = $this->drupalCreateUser();
+    $name = $account->getUsername();
+    $pass = $account->passRaw;
+
+    $client = \Drupal::httpClient();
+
+    $formats = ['json'];
+    foreach ($formats as $format) {
+
+      $user_login_status_url = Url::fromRoute('user.login_status.http');
+      $user_login_status_url->setRouteParameter('_format', $format);
+      $user_login_status_url->setAbsolute();
+
+      $result = $client->post($user_login_status_url->toString());
+      $this->assertEquals(200, $result->getStatusCode());
+      $this->assertEquals(UserAuthenticationController::LOGGED_OUT, (string) $result->getBody());
+
+      // Flooded.
+      \Drupal::configFactory()->getEditable('user.flood')
+        ->set('user_limit', 3)
+        ->save();
+
+      $result = $this->loginRequest($name, 'wrong-pass', $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Sorry, unrecognized username or password."}', (string) $result->getBody());
+
+      $result = $this->loginRequest($name, 'wrong-pass', $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Sorry, unrecognized username or password."}', (string) $result->getBody());
+
+      $result = $this->loginRequest($name, 'wrong-pass', $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Sorry, unrecognized username or password."}', (string) $result->getBody());
+
+      $result = $this->loginRequest($name, 'wrong-pass', $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Blocked."}', (string) $result->getBody());
+
+      // After testing the flood control we can increase the limit.
+      \Drupal::configFactory()->getEditable('user.flood')
+        ->set('user_limit', 100)
+        ->save();
+
+      $result = $this->loginRequest(NULL, NULL, $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Missing credentials."}', (string) $result->getBody());
+
+      $result = $this->loginRequest(NULL, $pass, $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Missing credentials.name."}', (string) $result->getBody());
+
+      $result = $this->loginRequest($name, NULL, $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Missing credentials.pass."}', (string) $result->getBody());
+
+      // Blocked.
+      $account
+        ->block()
+        ->save();
+
+      $result = $this->loginRequest($name, $pass, $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"The user has not been activated or is blocked."}', (string) $result->getBody());
+
+      $account
+        ->activate()
+        ->save();
+
+      $result = $this->loginRequest($name, 'garbage', $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Sorry, unrecognized username or password."}', (string) $result->getBody());
+
+      $result = $this->loginRequest('garbage', $pass, $format);
+      $this->assertEquals(400, $result->getStatusCode());
+      $this->assertEquals('{"message":"Sorry, unrecognized username or password."}', (string) $result->getBody());
+
+      $result = $this->loginRequest($name, $pass, $format);
+      $this->assertEquals(200, $result->getStatusCode());
+      $result_data = $this->decode($result->getBody(), $format);
+      $this->assertEquals($name, $result_data['current_user']['name']);
+
+      $result = $client->post($user_login_status_url->toString(), ['cookies' => $this->cookies]);
+      $this->assertEquals(200, $result->getStatusCode());
+      $this->assertEquals(UserAuthenticationController::LOGGED_IN, (string) $result->getBody());
+
+      $user_logout_url = Url::fromRoute('user.logout.http')->setRouteParameter('_format', $format)->setAbsolute();
+      $result = $client->post($user_logout_url->toString(), [
+        'headers' => [
+          'Accept' => "application/$format",
+        ],
+        'http_errors' => FALSE,
+        'cookies' => $this->cookies,
+      ]);
+      $this->assertEquals(204, $result->getStatusCode());
+
+      $result = $client->post($user_login_status_url->toString(), ['cookies' => $this->cookies]);
+      $this->assertEquals(200, $result->getStatusCode());
+      $this->assertEquals(UserAuthenticationController::LOGGED_OUT, (string) $result->getBody());
+    }
+
+  }
+
+  /**
+   * Encodes data for a request into a given format.
+   *
+   * @param mixed $data
+   *   The data to be encoded.
+   * @param string $format
+   *   The format to be encoded into.
+   */
+  protected function encode($data, $format) {
+    return $this->serializer->encode($data, $format);
+  }
+
+  /**
+   * Decodes data for a request from a given format.
+   *
+   * @param mixed $data
+   *   The data to be encoded.
+   * @param string $format
+   *   The format to be encoded into.
+   */
+  protected function decode($data, $format) {
+    return $this->serializer->decode($data, $format);
+  }
+
+}
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 6eea7ec..09ac772 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -129,6 +129,33 @@ user.login:
   options:
     _maintenance_access: TRUE
 
+user.login.http:
+  path: '/user/login'
+  defaults:
+    _controller: \Drupal\user\Controller\UserAuthenticationController::login
+  methods: [POST]
+  requirements:
+    _user_is_logged_in: 'FALSE'
+    _format: 'json'
+
+user.login_status.http:
+  path: '/user/login_status'
+  defaults:
+    _controller: \Drupal\user\Controller\UserAuthenticationController::loginStatus
+  methods: [POST]
+  requirements:
+    _access: 'TRUE'
+    _format: 'json'
+
+user.logout.http:
+  path: '/user/logout'
+  defaults:
+    _controller: \Drupal\user\Controller\UserAuthenticationController::logout
+  methods: [POST]
+  requirements:
+    _user_is_logged_in: 'TRUE'
+    _format: 'json'
+
 user.cancel_confirm:
   path: '/user/{user}/cancel/confirm/{timestamp}/{hashed_pass}'
   defaults:
