diff --git a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php
index 2d5d78f..147b593 100644
--- a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php
+++ b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php
@@ -47,6 +47,7 @@ public function onRoutingAlterAddFormats(RouteBuildEvent $event) {
       'user.login_status.http',
       'user.login.http',
       'user.logout.http',
+      'user.pass.http',
     ];
     $routes = $event->getRouteCollection();
     foreach ($route_names as $route_name) {
diff --git a/core/modules/user/src/Controller/UserAuthenticationController.php b/core/modules/user/src/Controller/UserAuthenticationController.php
index 3aff75f..52027f6 100644
--- a/core/modules/user/src/Controller/UserAuthenticationController.php
+++ b/core/modules/user/src/Controller/UserAuthenticationController.php
@@ -10,6 +10,7 @@
 use Drupal\user\UserAuthInterface;
 use Drupal\user\UserInterface;
 use Drupal\user\UserStorageInterface;
+use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -87,6 +88,13 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
   protected $serializerFormats = [];
 
   /**
+   * A logger instance.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
    * Constructs a new UserAuthenticationController object.
    *
    * @param \Drupal\Core\Flood\FloodInterface $flood
@@ -103,8 +111,10 @@ class UserAuthenticationController extends ControllerBase implements ContainerIn
    *   The serializer.
    * @param array $serializer_formats
    *   The available serialization formats.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   A logger instance.
    */
-  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats) {
+  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, CsrfTokenGenerator $csrf_token, UserAuthInterface $user_auth, RouteProviderInterface $route_provider, Serializer $serializer, array $serializer_formats, LoggerInterface $logger) {
     $this->flood = $flood;
     $this->userStorage = $user_storage;
     $this->csrfToken = $csrf_token;
@@ -112,6 +122,7 @@ public function __construct(FloodInterface $flood, UserStorageInterface $user_st
     $this->serializer = $serializer;
     $this->serializerFormats = $serializer_formats;
     $this->routeProvider = $route_provider;
+    $this->logger = $logger;
   }
 
   /**
@@ -135,7 +146,8 @@ public static function create(ContainerInterface $container) {
       $container->get('user.auth'),
       $container->get('router.route_provider'),
       $serializer,
-      $formats
+      $formats,
+      $container->get('logger.factory')->get('user')
     );
   }
 
@@ -208,6 +220,64 @@ public function login(Request $request) {
   }
 
   /**
+   * Resets a user password.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response object.
+   */
+  public function resetPassword(Request $request) {
+    $format = $this->getRequestFormat($request);
+
+    $content = $request->getContent();
+    $credentials = $this->serializer->decode($content, $format);
+
+    // Check if a name or mail is provided.
+    if (!isset($credentials['name']) && !isset($credentials['mail'])) {
+      throw new BadRequestHttpException('Missing credentials.name or credential.mail');
+    }
+
+    $name_or_mail = isset($credentials['name']) ? $credentials['name'] : $credentials['mail'];
+
+    $users = $this->userStorage->loadByProperties(['name' => trim($name_or_mail)]);
+    if (!count($users) && isset($credentials['mail'])) {
+      $users = $this->userStorage->loadByProperties(['mail' => trim($name_or_mail)]);
+    }
+
+    $account = reset($users);
+    if ($account && $account->id()) {
+
+      // Check if user is blocked.
+      if ($this->userIsBlocked($account->getAccountName())) {
+        throw new BadRequestHttpException('The user is blocked or has not been activated yet.');
+      }
+
+      // Send the password reset email.
+      $mail = _user_mail_notify('password_reset', $account);
+
+      if (empty($mail)) {
+        throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
+      }
+
+      // Log a message.
+      $this->logger->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
+
+      // Respond with message.
+      $message = $this->t('Further instructions have been sent to the provided email address.');
+      $response_data = [
+        'message' => $message->render(),
+      ];
+      $encoded_response_data = $this->serializer->encode($response_data, $format);
+      return new Response($encoded_response_data);
+    }
+
+    // Error if no users found with provided name or mail.
+    throw new BadRequestHttpException('Sorry, unrecognized username or email address.');
+  }
+
+  /**
    * Verifies if the user is blocked.
    *
    * @param string $name
diff --git a/core/modules/user/tests/src/Functional/UserPasswordHttpTest.php b/core/modules/user/tests/src/Functional/UserPasswordHttpTest.php
new file mode 100644
index 0000000..5c2ab51
--- /dev/null
+++ b/core/modules/user/tests/src/Functional/UserPasswordHttpTest.php
@@ -0,0 +1,203 @@
+<?php
+
+namespace Drupal\Tests\user\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use GuzzleHttp\Cookie\CookieJar;
+use Psr\Http\Message\ResponseInterface;
+use Symfony\Component\Serializer\Encoder\JsonEncoder;
+use Symfony\Component\Serializer\Encoder\XmlEncoder;
+use Drupal\hal\Encoder\JsonEncoder as HALJsonEncoder;
+use Symfony\Component\Serializer\Serializer;
+
+/**
+ * Tests password reset via HTTP.
+ *
+ * @group user
+ */
+class UserPasswordHttpTest extends BrowserTestBase {
+  /**
+   * Modules to install.
+   *
+   * @var array
+   */
+  public static $modules = ['hal'];
+
+  /**
+   * The cookie jar.
+   *
+   * @var \GuzzleHttp\Cookie\CookieJar
+   */
+  protected $cookies;
+
+  /**
+   * The serializer.
+   *
+   * @var \Symfony\Component\Serializer\Serializer
+   */
+  protected $serializer;
+
+  /**
+   * The user account.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $account;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->cookies = new CookieJar();
+    $encoders = [new JsonEncoder(), new XmlEncoder(), new HALJsonEncoder()];
+    $this->serializer = new Serializer([], $encoders);
+
+    // Create a user account.
+    $this->account = $this->drupalCreateUser();
+  }
+
+  /**
+   * Executes a password HTTP request.
+   *
+   * @param array $request_body
+   *   The request body.
+   * @param string $format
+   *   The format to use to make the request.
+   *
+   * @return \Psr\Http\Message\ResponseInterface
+   *   The HTTP response.
+   */
+  protected function passwordRequest(array $request_body, $format = 'json') {
+    $password_reset_url = Url::fromRoute('user.pass.http')
+      ->setRouteParameter('_format', $format)
+      ->setAbsolute();
+
+    $result = \Drupal::httpClient()->post($password_reset_url->toString(), [
+      'body' => $this->serializer->encode($request_body, $format),
+      'headers' => [
+        'Accept' => "application/$format",
+      ],
+      'http_errors' => FALSE,
+      'cookies' => $this->cookies,
+    ]);
+
+    return $result;
+  }
+
+  /**
+   * Tests user password reset.
+   */
+  public function testPasswordReset() {
+    foreach ([FALSE, TRUE] as $serialization_enabled_option) {
+      if ($serialization_enabled_option) {
+        /** @var \Drupal\Core\Extension\ModuleInstaller $module_installer */
+        $module_installer = $this->container->get('module_installer');
+        $module_installer->install(['serialization']);
+        $formats = ['json', 'xml', 'hal_json'];
+      }
+      else {
+        // Without the serialization module only JSON is supported.
+        $formats = ['json'];
+      }
+
+      foreach ($formats as $format) {
+
+        // 400 Bad Request for empty body.
+        $response = $this->passwordRequest([], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.name or credential.mail', $format);
+
+        // 400 Bad Request for unrecognized username.
+        $response = $this->passwordRequest(['name' => 'dramallama'], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or email address.', $format);
+
+        // 400 Bad Request for unrecognized email.
+        $response = $this->passwordRequest(['mail' => 'llama@drupal.org'], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'Sorry, unrecognized username or email address.', $format);
+
+        // Block the account.
+        $this->account
+          ->block()
+          ->save();
+
+        // 400 Bad Request for blocked user with username.
+        $response = $this->passwordRequest(['name' => $this->account->getAccountName()], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'The user is blocked or has not been activated yet.', $format);
+
+        // 400 Bad Request for blocked user with mail.
+        $response = $this->passwordRequest(['mail' => $this->account->getEmail()], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'The user is blocked or has not been activated yet.', $format);
+
+        // Activate account.
+        $this->account
+          ->activate()
+          ->save();
+
+        // 200 for correct username.
+        $response = $this->passwordRequest(['name' => $this->account->getAccountName()], $format);
+        $this->assertHttpResponseWithMessage($response, 200, 'Further instructions have been sent to the provided email address.', $format);
+
+        // 200 for correct email.
+        $response = $this->passwordRequest(['mail' => $this->account->getEmail()], $format);
+        $this->assertHttpResponseWithMessage($response, 200, 'Further instructions have been sent to the provided email address.', $format);
+      }
+    }
+  }
+
+  /**
+   * Gets a value for a given key from the response.
+   *
+   * @param \Psr\Http\Message\ResponseInterface $response
+   *   The response object.
+   * @param string $key
+   *   The key for the value.
+   * @param string $format
+   *   The encoded format.
+   *
+   * @return mixed
+   *   The value for the key.
+   */
+  protected function getResultValue(ResponseInterface $response, $key, $format) {
+    $decoded = $this->serializer->decode((string) $response->getBody(), $format);
+    if (is_array($decoded)) {
+      return $decoded[$key];
+    }
+    else {
+      return $decoded->{$key};
+    }
+  }
+
+  /**
+   * Checks a response for status code and body.
+   *
+   * @param \Psr\Http\Message\ResponseInterface $response
+   *   The response object.
+   * @param int $expected_code
+   *   The expected status code.
+   * @param mixed $expected_body
+   *   The expected response body.
+   */
+  protected function assertHttpResponse(ResponseInterface $response, $expected_code, $expected_body) {
+    $this->assertEquals($expected_code, $response->getStatusCode());
+    $this->assertEquals($expected_body, (string) $response->getBody());
+  }
+
+  /**
+   * Checks a response for status code and message.
+   *
+   * @param \Psr\Http\Message\ResponseInterface $response
+   *   The response object.
+   * @param int $expected_code
+   *   The expected status code.
+   * @param string $expected_message
+   *   The expected message encoded in response.
+   * @param string $format
+   *   The format that the response is encoded in.
+   */
+  protected function assertHttpResponseWithMessage(ResponseInterface $response, $expected_code, $expected_message, $format = 'json') {
+    $this->assertEquals($expected_code, $response->getStatusCode());
+    $this->assertEquals($expected_message, $this->getResultValue($response, 'message', $format));
+  }
+
+}
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 319f219..e38bb7a 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -111,6 +111,15 @@ user.pass:
   options:
     _maintenance_access: TRUE
 
+user.pass.http:
+  path: '/user/password'
+  defaults:
+    _controller: \Drupal\user\Controller\UserAuthenticationController::resetPassword
+  methods: [POST]
+  requirements:
+    _access: 'TRUE'
+    _format: 'json'
+
 user.page:
   path: '/user'
   defaults:
