diff --git a/core/lib/Drupal/Core/Cache/Context/AccountPermissionsCacheContext.php b/core/lib/Drupal/Core/Cache/Context/AccountPermissionsCacheContext.php
index be5d75409fc1b143b1d5bd38050775e945e47901..9c4702ac46e85bd170d4f7433a5222cd28b902ca 100644
--- a/core/lib/Drupal/Core/Cache/Context/AccountPermissionsCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/Context/AccountPermissionsCacheContext.php
@@ -57,7 +57,11 @@ public function getContext() {
    */
   public function getCacheableMetadata() {
     $cacheable_metadata = new CacheableMetadata();
-    $tags = [];
+
+    // The permissions hash changes when:
+    // - a user is updated to have different roles;
+    $tags = ['user:' . $this->user->id()];
+    // - a role is updated to have different permissions.
     foreach ($this->user->getRoles() as $rid) {
       $tags[] = "config:user.role.$rid";
     }
diff --git a/core/lib/Drupal/Core/Cache/Context/UserRolesCacheContext.php b/core/lib/Drupal/Core/Cache/Context/UserRolesCacheContext.php
index 94784f7cd12b1c231bb9fa3820aa1c041338968d..8143bed1fa929e271313b310ba4cbda2577803d3 100644
--- a/core/lib/Drupal/Core/Cache/Context/UserRolesCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/Context/UserRolesCacheContext.php
@@ -51,7 +51,7 @@ public function getContext($role = NULL) {
    * {@inheritdoc}
    */
   public function getCacheableMetadata($role = NULL) {
-    return new CacheableMetadata();
+    return (new CacheableMetadata())->setCacheTags(['user:' . $this->user->id()]);
   }
 
 }
diff --git a/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php b/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php
index d304bf76e0933ef0de5c129997bb5495d8b428c8..767d2860591c8a1e8465e8be1f37213a5965ddaf 100644
--- a/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php
+++ b/core/modules/node/src/Cache/NodeAccessGrantsCacheContext.php
@@ -93,6 +93,11 @@ public function getCacheableMetadata($operation = NULL) {
       return $cacheable_metadata;
     }
 
+    // The node grants may change if the user is updated. (The max-age is set to
+    // zero below, but sites may override this cache context, and change it to a
+    // non-zero value. In such cases, this cache tag is needed for correctness.)
+    $cacheable_metadata->setCacheTags(['user:' . $this->user->id()]);
+
     // If the site is using node grants, this cache context can not be
     // optimized.
     return $cacheable_metadata->setCacheMaxAge(0);
diff --git a/core/modules/system/src/Tests/Cache/CacheContextOptimizationTest.php b/core/modules/system/src/Tests/Cache/CacheContextOptimizationTest.php
index 050ef4ad4f87047750e73f87be16d2c6b3a53059..3bd46df7eb6161810c3a1d530d22820d4d919d96 100644
--- a/core/modules/system/src/Tests/Cache/CacheContextOptimizationTest.php
+++ b/core/modules/system/src/Tests/Cache/CacheContextOptimizationTest.php
@@ -80,4 +80,45 @@ public function testUserPermissionCacheContextOptimization() {
     $this->assertEqual($output, 'this should be visible');
   }
 
+  /**
+   * Ensures that 'user.roles' still works when it is optimized away.
+   */
+  public function testUserRolesCacheContextOptimization() {
+    $root_user = $this->createUser();
+    $this->assertEqual($root_user->id(), 1);
+
+    $authenticated_user = $this->createUser(['administer permissions']);
+    $role = $authenticated_user->getRoles()[1];
+
+    $test_element = [
+      '#cache' => [
+        'keys' => ['test'],
+        'contexts' => ['user', 'user.roles'],
+      ],
+    ];
+    \Drupal::service('account_switcher')->switchTo($authenticated_user);
+    $element = $test_element;
+    $element['#markup'] = 'content for authenticated users';
+    $output = \Drupal::service('renderer')->renderRoot($element);
+    $this->assertEqual($output, 'content for authenticated users');
+
+    // Verify that the render caching is working so that other tests can be
+    // trusted.
+    $element = $test_element;
+    $element['#markup'] = 'this should not be visible';
+    $output = \Drupal::service('renderer')->renderRoot($element);
+    $this->assertEqual($output, 'content for authenticated users');
+
+    // Even though the cache contexts have been optimized to only include 'user'
+    // cache context, the element should have been changed because 'user.roles'
+    // cache context defined a cache tag for user entity changes23, which should
+    // have bubbled up for the element when it was optimized away.
+    $authenticated_user->removeRole($role);
+    $authenticated_user->save();
+    $element = $test_element;
+    $element['#markup'] = 'this should be visible';
+    $output = \Drupal::service('renderer')->renderRoot($element);
+    $this->assertEqual($output, 'this should be visible');
+  }
+
 }