feat: redirect to the deeplink after the form login
This commit is contained in:
1 parent
0e2021e96d
commit
18fb3382c6
2 files changed
+96
-4
No files matched your search
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
@@ -23,9 +24,14 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
private const FIREWALL_NAME = 'main';
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
@@ -41,6 +47,21 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
return $this->urlGenerator->generate('app_security_login');
|
||||
}
|
||||
|
||||
/**
|
||||
* The firewall saves the requested url as the target path right before it calls this. An
|
||||
* htmx request is a fragment - a modal, a table body - that cannot stand as a page, so
|
||||
* after the login it must not be where the user lands. htmx does not send the
|
||||
* X-Requested-With header that would make Symfony skip it on its own.
|
||||
*/
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
{
|
||||
if (true === $request->headers->has('HX-Request') && true === $request->hasSession()) {
|
||||
$this->removeTargetPath($request->getSession(), self::FIREWALL_NAME);
|
||||
}
|
||||
|
||||
return parent::start($request, $authException);
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$email = mb_strtolower(trim($request->request->getString('_username')));
|
||||
@@ -92,6 +113,14 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
// A deeplink the user was sent to the login from. Outstanding teamer checks still
|
||||
// win: RequiredTeamerCheckSubscriber redirects every app_teamer_ request to them.
|
||||
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
|
||||
$this->removeTargetPath($request->getSession(), $firewallName);
|
||||
|
||||
return new RedirectResponse($targetPath);
|
||||
}
|
||||
|
||||
$route = $user->getDefaultRoute();
|
||||
if ('app_teamer_index' === $route) {
|
||||
$check = $this->requiredTeamerCheckRegistry->getFirstUnresolvedCheck($user);
|
||||
|
||||
@@ -24,6 +24,7 @@ use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
|
||||
|
||||
#[AllowMockObjectsWithoutExpectations]
|
||||
class BpnAuthenticatorTest extends TestCase
|
||||
@@ -137,6 +138,48 @@ class BpnAuthenticatorTest extends TestCase
|
||||
$this->assertFalse($user->isDisabled());
|
||||
}
|
||||
|
||||
public function testTheLoginLeadsToTheDeeplinkTheUserCameFrom(): void
|
||||
{
|
||||
$request = $this->createRequest();
|
||||
$request->getSession()->set('_security.main.target_path', 'https://team.example.com/administrative/assignment/detail/abc');
|
||||
|
||||
$response = $this->createAuthenticator()->onAuthenticationSuccess($request, $this->createToken(), 'main');
|
||||
|
||||
$this->assertSame('https://team.example.com/administrative/assignment/detail/abc', $response->headers->get('Location'));
|
||||
// used once: a later login in the same session starts from the default route again
|
||||
$this->assertFalse($request->getSession()->has('_security.main.target_path'));
|
||||
}
|
||||
|
||||
public function testWithoutADeeplinkTheLoginLeadsToTheDefaultRoute(): void
|
||||
{
|
||||
$response = $this->createAuthenticator()->onAuthenticationSuccess($this->createRequest(), $this->createToken(), 'main');
|
||||
|
||||
$this->assertSame('/default-route', $response->headers->get('Location'));
|
||||
}
|
||||
|
||||
public function testAnHtmxRequestIsNotKeptAsTheDeeplink(): void
|
||||
{
|
||||
// a modal opened after the session expired: the firewall saved its url just before
|
||||
$request = $this->createRequest();
|
||||
$request->headers->set('HX-Request', 'true');
|
||||
$request->getSession()->set('_security.main.target_path', 'https://team.example.com/admin/system/fee/edit/1');
|
||||
|
||||
$this->createAuthenticator()->start($request);
|
||||
|
||||
$this->assertFalse($request->getSession()->has('_security.main.target_path'));
|
||||
}
|
||||
|
||||
public function testAPageRequestIsKeptAsTheDeeplink(): void
|
||||
{
|
||||
$request = $this->createRequest();
|
||||
$request->getSession()->set('_security.main.target_path', 'https://team.example.com/administrative/assignment');
|
||||
|
||||
$response = $this->createAuthenticator()->start($request);
|
||||
|
||||
$this->assertSame('/login', $response->headers->get('Location'));
|
||||
$this->assertTrue($request->getSession()->has('_security.main.target_path'));
|
||||
}
|
||||
|
||||
private function createCrmAttributes(): CrmAttributesResponse
|
||||
{
|
||||
return (new CrmAttributesResponse())->setAttributeGroups([new CrmAttributeGroup()]);
|
||||
@@ -148,17 +191,25 @@ class BpnAuthenticatorTest extends TestCase
|
||||
$this->apiClient->method('getCrmAttributes')->willReturn($crmAttributes);
|
||||
}
|
||||
|
||||
private function loadUser(): ?User
|
||||
private function createAuthenticator(): BpnAuthenticator
|
||||
{
|
||||
$authenticator = new BpnAuthenticator(
|
||||
$this->createStub(UrlGeneratorInterface::class),
|
||||
$urlGenerator = $this->createStub(UrlGeneratorInterface::class);
|
||||
$urlGenerator->method('generate')->willReturnCallback(
|
||||
static fn (string $route): string => 'app_security_login' === $route ? '/login' : '/default-route'
|
||||
);
|
||||
|
||||
return new BpnAuthenticator(
|
||||
$urlGenerator,
|
||||
$this->createStub(EntityManagerInterface::class),
|
||||
$this->apiClient,
|
||||
$this->createStub(LoggerInterface::class),
|
||||
$this->userDataHandler,
|
||||
$this->createStub(RequiredTeamerCheckRegistry::class),
|
||||
);
|
||||
}
|
||||
|
||||
private function createRequest(): Request
|
||||
{
|
||||
$request = new Request(request: [
|
||||
'_username' => '[email protected]',
|
||||
'_password' => 'secret',
|
||||
@@ -166,7 +217,19 @@ class BpnAuthenticatorTest extends TestCase
|
||||
]);
|
||||
$request->setSession(new Session(new MockArraySessionStorage()));
|
||||
|
||||
$passport = $authenticator->authenticate($request);
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function createToken(): PostAuthenticationToken
|
||||
{
|
||||
$user = (new User())->setRoles(['ROLE_TEAM_ADMIN']);
|
||||
|
||||
return new PostAuthenticationToken($user, 'main', $user->getRoles());
|
||||
}
|
||||
|
||||
private function loadUser(): ?User
|
||||
{
|
||||
$passport = $this->createAuthenticator()->authenticate($this->createRequest());
|
||||
|
||||
/** @var UserBadge $badge */
|
||||
$badge = $passport->getBadge(UserBadge::class);
|
||||
|
||||
Reference in new issue
Block a user