 core/lib/Drupal/Core/Form/FormBuilder.php          |  3 --
 .../ckeditor/src/Tests/CKEditorLoadingTest.php     |  2 +
 .../Tests/CommentDefaultFormatterCacheTagsTest.php |  2 +
 core/modules/contact/src/MessageForm.php           |  3 ++
 .../src/Tests/ContactAuthenticatedUserTest.php     | 14 ++++++-
 core/modules/editor/src/Element.php                |  1 +
 .../modules/editor/src/Tests/EditorLoadingTest.php |  2 +
 .../editor/src/Tests/EditorSecurityTest.php        |  2 +
 core/modules/filter/src/Element/TextFormat.php     |  2 +
 .../Drupal/Tests/Core/Form/FormBuilderTest.php     | 45 ++++++++++++++--------
 10 files changed, 55 insertions(+), 21 deletions(-)

diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 721ee92..3dac78b 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -734,9 +734,6 @@ public function prepareForm($form_id, &$form, FormStateInterface &$form_state) {
           // submitted form value appears literally, regardless of custom #tree
           // and #parents being set elsewhere.
           '#parents' => array('form_token'),
-          '#cache' => [
-            'max-age' => 0,
-          ],
         );
       }
     }
diff --git a/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
index 1081eb9..92e694e 100644
--- a/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
+++ b/core/modules/ckeditor/src/Tests/CKEditorLoadingTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\ckeditor\Tests;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -126,6 +127,7 @@ function testLoading() {
     // configuration also results in modified CKEditor configuration, so we
     // don't test that here.
     \Drupal::service('module_installer')->install(array('ckeditor_test'));
+    Cache::invalidateTags(['rendered']);
     $this->container->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
     $editor_settings = $editor->getSettings();
     $editor_settings['toolbar']['rows'][0][0]['items'][] = 'Llama';
diff --git a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
index 327a354..4346827 100644
--- a/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
+++ b/core/modules/comment/src/Tests/CommentDefaultFormatterCacheTagsTest.php
@@ -78,6 +78,7 @@ public function testCacheTags() {
       'config:field.field.entity_test.entity_test.comment',
       'config:field.storage.comment.comment_body',
       'config:user.settings',
+      'user:1',
     ];
     sort($expected_cache_tags);
     $this->assertEqual($build['#cache']['tags'], $expected_cache_tags);
@@ -124,6 +125,7 @@ public function testCacheTags() {
       'config:field.field.entity_test.entity_test.comment',
       'config:field.storage.comment.comment_body',
       'config:user.settings',
+      'user:1',
     ];
     sort($expected_cache_tags);
     $this->assertEqual($build['#cache']['tags'], $expected_cache_tags);
diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php
index 9cb5e54..c576a56 100644
--- a/core/modules/contact/src/MessageForm.php
+++ b/core/modules/contact/src/MessageForm.php
@@ -120,6 +120,7 @@ public function form(array $form, FormStateInterface $form_state) {
       '#title' => $this->t('Your email address'),
       '#required' => TRUE,
     );
+    $form['#cache']['contexts'][] = 'user.roles:authenticated';
     if ($user->isAnonymous()) {
       $form['#attached']['library'][] = 'core/drupal.form';
       $form['#attributes']['data-user-info-from-browser'] = TRUE;
@@ -131,11 +132,13 @@ public function form(array $form, FormStateInterface $form_state) {
       $form['name']['#value'] = $user->getUsername();
       $form['name']['#required'] = FALSE;
       $form['name']['#plain_text'] = $user->getUsername();
+      $form['name']['#cache']['contexts'][] = 'user';
 
       $form['mail']['#type'] = 'item';
       $form['mail']['#value'] = $user->getEmail();
       $form['mail']['#required'] = FALSE;
       $form['mail']['#plain_text'] = $user->getEmail();
+      $form['mail']['#cache']['contexts'][] = 'user';
     }
 
     // The user contact form has a preset recipient.
diff --git a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php
index 2adc4c2..923878e 100644
--- a/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php
+++ b/core/modules/contact/src/Tests/ContactAuthenticatedUserTest.php
@@ -21,19 +21,29 @@ class ContactAuthenticatedUserTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('contact');
+  public static $modules = array('contact', 'contact_test');
 
   /**
    * Tests that name and email fields are not present for authenticated users.
    */
   function testContactSiteWideTextfieldsLoggedInTestCase() {
-    $this->drupalLogin($this->drupalCreateUser(array('access site-wide contact form')));
+    $user1 = $this->drupalCreateUser(array('access site-wide contact form'));
+    $this->drupalLogin($user1);
     $this->drupalGet('contact');
+    $this->assertResponse(200);
+    $this->assertCacheContext('user');
 
     // Ensure that there is no textfield for name.
     $this->assertFalse($this->xpath('//input[@name=:name]', array(':name' => 'name')));
+    $this->assertRaw($user1->getAccountName());
 
     // Ensure that there is no textfield for email.
     $this->assertFalse($this->xpath('//input[@name=:name]', array(':name' => 'mail')));
+
+    // Log in as a different user and confirm that
+    $user2 = $this->drupalCreateUser(array('access site-wide contact form'));
+    $this->drupalLogin($user2);
+    $this->drupalGet('contact');
+    $this->assertRaw($user2->getAccountName());
   }
 }
diff --git a/core/modules/editor/src/Element.php b/core/modules/editor/src/Element.php
index 43b99ba..41ffedf 100644
--- a/core/modules/editor/src/Element.php
+++ b/core/modules/editor/src/Element.php
@@ -53,6 +53,7 @@ function preRenderTextFormat(array $element) {
     $format_ids = array_keys($element['format']['format']['#options']);
 
     // Early-return if no text editor is associated with any of the text formats.
+    $element['#cache']['tags'][] = 'config:editor_list';
     $editors = Editor::loadMultiple($format_ids);
     foreach ($editors as $key => $editor) {
       $definition = $this->pluginManager->getDefinition($editor->getEditor());
diff --git a/core/modules/editor/src/Tests/EditorLoadingTest.php b/core/modules/editor/src/Tests/EditorLoadingTest.php
index 002ccd3..95dda17 100644
--- a/core/modules/editor/src/Tests/EditorLoadingTest.php
+++ b/core/modules/editor/src/Tests/EditorLoadingTest.php
@@ -245,6 +245,7 @@ public function testSupportedElementTypes() {
     // Assert the unicorn editor works with textfields.
     $this->drupalLogin($this->privilegedUser);
     $this->drupalGet('node/1/edit');
+    $this->assertCacheTag('config:editor_list');
     list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
     $this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
     $this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
@@ -261,6 +262,7 @@ public function testSupportedElementTypes() {
     ))->save();
 
     $this->drupalGet('node/1/edit');
+    $this->assertCacheTag('config:editor_list');
     list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
     $this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page.");
     $this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.');
diff --git a/core/modules/editor/src/Tests/EditorSecurityTest.php b/core/modules/editor/src/Tests/EditorSecurityTest.php
index bb39450..db15b05 100644
--- a/core/modules/editor/src/Tests/EditorSecurityTest.php
+++ b/core/modules/editor/src/Tests/EditorSecurityTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\editor\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Core\Cache\Cache;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -428,6 +429,7 @@ function testEditorXssFilterOverride() {
     // Enable editor_test.module's hook_editor_xss_filter_alter() implementation
     // to alter the text editor XSS filter class being used.
     \Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
+    Cache::invalidateTags(['rendered']);
 
     // First: the Insecure text editor XSS filter.
     $this->drupalGet('node/2/edit');
diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php
index 9247dba..9b3000f 100644
--- a/core/modules/filter/src/Element/TextFormat.php
+++ b/core/modules/filter/src/Element/TextFormat.php
@@ -210,6 +210,8 @@ public static function processFormat(&$element, FormStateInterface $form_state,
     $user_has_access = isset($formats[$element['#format']]);
     $user_is_admin = $user->hasPermission('administer filters');
 
+    $element['#cache']['tags'][] = 'config:filter_format_list';
+
     // If the stored format does not exist or if it is not among the allowed
     // formats for this textarea, administrators have to assign a new format.
     if ((!$format_exists || !$format_allowed) && $user_is_admin) {
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index e5f2963..f37a4c7 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -820,7 +820,7 @@ public function providerTestInvalidToken() {
    *
    * @dataProvider providerTestFormTokenCacheability
    */
-  public function testFormTokenCacheability($token, $is_authenticated, $expected_form_cacheability, $expected_token_cacheability, $method) {
+  public function testFormTokenCacheability($token, $is_authenticated, $method) {
     $user = $this->prophesize(AccountProxyInterface::class);
     $user->isAuthenticated()
       ->willReturn($is_authenticated);
@@ -845,19 +845,32 @@ public function testFormTokenCacheability($token, $is_authenticated, $expected_f
 
     $form_state = new FormState();
     $built_form = $this->formBuilder->buildForm($form_arg, $form_state);
-    if (!isset($expected_form_cacheability) || ($method == 'get' && !is_string($token))) {
+
+    // FormBuilder does not even consider to set a form token when:
+    // - #token = FALSE (opting out explicitly)
+    // - #method = GET and #token is not set to a string (GET forms don't get a
+    //   form token by default, and this form did not explicitly opt in)
+    if ($token === FALSE || ($method == 'get' && !is_string($token))) {
       $this->assertFalse(isset($built_form['#cache']));
-    }
-    else {
-      $this->assertTrue(isset($built_form['#cache']));
-      $this->assertEquals($expected_form_cacheability, $built_form['#cache']);
-    }
-    if (!isset($expected_token_cacheability)) {
       $this->assertFalse(isset($built_form['form_token']));
     }
+    // Otherwise, a form token is set, but only if the user is logged in. It is
+    // impossible (and unnecessary) to set a form token if the user is not
+    // logged in, because there is no session, and hence no CSRF token.
     else {
-      $this->assertTrue(isset($built_form['form_token']));
-      $this->assertEquals($expected_token_cacheability, $built_form['form_token']['#cache']);
+      // For forms that are eligible for form tokens, a cache context must be
+      // set that indicates the form token only exists for logged in users.
+      $this->assertTrue(isset($built_form['#cache']));
+      $this->assertEquals(['contexts' => ['user.roles:authenticated']], $built_form['#cache']);
+      // Finally, verify that a form token is generated when appropriate, with
+      // the expected cacheability metadata (or lack thereof).
+      if (!$is_authenticated) {
+        $this->assertFalse(isset($built_form['form_token']));
+      }
+      else {
+        $this->assertTrue(isset($built_form['form_token']));
+        $this->assertFalse(isset($built_form['form_token']['#cache']));
+      }
     }
   }
 
@@ -868,12 +881,12 @@ public function testFormTokenCacheability($token, $is_authenticated, $expected_f
    */
   function providerTestFormTokenCacheability() {
     return [
-      'token:none,authenticated:true' => [NULL, TRUE, ['contexts' => ['user.roles:authenticated']], ['max-age' => 0], 'post'],
-      'token:none,authenticated:false' => [NULL, FALSE, ['contexts' => ['user.roles:authenticated']], NULL, 'post'],
-      'token:false,authenticated:false' => [FALSE, FALSE, NULL, NULL, 'post'],
-      'token:false,authenticated:true' => [FALSE, TRUE, NULL, NULL, 'post'],
-      'token:none,authenticated:false,method:get' => [NULL, FALSE, ['contexts' => ['user.roles:authenticated']], NULL, 'get'],
-      'token:test_form_id,authenticated:false,method:get' => ['test_form_id', TRUE, ['contexts' => ['user.roles:authenticated']], ['max-age' => 0], 'get'],
+      'token:none,authenticated:true' => [NULL, TRUE, 'post'],
+      'token:none,authenticated:false' => [NULL, FALSE, 'post'],
+      'token:false,authenticated:false' => [FALSE, FALSE, 'post'],
+      'token:false,authenticated:true' => [FALSE, TRUE, 'post'],
+      'token:none,authenticated:false,method:get' => [NULL, FALSE, 'get'],
+      'token:test_form_id,authenticated:true,method:get' => ['test_form_id', TRUE, 'get'],
     ];
   }
 
