diff --git a/core/modules/user/config/install/user.mail.yml b/core/modules/user/config/install/user.mail.yml
index 382de7a59c0..a1faafb7a49 100644
--- a/core/modules/user/config/install/user.mail.yml
+++ b/core/modules/user/config/install/user.mail.yml
@@ -1,4 +1,32 @@
 langcode: en
+register_password_set:
+  subject: 'Account details for [user:display-name] at [site:name]'
+  body: |-
+    [user:display-name],
+
+    Thank you for registering at [site:name]. You may now log in and verify your account by clicking this link or copying and pasting it to your browser:
+
+    [user:one-time-login-validate-url]
+
+    This link can only be used once. You will be able to log in at [site:login-url] in the future using:
+
+    username: [user:name]
+    password: Your password
+
+    --  [site:name] team
+register_password_set_activation:
+  subject: 'Account details for [user:display-name] at [site:name] (approved)'
+  body: |-
+    [user:display-name],
+
+    Your account at [site:name] has been activated.
+
+    You will be able to log in to [site:login-url] in the future using:
+
+    username: [user:name]
+    password: your password.
+
+    --  [site:name] team
 cancel_confirm:
   subject: 'Account cancellation request for [user:display-name] at [site:name]'
   body: |-
diff --git a/core/modules/user/config/install/user.settings.yml b/core/modules/user/config/install/user.settings.yml
index 3232949963b..425cec60e97 100644
--- a/core/modules/user/config/install/user.settings.yml
+++ b/core/modules/user/config/install/user.settings.yml
@@ -10,6 +10,8 @@ notify:
   register_admin_created: true
   register_no_approval_required: true
   register_pending_approval: true
+  register_password_set: false
+  register_password_set_activation: false
 register: visitors
 cancel_method: user_cancel_block
 password_reset_timeout: 86400
diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml
index 54990b28497..2d7066edb91 100644
--- a/core/modules/user/config/schema/user.schema.yml
+++ b/core/modules/user/config/schema/user.schema.yml
@@ -12,6 +12,9 @@ user.settings:
     verify_mail:
       type: boolean
       label: 'Require email verification when a visitor creates an account'
+    register_password_set:
+      type: boolean
+      label: 'Require people to choose a password during registration.'
     notify:
       type: mapping
       label: 'Notify user'
@@ -40,6 +43,12 @@ user.settings:
         register_pending_approval:
           type: boolean
           label: 'Welcome (awaiting approval)'
+        register_password_set:
+          type: boolean
+          label: 'Password set during registration'
+        register_password_set_activation:
+          type: boolean
+          label: 'Password set during registration - after activation'
     register:
       type: string
       label: 'Who can register accounts?'
@@ -86,6 +95,12 @@ user.mail:
     register_no_approval_required:
       type: mail
       label: 'Registration confirmation (No approval required)'
+    register_password_set:
+      type: mail
+      label: 'Registration with password'
+    register_password_set_activation:
+      type: mail
+      label: 'Registration with password - after activation'
     register_pending_approval:
       type: mail
       label: 'Registration confirmation (Pending approval)'
diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php
index d0c5e8d2d9b..0935bfe7706 100644
--- a/core/modules/user/src/AccountForm.php
+++ b/core/modules/user/src/AccountForm.php
@@ -170,7 +170,7 @@ public function form(array $form, FormStateInterface $form_state) {
         }
       }
     }
-    elseif (!$config->get('verify_mail') || $admin_create) {
+    elseif (!$config->get('verify_mail') || $config->get('register_password_set') || $admin_create) {
       $form['account']['pass'] = [
         '#type' => 'password_confirm',
         '#size' => 25,
diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php
index 60ad0a6c917..a39b7e28968 100644
--- a/core/modules/user/src/AccountSettingsForm.php
+++ b/core/modules/user/src/AccountSettingsForm.php
@@ -138,6 +138,22 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#config_target' => 'user.settings:verify_mail',
       '#description' => $this->t('New users will be required to validate their email address prior to logging into the site, and will be assigned a system-generated password. With this setting disabled, users will be logged in immediately upon registering, and may select their own passwords during registration.'),
     ];
+    $form['registration_cancellation']['register_password_set'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require visitors to set their password during registration'),
+      '#description' => $this->t('If <em>Require e-mail verification</em> is disabled, this setting is automatically enabled.'),
+      '#config_target' => 'user.settings:register_password_set',
+      '#states' => [
+        // Disable this option if email_verification is unchecked.
+        'disabled' => [
+          'input[name="user_email_verification"]' => ['checked' => FALSE],
+        ],
+        // Enable this option if email_verification is checked.
+        'enabled' => [
+          'input[name="user_email_verification"]' => ['checked' => TRUE],
+        ],
+      ]
+    ];
     $form['registration_cancellation']['user_password_strength'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Enable password strength indicator'),
@@ -260,6 +276,55 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#rows' => 15,
     ];
 
+    $form['email_password_set_activation'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Welcome (password set at registration)'),
+      '#collapsed' => TRUE,
+      '#description' => $this->t('Edit the welcome e-mail messages sent to new members upon registering, when no administrator approval is required and password has already been set.') . ' ' . $email_token_help,
+      '#group' => 'email',
+    ];
+    $form['email_password_set_activation']['user_mail_register_password_set_activation_notify'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Notify user'),
+      '#config_target' => 'user.settings:notify.register_password_set_activation',
+    ];
+    $form['email_password_set_activation']['user_mail_register_password_set_activation_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Subject'),
+      '#config_target' => 'user.mail:register_password_set_activation.subject',
+      '#maxlength' => 180,
+    ];
+    $form['email_password_set_activation']['user_mail_register_password_set_activation_body'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Body'),
+      '#config_target' => 'user.mail:register_password_set_activation.body',
+      '#rows' => 15,
+    ];
+    $form['email_password_set'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Account activation (password set at registration)'),
+      '#collapsed' => TRUE,
+      '#description' => $this->t('Edit the activation e-mail messages sent to new members upon registering, when no administrator approval is required and password has already been set during registration.') . ' ' . $email_token_help,
+      '#group' => 'email',
+    ];
+    $form['email_password_set']['user_mail_register_password_set_notify'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Notify user'),
+      '#config_target' => 'user.settings:notify.register_password_set',
+    ];
+    $form['email_password_set']['user_mail_register_password_set_subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Subject'),
+      '#config_target' => 'user.mail:register_password_set.subject',
+      '#maxlength' => 180,
+    ];
+    $form['email_password_set']['user_mail_register_password_set_body'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Body'),
+      '#config_target' => 'user.mail:register_password_set.body',
+      '#rows' => 15,
+    ];
+
     $form['email_password_reset'] = [
       '#type' => 'details',
       '#title' => $this->t('Password recovery'),
diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php
index 8e271fa9819..6f98a069d88 100644
--- a/core/modules/user/src/Controller/UserController.php
+++ b/core/modules/user/src/Controller/UserController.php
@@ -207,12 +207,16 @@ public function getResetPassForm(Request $request, $uid) {
 
     /** @var \Drupal\user\UserInterface $user */
     $user = $this->userStorage->load($uid);
-    if ($user === NULL || !$user->isActive()) {
+    if ($user === NULL || (!$user->isActive() && $user->getLastLoginTime())) {
       // Blocked or invalid user ID, so deny access. The parameters will be in
       // the watchdog's URL for the administrator to check.
       throw new AccessDeniedHttpException();
     }
 
+    if ($redirect = $this->activateUserOnFirstLogin($user, $timestamp, $hash)) {
+      return $redirect;
+    }
+
     // Time out, in seconds, until login URL expires.
     $timeout = $this->config('user.settings')->get('password_reset_timeout');
 
@@ -302,12 +306,16 @@ protected function determineErrorRedirect(?UserInterface $user, int $timestamp,
     // The current user is not logged in, so check the parameters.
     $current = $this->time->getRequestTime();
     // Verify that the user exists and is active.
-    if ($user === NULL || !$user->isActive()) {
+    if ($user === NULL || (!$user->isActive() && $user->getLastLoginTime())) {
       // Blocked or invalid user ID, so deny access. The parameters will be in
       // the watchdog's URL for the administrator to check.
       throw new AccessDeniedHttpException();
     }
 
+    if ($redirect = $this->activateUserOnFirstLogin($user, $timestamp, $hash)) {
+      return $redirect;
+    }
+
     // Time out, in seconds, until login URL expires.
     $timeout = $this->config('user.settings')->get('password_reset_timeout');
     // No time out for first time login.
@@ -439,4 +447,39 @@ public function confirmCancel(UserInterface $user, $timestamp = 0, $hashed_pass
     throw new AccessDeniedHttpException();
   }
 
+  /**
+   * Activate the user if it is blocked and has never logged in.
+   * @param $user
+   * @param $timestamp
+   * @param $hash
+   * @return RedirectResponse|void
+   */
+  public function activateUserOnFirstLogin($user, $timestamp, $hash) {
+    if (!$user->isActive() && !$user->getLastLoginTime() && hash_equals($hash, user_pass_rehash($user, $timestamp))) {
+      // Convert timestamp to date for logs.
+      $current = \Drupal::time()->getRequestTime();
+      $date = $this->dateFormatter->format($timestamp);
+      $this->logger->notice('User %name used one-time login link at time %timestamp.', [
+        '%name' => $user->getDisplayName(),
+        '%timestamp' => $date,
+      ]);
+      // Activate the user and update the access and login time to $current.
+      $user->activate()
+        ->setLastAccessTime($current)
+        ->setLastLoginTime($current)
+        ->save();
+
+      // user_login_finalize() also updates the login timestamp of the
+      // user, which invalidates further use of the one-time login link.
+      user_login_finalize($user);
+
+      // Display default welcome message.
+      $this->messenger()
+        ->addStatus($this->t('You have just used your one-time login link. Your account is now active and you are authenticated.'));
+
+      // By default, redirect to the user profile page.
+      return $this->redirect('entity.user.canonical', ['user' => $user->id()], ['query' => ['activate_first_login' => 1]]);
+    }
+  }
+
 }
diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php
index 040c4ff20f5..a33d085d208 100644
--- a/core/modules/user/src/Entity/User.php
+++ b/core/modules/user/src/Entity/User.php
@@ -167,6 +167,14 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
       if ($this->status->value != $this->getOriginal()->status->value) {
         // The user's status is changing; conditionally send notification email.
         $op = $this->status->value == 1 ? 'status_activated' : 'status_blocked';
+
+        // Send the password_set activation e-mail to the user
+        // if we are called from the user_register_form and
+        // password_register is set to enabled.
+        if (\Drupal::config('user.settings')->get('register_password_set') && !$this->original->status->value && $this->status->value) {
+          $op = 'register_password_set_activation';
+        }
+
         _user_mail_notify($op, $this);
       }
     }
diff --git a/core/modules/user/src/RegisterForm.php b/core/modules/user/src/RegisterForm.php
index 0ee891b7094..7c3ec98e482 100644
--- a/core/modules/user/src/RegisterForm.php
+++ b/core/modules/user/src/RegisterForm.php
@@ -62,15 +62,23 @@ protected function actions(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    $config = $this->config('user.settings');
     $admin = $form_state->getValue('administer_users');
 
-    if (!\Drupal::config('user.settings')->get('verify_mail') || $admin) {
+    if (!$config->get('verify_mail') || ($config->get('verify_mail') && $config->get('register_password_set')) || $admin) {
       $pass = $form_state->getValue('pass');
     }
     else {
       $pass = \Drupal::service('password_generator')->generate();
     }
 
+    // If we are not an admin and we try to register and password_register
+    // is set, make sure the status is set to disabled before we save
+    // the newly created account.
+    if ($config->get('verify_mail') && $config->get('register_password_set') && !$form_state->getValue('uid') && !$admin) {
+      $form_state->setValue('status', 0);
+    }
+
     // Remove unneeded values.
     $form_state->cleanValues();
 
@@ -84,6 +92,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
+    $config = $this->config('user.settings');
     $account = $this->entity;
     $pass = $account->getPassword();
     $admin = $form_state->getValue('administer_users');
@@ -105,8 +114,21 @@ public function save(array $form, FormStateInterface $form_state) {
     if ($admin && !$notify) {
       $this->messenger()->addStatus($this->t('Created a new user account for <a href=":url">%name</a>. No email has been sent.', [':url' => $account->toUrl()->toString(), '%name' => $account->getAccountName()]));
     }
+    // Email verification enabled, but users set a password during registration.
+    elseif (!$admin && $config->get('register') === UserInterface::REGISTER_VISITORS && $config->get('register_password_set') && $config->get('verify_mail') && !$account->isActive()) {
+      _user_mail_notify('register_password_set', $account);
+      $this->messenger()->addStatus($this->t('A welcome message with further instructions has been sent to your email address.'));
+      $form_state->setRedirect('<front>');
+    }
+    // Email verification enabled.
+    elseif (!$admin && $config->get('register') === UserInterface::REGISTER_VISITORS && $config->get('register_password_set') && !$account->isActive()) {
+      _user_mail_notify('register_pending_approval', $account);
+      $this->messenger()->addStatus($this->t('A welcome message with further instructions has been sent to your email address.'));
+      $form_state->setRedirect('<front>');
+    }
+
     // No email verification required; log in user immediately.
-    elseif (!$admin && !\Drupal::config('user.settings')->get('verify_mail') && $account->isActive()) {
+    elseif (!$admin && !$config->get('verify_mail') && $account->isActive()) {
       _user_mail_notify('register_no_approval_required', $account);
       user_login_finalize($account);
       $this->messenger()->addStatus($this->t('Registration successful. You are now logged in.'));
diff --git a/core/modules/user/tests/src/Functional/UserRegistrationTest.php b/core/modules/user/tests/src/Functional/UserRegistrationTest.php
index 37f91cef151..d394cb5d282 100644
--- a/core/modules/user/tests/src/Functional/UserRegistrationTest.php
+++ b/core/modules/user/tests/src/Functional/UserRegistrationTest.php
@@ -6,6 +6,7 @@
 
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Test\AssertMailTrait;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\Tests\BrowserTestBase;
@@ -20,6 +21,8 @@
 #[RunTestsInSeparateProcesses]
 class UserRegistrationTest extends BrowserTestBase {
 
+  use AssertMailTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -46,12 +49,13 @@ public function testRegistrationWithEmailVerification(): void {
 
     // Allow registration by site visitors without administrator approval.
     $config->set('register', UserInterface::REGISTER_VISITORS)->save();
+    $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $edit = [];
     $edit['name'] = $name = $this->randomMachineName();
     $edit['mail'] = $mail = $edit['name'] . '@example.com';
-    $this->drupalGet('user/register');
     $this->submitForm($edit, 'Create new account');
-    $this->assertSession()->pageTextContains('A welcome message with further instructions has been sent to your email address.');
+    $this->assertSession()->responseContains('A welcome message with further instructions has been sent to your email address.');
 
     /** @var EntityStorageInterface $storage */
     $storage = $this->container->get('entity_type.manager')->getStorage('user');
@@ -64,10 +68,11 @@ public function testRegistrationWithEmailVerification(): void {
 
     // Allow registration by site visitors, but require administrator approval.
     $config->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save();
+    $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $edit = [];
     $edit['name'] = $name = $this->randomMachineName();
     $edit['mail'] = $mail = $edit['name'] . '@example.com';
-    $this->drupalGet('user/register');
     $this->submitForm($edit, 'Create new account');
     $this->container->get('entity_type.manager')->getStorage('user')->resetCache();
     $accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]);
@@ -92,42 +97,46 @@ public function testRegistrationWithoutEmailVerification(): void {
     $edit['mail'] = $mail = $edit['name'] . '@example.com';
 
     // Try entering a mismatching password.
+    $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $edit['pass[pass1]'] = '99999.0';
     $edit['pass[pass2]'] = '99999';
-    $this->drupalGet('user/register');
     $this->submitForm($edit, 'Create new account');
-    $this->assertSession()->pageTextContains('The specified passwords do not match.');
+    $this->assertSession()->responseContains('The specified passwords do not match.');
 
     // Enter a correct password.
     $edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
     $edit['pass[pass2]'] = $new_pass;
     $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($edit, 'Create new account');
     $this->container->get('entity_type.manager')->getStorage('user')->resetCache();
     $accounts = $this->container->get('entity_type.manager')->getStorage('user')
       ->loadByProperties(['name' => $name, 'mail' => $mail]);
     $new_user = reset($accounts);
     $this->assertNotNull($new_user, 'New account successfully created with matching passwords.');
-    $this->assertSession()->pageTextContains('Registration successful. You are now logged in.');
+    $this->assertSession()->responseContains('Registration successful. You are now logged in.');
     $this->drupalLogout();
 
     // Allow registration by site visitors, but require administrator approval.
     $config->set('register', UserInterface::REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL)->save();
+    $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $edit = [];
     $edit['name'] = $name = $this->randomMachineName();
     $edit['mail'] = $mail = $edit['name'] . '@example.com';
     $edit['pass[pass1]'] = $pass = $this->randomMachineName();
     $edit['pass[pass2]'] = $pass;
-    $this->drupalGet('user/register');
     $this->submitForm($edit, 'Create new account');
-    $this->assertSession()->pageTextContains('Thank you for applying for an account. Your account is currently pending approval by the site administrator.');
+    $this->assertSession()->responseContains('Thank you for applying for an account. Your account is currently pending approval by the site administrator.');
 
     // Try to log in before administrator approval.
+    $this->drupalGet('user/login');
+    $this->assertSession()->statusCodeEquals(200);
     $auth = [
       'name' => $name,
       'pass' => $pass,
     ];
-    $this->drupalGet('user/login');
     $this->submitForm($auth, 'Log in');
     $this->assertSession()->pageTextContains('The username ' . $name . ' has not been activated or is blocked.');
 
@@ -141,13 +150,80 @@ public function testRegistrationWithoutEmailVerification(): void {
       'status' => 1,
     ];
     $this->drupalGet('user/' . $new_user->id() . '/edit');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($edit, 'Save');
     $this->drupalLogout();
 
     // Log in after administrator approval.
     $this->drupalGet('user/login');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($auth, 'Log in');
-    $this->assertSession()->pageTextContains('Member for');
+    $this->assertSession()->responseContains('Member for');
+  }
+
+  /**
+   * Tests registration form with password set.
+   */
+  public function testRegistrationWithPasswordSet(): void {
+    // Require e-mail verification, but let's users choose a password during
+    // registration and allow registration by site visitors without
+    // administrator approval
+    $this->config('user.settings')
+      ->set('verify_mail', TRUE)
+      ->set('register', UserInterface::REGISTER_VISITORS)
+      ->set('register_password_set', TRUE)
+      ->set('notify.register_pending_approval', TRUE)
+      ->save();
+
+    $edit = [];
+    $edit['name'] = $name = $this->randomMachineName();
+    $edit['mail'] = $mail = $edit['name'] . '@example.com';
+    $edit['pass[pass1]'] = $new_pass = $this->randomMachineName();
+    $edit['pass[pass2]'] = $new_pass;
+
+    // Create a new user.
+    $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->submitForm($edit, 'Create a new account');
+    $this->assertSession()->responseContains('A welcome message with further instructions has been sent to your email address.');
+
+    // Make sure the user is still blocked.
+    $this->container->get('entity_type.manager')->getStorage('user')->resetCache();
+    $accounts = $this->container->get('entity_type.manager')->getStorage('user')
+      ->loadByProperties(['name' => $name, 'mail' => $mail]);
+    $new_user = reset($accounts);
+    $this->assertEmpty($new_user->status->value, 'New account is blocked until approved via e-mail confirmation.');
+
+    // Try to login before activating the account via e-mail
+    $edit2 = [];
+    $edit2['name'] = $name;
+    $edit2['pass'] = $new_pass;
+    $this->drupalGet('user/login');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->submitForm($edit2, 'Log in');
+    $this->assertSession()->pageTextContains('The username ' . $name . ' has not been activated or is blocked.');
+
+    // Try to activate the user
+    $new_user->activate();
+    $new_user->save();
+    $edit = [
+      'name' => $new_user->getAccountName(),
+    ];
+    $this->drupalGet('user/password');
+    $this->submitForm($edit, 'Submit');
+    $_emails = $this->getMails();
+    $start = strpos($_emails[2]['body'], 'user/reset/' . $new_user->id());
+    $path = substr($_emails[2]['body'], $start, 66 + strlen($new_user->id()));
+
+    // Log in with temporary login link
+    $this->drupalGet($path);
+    $this->submitForm([], 'Log in');
+
+    // Change the password
+    $password = \Drupal::service('password_generator')->generate();
+    $edit = ['pass[pass1]' => $password, 'pass[pass2]' => $password];
+    $this->submitForm($edit, 'Save');
+    $this->assertSession()->pageTextContains('The changes have been saved.');
   }
 
   /**
@@ -170,6 +246,7 @@ public function testRegistrationEmailDuplicates(): void {
 
     // Attempt to create a new account using an existing email address.
     $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($edit, 'Create new account');
     $this->assertSession()->pageTextContains('The email address ' . $duplicate_user->getEmail() . ' is already taken.');
 
@@ -178,6 +255,7 @@ public function testRegistrationEmailDuplicates(): void {
     $edit['mail'] = '   ' . $duplicate_user->getEmail() . '   ';
 
     $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($edit, 'Create new account');
     $this->assertSession()->pageTextContains('The email address ' . $duplicate_user->getEmail() . ' is already taken.');
   }
@@ -232,6 +310,7 @@ public function testUuidFormState(): void {
 
     // Create one account.
     $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($edit, 'Create new account');
     $this->assertSession()->statusCodeEquals(200);
 
@@ -311,6 +390,7 @@ public function testUniqueFields(): void {
 
     $edit = ['mail' => $account->getEmail(), 'name' => $this->randomString()];
     $this->drupalGet('user/register');
+    $this->assertSession()->statusCodeEquals(200);
     $this->submitForm($edit, 'Create new account');
     $this->assertSession()->pageTextContains("The email address {$account->getEmail()} is already taken.");
   }
@@ -346,7 +426,7 @@ public function testRegistrationWithUserFields(): void {
 
     // Check that the field does not appear on the registration form.
     $this->drupalGet('user/register');
-    $this->assertSession()->pageTextNotContains($field->label());
+    $this->assertSession()->responseNotContains($field->label());
     $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:core.entity_form_display.user.user.register');
     $this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:user.settings');
 
@@ -367,7 +447,7 @@ public function testRegistrationWithUserFields(): void {
     $edit['test_user_field[0][value]'] = '';
     $this->submitForm($edit, 'Create new account');
     $this->assertRegistrationFormCacheTagsWithUserFields();
-    $this->assertSession()->pageTextContains("{$field->label()} field is required.");
+    $this->assertSession()->responseContains("{$field->label()} field is required.");
     // Invalid input.
     $edit['test_user_field[0][value]'] = '-1';
     $this->submitForm($edit, 'Create new account');
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 239cb187736..aac796c6c5f 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -198,6 +198,37 @@ function user_pass_reset_url($account, $options = []) {
   )->toString();
 }
 
+/**
+ * Generates a unique URL for a user to log in and validate their account.
+ *
+ * @param \Drupal\user\UserInterface $account
+ *   An object containing the user account.
+ * @param array $options
+ *   (optional) A keyed array of settings. Supported options are:
+ *   - langcode: A language code to be used when generating locale-sensitive
+ *    URLs. If langcode is NULL the users preferred language is used.
+ *
+ * @return string
+ *   A unique URL that provides a one-time log in for the user, from which
+ *   they can activate their account.
+ */
+function user_validate_url(UserInterface $account, array $options = []): string {
+  $timestamp = \Drupal::time()->getRequestTime();
+  $langcode = $options['langcode'] ?? $account->getPreferredLangcode();
+  return Url::fromRoute(
+    'user.reset.login',
+    [
+      'uid' => $account->id(),
+      'timestamp' => $timestamp,
+      'hash' => user_pass_rehash($account, $timestamp),
+    ],
+    [
+      'absolute' => TRUE,
+      'language' => \Drupal::languageManager()->getLanguage($langcode),
+    ]
+  )->toString();
+}
+
 /**
  * Generates a URL to confirm an account cancellation request.
  *
@@ -459,6 +490,7 @@ function user_mail_tokens(&$replacements, $data, $options): void {
   if (isset($data['user'])) {
     $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options);
     $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options);
+    $replacements['[user:one-time-login-validate-url]'] = user_validate_url($data['user'], $options);
   }
 }
 
@@ -558,6 +590,7 @@ function user_role_revoke_permissions($rid, array $permissions = []): void {
  *     self-registers.
  *   - 'register_pending_approval': Welcome message, user pending admin
  *     approval.
+ *   - 'register_password_set_activation': Activation message, password set.
  *   - 'password_reset': Password recovery request.
  *   - 'status_activated': Account activated.
  *   - 'status_blocked': Account blocked.
