diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php
index f67b9f5..143361e 100644
--- a/core/lib/Drupal/Core/Access/AccessResult.php
+++ b/core/lib/Drupal/Core/Access/AccessResult.php
@@ -31,11 +31,16 @@
   /**
    * Creates an AccessResultInterface object with isNeutral() === TRUE.
    *
+   * @param string|null $reason
+   *   (optional) The reason why access is forbidden. Intended for developers,
+   *   hence not translatable.
+   *
    * @return \Drupal\Core\Access\AccessResult
    *   isNeutral() will be TRUE.
    */
-  public static function neutral() {
-    return new AccessResultNeutral();
+  public static function neutral($reason = NULL) {
+    assert('is_string($reason) || is_null($reason)');
+    return new AccessResultNeutral($reason);
   }
 
   /**
@@ -106,7 +111,12 @@ public static function forbiddenIf($condition) {
    *   isNeutral() will be TRUE.
    */
   public static function allowedIfHasPermission(AccountInterface $account, $permission) {
-    return static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
+    $access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
+
+    if ($access_result instanceof AccessResultReasonInterface) {
+      $access_result->setReason("The {$permission} permission is required.");
+    }
+    return $access_result;
   }
 
   /**
@@ -319,6 +329,14 @@ public function orIf(AccessResultInterface $other) {
       $result = static::neutral();
       if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
         $merge_other = TRUE;
+        if ($other instanceof AccessResultReasonInterface) {
+          $result->setReason($other->getReason());
+        }
+      }
+      else {
+        if ($this instanceof AccessResultReasonInterface) {
+          $result->setReason($this->getReason());
+        }
       }
     }
     $result->inheritCacheability($this);
@@ -358,6 +376,14 @@ public function andIf(AccessResultInterface $other) {
       $result = static::neutral();
       if (!$this->isNeutral()) {
         $merge_other = TRUE;
+        if ($other instanceof AccessResultReasonInterface) {
+          $result->setReason($other->getReason());
+        }
+      }
+      else {
+        if ($this instanceof AccessResultReasonInterface) {
+          $result->setReason($this->getReason());
+        }
       }
     }
     $result->inheritCacheability($this);
diff --git a/core/lib/Drupal/Core/Access/AccessResultNeutral.php b/core/lib/Drupal/Core/Access/AccessResultNeutral.php
index 7a180f8..091565d 100644
--- a/core/lib/Drupal/Core/Access/AccessResultNeutral.php
+++ b/core/lib/Drupal/Core/Access/AccessResultNeutral.php
@@ -5,7 +5,24 @@
 /**
  * Value object indicating a neutral access result, with cacheability metadata.
  */
-class AccessResultNeutral extends AccessResult {
+class AccessResultNeutral extends AccessResult implements AccessResultReasonInterface {
+
+  /**
+   * The reason why access is neutral. For use in messages.
+   *
+   * @var string|null
+   */
+  protected $reason;
+
+  /**
+   * Constructs a new AccessResultForbidden instance.
+   *
+   * @param null|string $reason
+   *   (optional) a message to provide details about this access result
+   */
+  public function __construct($reason = NULL) {
+    $this->reason = $reason;
+  }
 
   /**
    * {@inheritdoc}
@@ -14,4 +31,19 @@ public function isNeutral() {
     return TRUE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getReason() {
+    return $this->reason;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setReason($reason) {
+    $this->reason = $reason;
+    return $this;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
index 62f5486..be3f55e 100644
--- a/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/AuthenticationSubscriber.php
@@ -96,7 +96,7 @@ public function onKernelRequestFilterProvider(GetResponseEvent $event) {
     if (isset($this->filter) && $event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
       $request = $event->getRequest();
       if ($this->authenticationProvider->applies($request) && !$this->filter->appliesToRoutedRequest($request, TRUE)) {
-        throw new AccessDeniedHttpException();
+        throw new AccessDeniedHttpException('The used authentication method is not allowed on this route.');
       }
     }
   }
diff --git a/core/modules/basic_auth/src/Tests/Authentication/BasicAuthTest.php b/core/modules/basic_auth/src/Tests/Authentication/BasicAuthTest.php
index 2aca988..04bb0fd 100644
--- a/core/modules/basic_auth/src/Tests/Authentication/BasicAuthTest.php
+++ b/core/modules/basic_auth/src/Tests/Authentication/BasicAuthTest.php
@@ -173,6 +173,12 @@ function testUnauthorizedErrorMessage() {
     $this->basicAuthGet($url, $account->getUsername(), $this->randomMachineName());
     $this->assertResponse('403', 'The user is blocked when wrong credentials are passed.');
     $this->assertText('Access denied', "A user friendly access denied message is displayed");
+
+    // Case when correct credentials but hasn't access to the route.
+    $url = Url::fromRoute('router_test.15');
+    $this->basicAuthGet($url, $account->getUsername(), $account->pass_raw);
+    $this->assertResponse('403', 'The used authentication method is not allowed on this route.');
+    $this->assertText('Access denied', "A user friendly access denied message is displayed");
   }
 
   /**
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index a5cb361..58e359e 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -106,7 +106,7 @@ public static function create(ContainerInterface $container, array $configuratio
   public function get(EntityInterface $entity) {
     $entity_access = $entity->access('view', NULL, TRUE);
     if (!$entity_access->isAllowed()) {
-      throw new AccessDeniedHttpException();
+      throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->accessDeniedExceptionMessage($entity, 'view'));
     }
 
     $response = new ResourceResponse($entity, 200);
@@ -144,9 +144,11 @@ public function post(EntityInterface $entity = NULL) {
       throw new BadRequestHttpException('No entity content received.');
     }
 
-    if (!$entity->access('create')) {
-      throw new AccessDeniedHttpException();
+    $entity_access = $entity->access('create');
+    if (!$entity_access->isAllowed()) {
+      throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->accessDeniedExceptionMessage($entity, 'create'));
     }
+
     $definition = $this->getPluginDefinition();
     // Verify that the deserialized entity is of the type that we expect to
     // prevent security issues.
@@ -199,8 +201,10 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
     if ($entity->getEntityTypeId() != $definition['entity_type']) {
       throw new BadRequestHttpException('Invalid entity type');
     }
-    if (!$original_entity->access('update')) {
-      throw new AccessDeniedHttpException();
+
+    $entity_access = $original_entity->access('update');
+    if (!$entity_access->isAllowed()) {
+      throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->accessDeniedExceptionMessage($entity, 'update'));
     }
 
     // Overwrite the received properties.
@@ -263,9 +267,12 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
   public function delete(EntityInterface $entity) {
-    if (!$entity->access('delete')) {
-      throw new AccessDeniedHttpException();
+
+    $entity_access = $entity->access('delete');
+    if (!$entity_access->isAllowed()) {
+      throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->accessDeniedExceptionMessage($entity, 'delete'));
     }
+
     try {
       $entity->delete();
       $this->logger->notice('Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
@@ -279,6 +286,26 @@ public function delete(EntityInterface $entity) {
   }
 
   /**
+   * Return the proper message checking if the entity has bundles.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity object.
+   * @param string $operation
+   *   The operation executed before to call the exception.
+   *
+   * @return string $operation
+   *   The proper message to display in the AccessDeniedHttpException.
+   */
+  public function accessDeniedExceptionMessage(EntityInterface $entity, $operation) {
+    $message = "You are not authorized to {$operation} this {$entity->getEntityTypeId()} entity";
+
+    if ($entity->bundle() !== $entity->getEntityTypeId()) {
+      $message .= " of bundle {$entity->bundle()}";
+    }
+    return "{$message}.";
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function permissions() {
diff --git a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
index 18dc296..c187b65 100644
--- a/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/CookieResourceTestTrait.php
@@ -92,7 +92,9 @@ protected function getAuthenticationRequestOptions($method) {
    * {@inheritdoc}
    */
   protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
-    $this->assertResourceErrorResponse(403, '', $response);
+    // Requests needing cookie authentication but missing it results in a 403
+    // response. The cookie authentication mechanism sets no response message.
+    $this->assertResourceErrorResponse(403, FALSE, $response);
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index e154a75..37dcb9e 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Url;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\Core\Render\Element\PasswordConfirmTest;
 use Drupal\Tests\rest\Functional\ResourceTestBase;
 use GuzzleHttp\RequestOptions;
 use Psr\Http\Message\ResponseInterface;
@@ -339,9 +340,13 @@ public function testGet() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('GET', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
-    $this->assertResourceErrorResponse(403, '', $response);
-
+    $permission = $this->entity->getEntityType()->getAdminPermission();
+    if ($permission !== FALSE) {
+      $this->assertResourceErrorResponse(403, "The {$permission} permission is required.", $response);
+    }
+    else {
+      $this->assertResourceErrorResponse(403, "You are not authorized to view this {$this->entity->getEntityTypeId()} entity" . (($this->entity->bundle() !== $this->entity->getEntityTypeId()) ? " of bundle {$this->entity->bundle()}." : "."), $response);
+    }
 
     $this->setUpAuthorization('GET');
 
@@ -401,7 +406,8 @@ public function testGet() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('GET', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
+    // In case we have a BC layer, permissions are used, which don't have a good
+    // error message.
     $this->assertResourceErrorResponse(403, '', $response);
 
 
@@ -556,9 +562,13 @@ public function testPost() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('POST', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
-    $this->assertResourceErrorResponse(403, '', $response);
-
+    $permission = $this->entity->getEntityType()->getAdminPermission();
+    if ($permission !== FALSE) {
+      $this->assertResourceErrorResponse(403, "The {$permission} permission is required.", $response);
+    }
+    else {
+      $this->assertResourceErrorResponse(403, "You are not authorized to create this {$this->entity->getEntityTypeId()} entity" . (($this->entity->bundle() !== $this->entity->getEntityTypeId()) ? " of bundle {$this->entity->bundle()}." : "."), $response);
+    }
 
     $this->setUpAuthorization('POST');
 
@@ -624,7 +634,8 @@ public function testPost() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('POST', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
+    // In case we have a BC layer, permissions are used, which don't have a good
+    // error message.
     $this->assertResourceErrorResponse(403, '', $response);
 
 
@@ -748,9 +759,13 @@ public function testPatch() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('PATCH', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
-    $this->assertResourceErrorResponse(403, '', $response);
-
+    $permission = $this->entity->getEntityType()->getAdminPermission();
+    if ($permission !== FALSE) {
+      $this->assertResourceErrorResponse(403, "The {$permission} permission is required.", $response);
+    }
+    else {
+      $this->assertResourceErrorResponse(403, "You are not authorized to update this {$this->entity->getEntityTypeId()} entity" . (($this->entity->bundle() !== $this->entity->getEntityTypeId()) ? " of bundle {$this->entity->bundle()}." : "."), $response);
+    }
 
     $this->setUpAuthorization('PATCH');
 
@@ -831,7 +846,8 @@ public function testPatch() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('PATCH', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
+    // In case we have a BC layer, permissions are used, which don't have a good
+    // error message.
     $this->assertResourceErrorResponse(403, '', $response);
 
 
@@ -901,9 +917,13 @@ public function testDelete() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('DELETE', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
-    $this->assertResourceErrorResponse(403, '', $response);
-
+    $permission = $this->entity->getEntityType()->getAdminPermission();
+    if ($permission !== FALSE) {
+      $this->assertResourceErrorResponse(403, "The {$permission} permission is required.", $response);
+    }
+    else {
+      $this->assertResourceErrorResponse(403, "You are not authorized to delete this {$this->entity->getEntityTypeId()} entity" . (($this->entity->bundle() !== $this->entity->getEntityTypeId()) ? " of bundle {$this->entity->bundle()}." : "."), $response);
+    }
 
     $this->setUpAuthorization('DELETE');
 
@@ -931,7 +951,8 @@ public function testDelete() {
 
     // DX: 403 when unauthorized.
     $response = $this->request('DELETE', $url, $request_options);
-    // @todo Update the message in https://www.drupal.org/node/2808233.
+    // In case we have a BC layer, permissions are used, which don't have a good
+    // error message.
     $this->assertResourceErrorResponse(403, '', $response);
 
 
diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
index 218e96f..6a3874b 100644
--- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php
@@ -321,7 +321,7 @@ protected function assertResourceResponse($expected_status_code, $expected_body,
    *   The error response to assert.
    */
   protected function assertResourceErrorResponse($expected_status_code, $expected_message, ResponseInterface $response) {
-    $expected_body = $this->serializer->encode(['message' => $expected_message], static::$format);
+    $expected_body = ($expected_message !== FALSE) ? $this->serializer->encode(['message' => $expected_message], static::$format) : FALSE;
     $this->assertResourceResponse($expected_status_code, $expected_body, $response);
   }
 
