feat: dedicated login page for oauth2 logins

This commit is contained in:
fromme committed 2026-09-27 13:43:17 +02:00
1 parent 56adf72d12
commit 2aa98b81fe
8 files changed
+376 -69

No files matched your search

+53 -12
View File
@@ -5,6 +5,7 @@ namespace App\Controller;
use App\Form\Model\BookingDto;
use App\Service\BookingSessionManager;
use App\Service\BookingSummaryAssembler;
use League\Bundle\OAuth2ServerBundle\Manager\ClientManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -21,13 +22,28 @@ class SecurityController extends AbstractController
Request $request,
BookingSessionManager $bookingSessionService,
BookingSummaryAssembler $summaryDataService,
ClientManagerInterface $clientManager,
): Response {
// flag this request in the session as external OAuth2 request if
// applicable to immediately logout the current user after successful
// authentication (see App\EventListener\AuthorizationCodeListener).
// Read before the booking flow is decided, because it overrules it.
$session = $request->getSession();
$targetPath = $session->get('_security.main.target_path');
$isOauth2 = \is_string($targetPath) && str_contains($targetPath, '/authorize');
$session->set('_oauth2', $isOauth2);
// Check if this is a booking flow (BookingDto exists in session)
$bookingDto = $bookingSessionService->getBookingDto($request, BookingDto::MODE_CREATE);
$isBookingFlow = null !== $bookingDto;
$hasPendingBooking = null !== $bookingDto;
// A pending authorization wins over a half-finished booking. Otherwise the target path
// below is overwritten with booking step 1, the user is sent there after logging in, and
// the application that asked for the authorization never receives its code.
$isBookingFlow = $hasPendingBooking && false === $isOauth2;
// If authenticated and in booking flow, proceed to Step 1
if (null !== $this->getUser() && true === $isBookingFlow) {
if (null !== $this->getUser() && true === $hasPendingBooking) {
return $this->redirectToRoute('app_booking_create_step_1');
}
@@ -39,21 +55,18 @@ class SecurityController extends AbstractController
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
// flag this request in the session as external OAuth2 request if
// applicable to immediately logout the current user after successful
// authentication (see App\EventListener\AuthorizationCodeListener).
$session = $request->getSession();
$targetPath = $session->get('_security.main.target_path');
$isOauth2 = null !== $targetPath && str_contains($targetPath, '/authorize');
$session->set('_oauth2', $isOauth2);
// For booking flow, set target path to Step 1 (after successful auth, redirect there)
if (true === $isBookingFlow) {
$session->set('_security.main.target_path', $this->generateUrl('app_booking_create_step_1'));
}
// Render booking login template if in booking flow, otherwise standard login
$template = true === $isBookingFlow ? 'booking/create/authenticate.html.twig' : 'security/login.html.twig';
// Render booking login template if in booking flow, the self-explaining one when another
// application sent them here to authorize it, otherwise standard login
$template = match (true) {
$isBookingFlow => 'booking/create/authenticate.html.twig',
$isOauth2 => 'security/login_oauth2.html.twig',
default => 'security/login.html.twig',
};
// Fetch CMS data for booking flow (uses cache warmed in IndexController)
$cmsData = null;
@@ -72,9 +85,37 @@ class SecurityController extends AbstractController
'travel_date_to' => $bookingDto?->travel->dateTo,
'cmsData' => $cmsData,
'oauth2' => $isOauth2,
'client_name' => $isOauth2 ? $this->authorizingClientName($clientManager, $targetPath) : null,
]);
}
/**
* The display name of the application that sent the user here to authorize it, so the login page
* can name it instead of leaving the user wondering why the site changed.
*
* Derived from the pending target path on every render rather than stashed on the session: a
* failed login redirects back to this route and the target path survives, so the name comes back
* with it. Returns null when the client cannot be resolved, and the page falls back to neutral
* wording rather than failing -- this runs before authentication.
*/
private function authorizingClientName(ClientManagerInterface $clientManager, ?string $targetPath): ?string
{
$query = null !== $targetPath ? parse_url($targetPath, \PHP_URL_QUERY) : null;
if (!\is_string($query) || '' === $query) {
return null;
}
parse_str($query, $params);
$clientId = $params['client_id'] ?? null;
if (!\is_string($clientId) || '' === $clientId) {
return null;
}
$name = $clientManager->find($clientId)?->getName() ?? '';
return '' !== $name ? $name : null;
}
#[Route('/logout', name: 'app_logout')]
#[IsGranted('ROLE_USER')]
public function logout(): void