diff --git a/core/modules/rest/src/Plugin/ResourceBase.php b/core/modules/rest/src/Plugin/ResourceBase.php
index 5f8065e..2038fa9 100644
--- a/core/modules/rest/src/Plugin/ResourceBase.php
+++ b/core/modules/rest/src/Plugin/ResourceBase.php
@@ -8,6 +8,7 @@
 namespace Drupal\rest\Plugin;
 
 use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Plugin\PluginBase;
 use Psr\Log\LoggerInterface;
@@ -42,6 +43,11 @@
   protected $logger;
 
   /**
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $flood;
+
+  /**
    * Constructs a Drupal\rest\Plugin\ResourceBase object.
    *
    * @param array $configuration
@@ -54,11 +60,14 @@
    *   The available serialization formats.
    * @param \Psr\Log\LoggerInterface $logger
    *   A logger instance.
+   * @param \Drupal\Core\Flood\FloodInterface $flood
+   *   The flood control mechanism.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger) {
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, FloodInterface $flood) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
     $this->serializerFormats = $serializer_formats;
     $this->logger = $logger;
+    $this->flood = $flood;
   }
 
   /**
@@ -70,7 +79,8 @@ public static function create(ContainerInterface $container, array $configuratio
       $plugin_id,
       $plugin_definition,
       $container->getParameter('serializer.formats'),
-      $container->get('logger.factory')->get('rest')
+      $container->get('logger.factory')->get('rest'),
+      $container->get('flood')
     );
   }
 
@@ -214,4 +224,21 @@ protected function getBaseRoute($canonical_path, $method) {
     return $route;
   }
 
+  /**
+   * Checks for flooding.
+   *
+   * @param \Drupal\Core\Config\ImmutableConfig $config
+   * @param $name
+   * @return bool
+   */
+  protected function restFloodControl($config, $name) {
+    $limit = $config->get('user_limit');
+    $interval = $config->get('user_window');
+    if (!$this->flood->isAllowed($name, $limit, $interval)) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+
 }
diff --git a/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php
new file mode 100644
index 0000000..49ef308
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/UserLoginResource.php
@@ -0,0 +1,202 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\rest\Plugin\rest\resource\UserLoginResource.
+ */
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Flood\FloodInterface;
+use Drupal\rest\ResourceResponse;
+use Drupal\rest\Plugin\ResourceBase;
+use Drupal\user\Entity\User;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Allows user logins by setting session cookies.
+ *
+ * @RestResource(
+ *   id = "user_login",
+ *   label = @Translation("User Login")
+ * )
+ */
+class UserLoginResource extends ResourceBase {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Constructs a new RestPermissions instance.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param array $serializer_formats
+   *   The available serialization formats.
+   * @param LoggerInterface $loggery
+   *   A logger instance.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $loggery, ConfigFactoryInterface $config_factory, FloodInterface $flood) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $loggery, $flood);
+    $this->configFactory = $config_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->getParameter('serializer.formats'),
+      $container->get('logger.factory')->get('rest'),
+      $container->get('config.factory'),
+      $container->get('flood')
+    );
+  }
+
+  /**
+   * Responds to the user login POST requests and log in a user.
+   *
+   * @param string[] $operation
+   *   array(
+   *     'op' => 'login', 'logout'
+   *     'credentials' => array(
+   *       'name' => 'your-name',
+   *       'pass' => 'your-password',
+   *     ),
+   *   )
+   *
+   *   The operation and username + pass for the login op.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   *   The HTTP response object.
+   */
+  public function post(array $operation = array()) {
+
+    if (array_key_exists('op', $operation)) {
+      switch ($operation['op']) {
+
+        case 'login':
+          if (!array_key_exists('credentials', $operation)) {
+            $operation['credentials'] = array();
+          }
+          return $this->login($operation['credentials']);
+
+        case 'status':
+          return $this->status();
+
+        case 'logout':
+          return $this->logout();
+
+        default:
+          // TODO: do we have to escape?
+          throw new BadRequestHttpException('Unsupported op '. Html::escape($operation['op']) . '.');
+
+      }
+    }
+    else {
+      throw new BadRequestHttpException('No op found.');
+    }
+  }
+
+  /**
+   * User login.
+   *
+   * @param array $credentials
+   *   The username and pass for the user.
+   *
+   * @return \Drupal\rest\ResourceResponse
+   *   The HTTP response object
+   */
+  protected function login(array $credentials = array()) {
+    if ($this->userIsAuthenticated()) {
+      // TODO we need this
+      //throw new BadRequestHttpException('You need to logout first.');
+    }
+
+    if (empty($credentials)) {
+      throw new BadRequestHttpException('Missing credentials.');
+    }
+
+    // Verify that the username is filled.
+    if (!array_key_exists('name', $credentials)) {
+      throw new BadRequestHttpException('Missing credentials.name.');
+    }
+    // Verify that the password is filled.
+    if (!array_key_exists('pass', $credentials)) {
+      throw new BadRequestHttpException('Missing credentials.pass.');
+    }
+
+    // Flood control.
+    if ($this->restFloodControl($this->configFactory->get('user.flood'), 'rest.login_cookie')) {
+      throw new BadRequestHttpException('Blocked.');
+    }
+
+    // Verify that the user is not blocked.
+    if ($this->userIsBlocked($credentials['name'])) {
+      throw new BadRequestHttpException('The user has not been activated or is blocked.');
+    }
+
+    // Log in the user.
+    if ($uid = \Drupal::service('user.auth')->authenticate($credentials['name'], $credentials['pass'])) {
+      $user = User::load($uid);
+      user_login_finalize($user);
+      return new ResourceResponse('You are logged in as ' . $credentials['name'] . '.', 200, array());
+    }
+
+    $this->flood->register('rest.login_cookie', $this->configFactory->get('user.flood')->get('user_window'));
+    throw new BadRequestHttpException('Sorry, unrecognized username or password.');
+  }
+
+  protected function userIsAuthenticated() {
+    return \Drupal::currentUser()->isAuthenticated();
+
+  }
+  protected function status() {
+    if (\Drupal::currentUser()->isAuthenticated()) {
+      return new ResourceResponse('You are logged in.', 200, array());
+    }
+    return new ResourceResponse('You are not logged in.', 200, array());
+  }
+
+    /**
+   * User Logout.
+   *
+   * @return ResourceResponse
+   */
+  protected function logout() {
+    if (!\Drupal::currentUser()->isAuthenticated()) {
+      //throw new BadRequestHttpException('You cannot logout as you are not logged in.');
+    }
+
+    user_logout();
+    return new ResourceResponse('You are logged out.', 200, array());
+  }
+
+  /**
+   * Verifies if the user is blocked.
+   *
+   * @param string $name
+   * @return bool
+   */
+  protected function userIsBlocked($name) {
+    return user_is_blocked($name);
+  }
+
+}
diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php
index ee4b890..801e0cd 100644
--- a/core/modules/rest/src/RequestHandler.php
+++ b/core/modules/rest/src/RequestHandler.php
@@ -60,9 +60,15 @@ public function handle(RouteMatchInterface $route_match, Request $request) {
       $method_settings = $config[$plugin][$request->getMethod()];
       if (empty($method_settings['supported_formats']) || in_array($format, $method_settings['supported_formats'])) {
         $definition = $resource->getPluginDefinition();
-        $class = $definition['serialization_class'];
+        $class = isset($definition['serialization_class']) ? $definition['serialization_class'] : NULL;
         try {
-          $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
+          if ($class) {
+            $unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
+          }
+          // Avoid denormalization because we need to instantiate a class.
+          else {
+            $unserialized = $serializer->decode($received, $format, array('request_method' => $method));
+          }
         }
         catch (UnexpectedValueException $e) {
           $error['error'] = $e->getMessage();
diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php
index 5e84827..a2da3a7 100644
--- a/core/modules/rest/src/Tests/RESTTestBase.php
+++ b/core/modules/rest/src/Tests/RESTTestBase.php
@@ -83,7 +83,7 @@ protected function setUp() {
    * @return string
    *   The content returned from the request.
    */
-  protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
+  protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $request_headers = []) {
     if (!isset($mime_type)) {
       $mime_type = $this->defaultMimeType;
     }
@@ -113,10 +113,11 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
           CURLOPT_POSTFIELDS => $body,
           CURLOPT_URL => $url,
           CURLOPT_NOBODY => FALSE,
-          CURLOPT_HTTPHEADER => array(
+          CURLOPT_HTTPHEADER => array_merge(
+            array(
             'Content-Type: ' . $mime_type,
             'X-CSRF-Token: ' . $token,
-          ),
+          ), $request_headers),
         );
         break;
 
@@ -168,6 +169,9 @@ protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
 
     $this->verbose($method . ' request to: ' . $url .
       '<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
+      '<hr />Request headers: ' . nl2br(print_r($curl_options[CURLOPT_HTTPHEADER], TRUE)) .
+      '<hr />Extra headers: ' . nl2br(print_r($request_headers, TRUE)) .
+      '<hr />Request body: ' . nl2br(print_r($body, TRUE)) .
       '<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
       '<hr />Response body: ' . $this->responseBody);
 
diff --git a/core/modules/rest/src/Tests/UserTest.php b/core/modules/rest/src/Tests/UserTest.php
new file mode 100644
index 0000000..66505e6
--- /dev/null
+++ b/core/modules/rest/src/Tests/UserTest.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\rest\test\AuthTest.
+ */
+
+namespace Drupal\rest\Tests;
+
+use Drupal\rest\Tests\RESTTestBase;
+
+/**
+ * Tests REST user login.
+ *
+ * @group rest
+ */
+class UserTest extends RESTTestBase {
+
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = array('basic_auth', 'hal', 'rest');
+
+  /**
+   * Test user session life cycle.
+   */
+  public function testLogin() {
+    $this->defaultAuth = array('basic_auth');
+
+    $this->enableService('user_login', 'POST');
+
+    $permissions[] = 'restful post user_login';
+    $account = $this->drupalCreateUser($permissions);
+
+    $name = $account->getUsername();
+    $pass = $account->pass_raw;
+
+    $basic_auth = ['Authorization: Basic ' . base64_encode("$name:$pass")];
+
+    $payload = array();
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'No op found.');
+
+    $payload = $this->getPayload('garbage');
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'Unsupported op garbage.');
+
+    $payload = $this->getPayload('status');
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(200, 'You are logged in.');
+
+    $payload = $this->getPayload('logout');
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(200, 'You are logged out.', $basic_auth);
+
+    $payload = $this->getPayload('login');
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'Missing credentials.');
+
+    $payload = $this->getPayload('login', $name);
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'Missing credentials.pass.');
+
+    $payload = $this->getPayload('login', NULL, $pass);
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'Missing credentials.name.');
+
+    $payload = $this->getPayload('login', $name, 'garbage');
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'Sorry, unrecognized username or password.');
+
+    $payload = $this->getPayload('login', 'garbage', $pass);
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(400, 'Sorry, unrecognized username or password.');
+
+    $payload = $this->getPayload('login', $name, $pass);
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(200, "You are logged in as $name");
+
+    $payload = $this->getPayload('status');
+    $this->httpRequest('user_login', 'POST', json_encode($payload), $this->defaultMimeType, $basic_auth);
+    $this->assertResponseAndText(200, 'You are logged in.');
+  }
+
+  protected function assertResponseAndText($code, $text) {
+    $this->assertResponse($code);
+    $this->assertText($text);
+  }
+
+  /**
+   * Helper function to build the payload.
+   *
+   * @param string $op
+   * @param string|null $user
+   * @param string|null $pass
+   * @return array
+   *
+   * @see UserLoginResource.php
+   */
+  private function getPayload( $op, $name = NULL, $pass = NULL) {
+    $result = array('op' => $op);
+
+    if ($op == 'login') {
+      $result['credentials'] = array();
+      if (isset($name)) {
+        $result['credentials']['name'] = $name;
+      }
+      if (isset($pass)) {
+        $result['credentials']['pass'] = $pass;
+      }
+    }
+    return $result;
+  }
+}
diff --git a/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php
new file mode 100644
index 0000000..3bd3786
--- /dev/null
+++ b/core/modules/rest/tests/src/Unit/UserLoginResourceTest.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\rest\Unit\UserLoginResourceTest.
+ */
+
+namespace Drupal\Tests\rest\Unit;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\rest\Plugin\rest\resource\UserLoginResource;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the Login Resource.
+ * @TODO Test flood control after https://www.drupal.org/node/2431357 has landed.
+ * @group rest
+ */
+class UserLoginResourceTest extends UnitTestCase {
+
+  protected $testClass;
+  protected $flood;
+  protected $logger;
+  protected $reflection;
+  protected $config;
+  protected $testClassMock;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $user_auth_service = $this->getMock('Drupal\user\UserAuthInterface');
+    $user_auth_service->expects($this->any())
+      ->method('authenticate')
+      ->will($this->returnValue(FALSE));
+
+    $container = new ContainerBuilder();
+    $container->set('user.auth', $user_auth_service);
+    \Drupal::setContainer($container);
+
+    $this->flood = $this->getMock('\Drupal\Core\Flood\FloodInterface');
+
+    $this->config = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory')
+      ->disableOriginalConstructor()
+      ->setMethods(['get'])
+      ->getMock();
+
+    $immutableConfig = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory')
+      ->disableOriginalConstructor()
+      ->setMethods(['get'])
+      ->getMock();
+
+    $this->config->expects($this->any())
+      ->method('get')
+      ->will($this->returnValue($immutableConfig));
+
+    $this->logger = $this->getMock('Psr\Log\LoggerInterface');
+
+    $this->testClass = new UserLoginResource([], 'plugin_id', '', [], $this->logger, $this->config,  $this->flood);
+
+    $this->testClassMock = $this->getMockBuilder('\Drupal\rest\Plugin\rest\resource\UserLoginResource')
+      ->setMethods(['restFloodControl', 'login', 'logout', 'post', 'userIsBlocked'])
+      ->setConstructorArgs([[], 'plugin_id', '', [], $this->logger, $this->config,  $this->flood])
+      ->getMock();
+
+    $this->reflection = new \ReflectionClass($this->testClass);
+  }
+
+  /**
+   * Gets a protected method from current class using reflection.
+   *
+   * @param $method
+   * @return mixed
+   */
+  public function getProtectedMethod($method) {
+    $method = $this->reflection->getMethod($method);
+    $method->setAccessible(TRUE);
+
+    return $method;
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage Missing credentials.
+   */
+  public function testMissingCredentials() {
+    $this->testClass->post(['op'=>'login']);
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage Unsupported op.
+   */
+  public function testUnsupportedOp() {
+    $this->testClass->post(['op'=>'UnsuportedOp']);
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage Missing credentials.name.
+   */
+  public function testLoginMissingCredentialName() {
+    $method = $this->getProtectedMethod('login');
+    $method->invokeArgs($this->testClass, ['credentials' => []]);
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage Missing credentials.pass.
+   */
+  public function testLoginMissingCredentialPass() {
+    $method = $this->getProtectedMethod('login');
+    $method->invokeArgs($this->testClass, ['credentials' => ['name' => 'Druplicon']]);
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage Blocked.
+   */
+  public function testLoginBlockedUserByFloodControl() {
+    $this->testClassMock->expects($this->once())
+      ->method('restFloodControl')
+      ->will($this->returnValue(TRUE));
+
+    $method = $this->getProtectedMethod('login');
+    $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]);
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage The user has not been activated or is blocked.
+   */
+  public function testLoginBlockedUser() {
+    $this->testClassMock->expects($this->once())
+      ->method('restFloodControl')
+      ->will($this->returnValue(FALSE));
+
+    $this->testClassMock->expects($this->once())
+      ->method('userIsBlocked')
+      ->will($this->returnValue(TRUE));
+
+    $method = $this->getProtectedMethod('login');
+    $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]);
+  }
+
+  /**
+   * @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+   * @expectedExceptionMessage Sorry, unrecognized username or password.
+   */
+  public function testLoginUnrecognizedUsernameOrPassword() {
+    $this->testClassMock->expects($this->once())
+      ->method('restFloodControl')
+      ->will($this->returnValue(FALSE));
+
+    $this->testClassMock->expects($this->once())
+      ->method('userIsBlocked')
+      ->will($this->returnValue(FALSE));
+
+    $method = $this->getProtectedMethod('login');
+    $method->invokeArgs($this->testClassMock, ['credentials' => ['name' => 'Druplicon', 'pass' => 'SuperSecret']]);
+  }
+}
+
