- ๐บ๐ธUnited States frob US
I think this is more of a Legal module issue. Here is a link to the issue in the legal module https://www.drupal.org/project/legal/issues/2638224 ๐ Add exemption api to allow a Masquerade module exemption Needs review
- ๐ฎ๐ณIndia Raveen Kumar
Raveen Thakur โ made their first commit to this issueโs fork.
This can't be a bug with Masquerade on its own, as it's working fine.
- ๐ต๐นPortugal introfini
I encountered and solved this exact issue. The root cause is stale CSRF tokens being cached after session regeneration during the masquerade process.
Root Cause:
The masquerade process calls
$this->sessionManager->regenerate()
inMasquerade::switchUser()
which invalidates the CSRF token seed stored in session metadata. When masquerade URLs are cached (which happens with lazy builders and entity operations), the cached CSRF tokens become invalid after the first masquerade attempt.Solution:
I created a custom route processor to ensure fresh CSRF tokens are always generated for masquerade routes. This single solution fixes the issue completely:
1. Create a custom route processor
src/Routing/MasqueradeRouteProcessor.php
:namespace Drupal\[YOUR_MODULE]\Routing; use Drupal\Core\Access\CsrfTokenGenerator; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; use Symfony\Component\Routing\Route; /** * Processes outbound routes for masquerade to ensure fresh CSRF tokens. * * Issue: Masquerade fails with 403 Access Denied on second attempt due to * stale CSRF tokens being cached. The masquerade process regenerates the * session ID which invalidates previously cached CSRF tokens. */ class MasqueradeRouteProcessor implements OutboundRouteProcessorInterface { protected $csrfTokenGenerator; public function __construct(CsrfTokenGenerator $csrf_token_generator) { $this->csrfTokenGenerator = $csrf_token_generator; } public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { // Only process masquerade routes if ($route_name === 'entity.user.masquerade') { // Force uncacheable to prevent stale CSRF tokens if ($bubbleable_metadata) { $bubbleable_metadata->setCacheMaxAge(0); $bubbleable_metadata->addCacheContexts(['session']); } // Generate a fresh CSRF token for this specific route $path = ltrim($route->getPath(), '/'); foreach ($parameters as $param => $value) { $path = str_replace("{{$param}}", $value, $path); } // Add fresh token to parameters if (!isset($parameters['token'])) { $parameters['token'] = $this->csrfTokenGenerator->get($path); } } } }
2. Register the service
In your module's
services.yml
:services:
your_module.masquerade_route_processor:
class: Drupal\your_module\Routing\MasqueradeRouteProcessor
arguments: [ '@csrf_token' ]
tags:
- { name: route_processor_outbound, priority: 1000 }Result:
This solution ensures that:
- Fresh CSRF tokens are generated for every masquerade request
- Masquerade URLs are never cached to prevent stale token reuse
- The fix works consistently across multiple masquerade attempts
Masquerade now works reliably on subsequent attempts without the 403 Access Denied error.
This fix addresses the core issue without requiring changes to the masquerade module itself, making it a safe workaround until the module is updated to handle this scenario properly.