Skip to content

Commit 9e0378b

Browse files
committed
SessionAuthenticator
1 parent 722c663 commit 9e0378b

8 files changed

Lines changed: 167 additions & 99 deletions

File tree

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"phplist/core": "dev-main",
5151
"symfony/twig-bundle": "^6.4",
5252
"symfony/webpack-encore-bundle": "^2.2",
53+
"symfony/security-bundle": "^6.4",
5354
"tatevikgr/rest-api-client": "dev-ISSUE-357"
5455
},
5556
"require-dev": {
@@ -107,7 +108,8 @@
107108
"symfony-tests-dir": "tests",
108109
"phplist/core": {
109110
"bundles": [
110-
"PhpList\\WebFrontend\\PhpListFrontendBundle"
111+
"PhpList\\WebFrontend\\PhpListFrontendBundle",
112+
"Symfony\\Bundle\\SecurityBundle\\SecurityBundle"
111113
],
112114
"routes": {
113115
"rest-api": {

config/packages/framework.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ framework:
33
http_method_override: false
44
php_errors:
55
log: true
6+
session:
7+
enabled: true
8+
handler_id: null
9+
cookie_secure: auto
10+
cookie_samesite: lax

config/packages/security.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
security:
2+
enable_authenticator_manager: true
3+
4+
providers:
5+
# Minimal provider; authenticator builds the user from the session token
6+
in_memory: { memory: null }
7+
8+
firewalls:
9+
main:
10+
pattern: ^/
11+
lazy: true
12+
provider: in_memory
13+
custom_authenticators:
14+
- PhpList\WebFrontend\Security\SessionAuthenticator
15+
entry_point: PhpList\WebFrontend\Security\SessionAuthenticator
16+
17+
access_control:
18+
- { path: ^/login, roles: PUBLIC_ACCESS }
19+
- { path: ^/, roles: ROLE_ADMIN }

src/Attribute/PublicRoute.php

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/Controller/AuthController.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Exception;
88
use GuzzleHttp\Exception\GuzzleException;
99
use PhpList\RestApiClient\Endpoint\AuthClient;
10-
use PhpList\WebFrontend\Attribute\PublicRoute;
1110
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1211
use Symfony\Component\HttpFoundation\Request;
1312
use Symfony\Component\HttpFoundation\Response;
@@ -22,7 +21,6 @@ public function __construct(AuthClient $apiClient)
2221
$this->apiClient = $apiClient;
2322
}
2423

25-
#[PublicRoute]
2624
#[Route('/login', name: 'login', methods: ['GET', 'POST'])]
2725
public function login(Request $request): Response
2826
{
@@ -66,7 +64,6 @@ public function login(Request $request): Response
6664
}
6765

6866
#[Route('/logout', name: 'logout')]
69-
#[PublicRoute]
7067
public function logout(Request $request): Response
7168
{
7269
$request->getSession()->remove('auth_token');
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\WebFrontend\EventSubscriber;
6+
7+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
8+
use Symfony\Component\HttpFoundation\RedirectResponse;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpKernel\Event\RequestEvent;
11+
use Symfony\Component\HttpKernel\KernelEvents;
12+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
13+
14+
/**
15+
* Temporary auth gate until Symfony SecurityBundle is active at runtime.
16+
*
17+
* Redirects all anonymous requests to the login page, except explicitly public paths.
18+
*/
19+
class AuthGateSubscriber implements EventSubscriberInterface
20+
{
21+
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
22+
{
23+
}
24+
25+
public static function getSubscribedEvents(): array
26+
{
27+
// Run early in the request, after routing is available is not required here
28+
return [
29+
KernelEvents::REQUEST => ['onKernelRequest', 8],
30+
];
31+
}
32+
33+
public function onKernelRequest(RequestEvent $event): void
34+
{
35+
if (!$event->isMainRequest()) {
36+
return;
37+
}
38+
39+
$request = $event->getRequest();
40+
if ($this->isPublicPath($request)) {
41+
return;
42+
}
43+
44+
$session = $request->getSession();
45+
if (!$session || !$session->has('auth_token')) {
46+
$loginUrl = $this->urlGenerator->generate('login');
47+
$event->setResponse(new RedirectResponse($loginUrl));
48+
}
49+
}
50+
51+
private function isPublicPath(Request $request): bool
52+
{
53+
$path = $request->getPathInfo() ?? '/';
54+
55+
// Public login route
56+
if ($path === '/login' || str_starts_with($path, '/login')) {
57+
return true;
58+
}
59+
60+
// Allow Symfony debug/profiler and WDT if present
61+
if (str_starts_with($path, '/_profiler') || str_starts_with($path, '/_wdt')) {
62+
return true;
63+
}
64+
65+
// Allow static assets commonly served under these prefixes
66+
foreach (['/build/', '/assets/', '/css/', '/js/', '/images/', '/img/', '/favicon', '/robots.txt'] as $prefix) {
67+
if (str_starts_with($path, $prefix)) {
68+
return true;
69+
}
70+
}
71+
72+
return false;
73+
}
74+
}

src/EventSubscriber/AuthRequiredSubscriber.php

Lines changed: 0 additions & 80 deletions
This file was deleted.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\WebFrontend\Security;
6+
7+
use Symfony\Component\HttpFoundation\RedirectResponse;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
11+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
12+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
13+
use Symfony\Component\Security\Core\User\InMemoryUser;
14+
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
15+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
16+
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
17+
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
18+
19+
class SessionAuthenticator extends AbstractAuthenticator
20+
{
21+
public function __construct(private readonly UrlGeneratorInterface $urlGenerator)
22+
{
23+
}
24+
25+
public function supports(Request $request): ?bool
26+
{
27+
$path = $request->getPathInfo();
28+
if (str_starts_with($path, '/login')) {
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
35+
public function authenticate(Request $request): Passport
36+
{
37+
$session = $request->getSession();
38+
39+
if ($session->has('auth_token')) {
40+
// Build a simple user granted ROLE_ADMIN when a session token exists
41+
$userBadge = new UserBadge('session-user', function (): InMemoryUser {
42+
return new InMemoryUser('session-user', '', ['ROLE_ADMIN']);
43+
});
44+
45+
return new SelfValidatingPassport($userBadge);
46+
}
47+
48+
throw new AuthenticationException('No auth token in session');
49+
}
50+
51+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
52+
{
53+
return null;
54+
}
55+
56+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
57+
{
58+
return $this->start($request, $exception);
59+
}
60+
61+
public function start(Request $request, AuthenticationException $authException = null): Response
62+
{
63+
$loginUrl = $this->urlGenerator->generate('login');
64+
return new RedirectResponse($loginUrl);
65+
}
66+
}

0 commit comments

Comments
 (0)