diff --git a/uw_cfg_common.module b/uw_cfg_common.module
index c5f83d95731e047bb6b2f5de93c80fe3dd7dfd10..745a1f40061d85f00c60d1495b19bb8ca297c5d0 100644
--- a/uw_cfg_common.module
+++ b/uw_cfg_common.module
@@ -377,6 +377,10 @@ function uw_cfg_common_webform_create(WebformInterface $webform) {
   // Submission purge settings. Set the default to purge drafts after 28 days.
   $webform->setSetting('purge', WebformSubmissionStorageInterface::PURGE_DRAFT);
   $webform->setSetting('purge_days', 28);
+
+  // Set so that uw_cfg_common_webform_build_access_denied_alter() will run.
+  // This value is tested for in Webform::preRenderWebformElement().
+  $webform->setSetting('form_access_denied', 'page');
 }
 
 /**
@@ -835,3 +839,43 @@ function uw_cfg_common_get_user_ad_groups(): ?array {
   $attributes = \Drupal::service('simplesamlphp_auth.manager')->getAttributes();
   return $attributes['http://schemas.xmlsoap.org/claims/Group'] ?? NULL;
 }
+
+/**
+ * Implements hook_webform_build_access_denied_alter().
+ *
+ * Custom access denied messages with login/logout links.
+ */
+function uw_cfg_common_webform_build_access_denied_alter(array &$build, WebformInterface $webform): void {
+  $message = NULL;
+  switch ($webform->getThirdPartySetting('uw_cfg_common', 'access_control_method')) {
+    case 'auth':
+    case 'group':
+    case 'user':
+      // If authenticated access and anonymous user, login.
+      if (\Drupal::currentUser()->isAnonymous()) {
+        $route = 'user.login';
+        $message = 'You must <a href="@url">login to view this form</a>.';
+      }
+      break;
+
+    case 'anon':
+      // If anonymous access and authenticated user, logout.
+      if (\Drupal::currentUser()->isAuthenticated()) {
+        $route = 'user.logout';
+        $message = 'This form must be completed anonymously. You must <a href="@url">logout to view this form</a>.';
+      }
+      break;
+  }
+
+  // Set a custom message only if a message has been chosen above.
+  if ($message) {
+    $options = ['query' => \Drupal::destination()->getAsArray()];
+    $url = Url::fromRoute($route, [], $options);
+    // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
+    $message = '<p>' . t($message, ['@url' => $url->toString()]) . '</p>';
+
+    $build['message'] = [
+      '#markup' => $message,
+    ];
+  }
+}