diff --git a/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php b/core/modules/serialization/src/EventSubscriber/UserRouteAlterSubscriber.php
index 2d5d78fbf7..147b593aa1 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 3aff75ff57..f45ec3a7ac 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,56 @@ 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 credentials.mail');
+    }
+
+    // Load by name if provided.
+    if (isset($credentials['name'])) {
+      $users = $this->userStorage->loadByProperties(['name' => trim($credentials['name'])]);
+    }
+    elseif (isset($credentials['mail'])) {
+      $users = $this->userStorage->loadByProperties(['mail' => trim($credentials['mail'])]);
+    }
+
+    /** @var \Drupal\Core\Session\AccountInterface $account */
+    $account = reset($users);
+    if ($account && $account->id()) {
+      if ($this->userIsBlocked($account->getAccountName())) {
+        throw new BadRequestHttpException('The user has not been activated or is blocked.');
+      }
+
+      // Send the password reset email.
+      $mail = _user_mail_notify('password_reset', $account, $account->getPreferredLangcode());
+      if (empty($mail)) {
+        throw new BadRequestHttpException('Unable to send email. Contact the site administrator if the problem persists.');
+      }
+      else {
+        $this->logger->notice('Password reset instructions mailed to %name at %email.', ['%name' => $account->getAccountName(), '%email' => $account->getEmail()]);
+        return new Response();
+      }
+    }
+
+    // Error if no users found with provided name or mail.
+    throw new BadRequestHttpException('Unrecognized username or email address.');
+  }
+
+  /**
    * Verifies if the user is blocked.
    *
    * @param string $name
diff --git a/core/modules/user/src/Tests/UserLoginTest.php b/core/modules/user/src/Tests/UserLoginTest.php
index a83e0c62b8..02f8fde2a6 100644
--- a/core/modules/user/src/Tests/UserLoginTest.php
+++ b/core/modules/user/src/Tests/UserLoginTest.php
@@ -12,6 +12,8 @@
  */
 class UserLoginTest extends WebTestBase {
 
+  use UserResetEmailTestTrait;
+
   /**
    * Tests login with destination.
    */
@@ -198,13 +200,7 @@ public function resetUserPassword($user) {
     $this->drupalGet('user/password');
     $edit['name'] = $user->getUsername();
     $this->drupalPostForm(NULL, $edit, 'Submit');
-    $_emails = $this->drupalGetMails();
-    $email = end($_emails);
-    $urls = [];
-    preg_match('#.+user/reset/.+#', $email['body'], $urls);
-    $resetURL = $urls[0];
-    $this->drupalGet($resetURL);
-    $this->drupalPostForm(NULL, NULL, 'Log in');
+    $this->loginFromResetEmail();
   }
 
 }
diff --git a/core/modules/user/src/Tests/UserResetEmailTestTrait.php b/core/modules/user/src/Tests/UserResetEmailTestTrait.php
new file mode 100644
index 0000000000..2fbbbbf78d
--- /dev/null
+++ b/core/modules/user/src/Tests/UserResetEmailTestTrait.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Drupal\user\Tests;
+
+use Drupal\Core\Test\AssertMailTrait;
+
+/**
+ * Helper function for logging in from reset password email.
+ */
+trait UserResetEmailTestTrait {
+
+  use AssertMailTrait {
+    getMails as drupalGetMails;
+  }
+
+  /**
+   * Login from reset password email.
+   */
+  protected function loginFromResetEmail() {
+    $_emails = $this->drupalGetMails();
+    $email = end($_emails);
+    $urls = [];
+    preg_match('#.+user/reset/.+#', $email['body'], $urls);
+    $resetURL = $urls[0];
+    $this->drupalGet($resetURL);
+    $this->drupalPostForm(NULL, NULL, 'Log in');
+  }
+
+}
diff --git a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
index 29b97277c0..fc50f62277 100644
--- a/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
+++ b/core/modules/user/tests/src/Functional/UserLoginHttpTest.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Url;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\user\Controller\UserAuthenticationController;
+use Drupal\user\Tests\UserResetEmailTestTrait;
 use GuzzleHttp\Cookie\CookieJar;
 use Psr\Http\Message\ResponseInterface;
 use Symfony\Component\Serializer\Encoder\JsonEncoder;
@@ -14,12 +15,14 @@
 use Symfony\Component\Serializer\Serializer;
 
 /**
- * Tests login via direct HTTP.
+ * Tests login and password reset via direct HTTP.
  *
  * @group user
  */
 class UserLoginHttpTest extends BrowserTestBase {
 
+  use UserResetEmailTestTrait;
+
   /**
    * Modules to install.
    *
@@ -188,6 +191,90 @@ public function testLogin() {
   }
 
   /**
+   * 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() {
+    // Create a user account.
+    $account = $this->drupalCreateUser();
+
+    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) {
+        $response = $this->passwordRequest([], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'Missing credentials.name or credentials.mail', $format);
+
+        $response = $this->passwordRequest(['name' => 'dramallama'], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'Unrecognized username or email address.', $format);
+
+        $response = $this->passwordRequest(['mail' => 'llama@drupal.org'], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'Unrecognized username or email address.', $format);
+
+        $account
+          ->block()
+          ->save();
+
+        $response = $this->passwordRequest(['name' => $account->getAccountName()], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked.', $format);
+
+        $response = $this->passwordRequest(['mail' => $account->getEmail()], $format);
+        $this->assertHttpResponseWithMessage($response, 400, 'The user has not been activated or is blocked.', $format);
+
+        $account
+          ->activate()
+          ->save();
+
+        $response = $this->passwordRequest(['name' => $account->getAccountName()], $format);
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->loginFromResetEmail();
+        $this->drupalLogout();
+
+        $response = $this->passwordRequest(['mail' => $account->getEmail()], $format);
+        $this->assertEquals(200, $response->getStatusCode());
+        $this->loginFromResetEmail();
+        $this->drupalLogout();
+      }
+    }
+  }
+
+  /**
    * Gets a value for a given key from the response.
    *
    * @param \Psr\Http\Message\ResponseInterface $response
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 319f219e8c..e38bb7acad 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:
