<?php
namespace App\Controller\User;
use App\Entity\User\User;
use App\Exception\Core\SSO\NoTokenException;
use App\Security\Firewall\DefaultFirewall;
use App\Services\User\LoginManager;
use App\Services\Core\AuthService;
use App\Services\Core\CreateDemoClassroom;
use App\Services\Core\EventLogger;
use App\Services\Core\SelfStudyVoterService;
use App\Services\Core\TextbookVoterService;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class SecurityController extends AbstractController
{
use TargetPathTrait;
private Request $request;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly EventLogger $eventLogger,
private readonly TextbookVoterService $textbookVoterService,
private readonly SelfStudyVoterService $selfStudyVoterService,
private readonly LoginManager $loginManager,
private readonly CreateDemoClassroom $createDemoClassroom,
private readonly AuthService $authService,
private readonly CsrfTokenManagerInterface $tokenManager,
private readonly RequestStack $requestStack,
private readonly AuthenticationUtils $authenticationUtils,
private readonly ParameterBagInterface $parameterBag
)
{
}
// route : /cms or /testlogin redirects to /login?alternative_login=1
#[Route('/testlogin', name: 'alternative_login')]
#[Route('/cms', name: 'alternative_login_cms')]
public function alternativeLogin(Request $request): RedirectResponse
{
$request->getSession()->set('login_method', 'alternative');
return $this->redirectToRoute("login", [
"alternative_login" => true,
]);
}
// route /login
#[Route('/login', name: 'login')]
public function loginAction(): Response
{
$this->request = $this->requestStack->getCurrentRequest();
$session = $this->request->getSession();
$authErrorKey = Security::AUTHENTICATION_ERROR;
// get the error if any (works with forward and redirect -- see below)
if ($this->request->attributes->has($authErrorKey)) {
$error = $this->request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
$csrfToken = $this->tokenManager->getToken('authenticate')->getValue();
$environment = $this->parameterBag->get('abacus_environment');
$data = [
'error' => $error,
'csrf_token' => $csrfToken,
'environment' => $environment
];
if ($this->shouldShowAlternativeLogin($error)) {
return $this->render('User/Security/alternative_login.html.twig', $data);
}
return $this->render('User/Security/skip_to_unilogin.html.twig', $data);
}
#[Route('/user/unilogin', name: 'uni_login_return_route')]
public function uniLoginReturn()
{
$ssoProvider = $this->authService->getSsoProvider();
try {
$ssoProvider->initialize();
if (!$ssoProvider->isValidToken()) {
throw $this->createAccessDeniedException('The UNI-login token is invalid.');
}
/* Validate license primaryschool */
if ($this->getParameter('abacus.loginconnector.require_license') == 1) {
if (!$ssoProvider->checkIfUserHasAccess()) {
return $this->render('User/Security/nolicence_kvik.html.twig', []);
}
}
} catch (RequestException | NoTokenException $e) {
return $this->render('User/Security/timeout_error.html.twig');
}
// Find UNI user
/** @var User $user */
$user = $ssoProvider->findUserIfExists();
if ($user instanceof User) {
/* Existing user */
/* Update institution and role */
/** @var User $user */
$user = $ssoProvider->updateUser($user);
$user->setLastLoginWithUnilogin(new DateTime()); // save timestamp
$this->eventLogger->log('login_' . $_SERVER['HTTP_USER_AGENT'], $user);
}
else {
/* New user */
/** @var User $user */
$user = $ssoProvider->generateUser();
$user->setLastLoginWithUnilogin(new DateTime()); // save timestamp
$this->eventLogger->log('createuser', $user, $user->getInstitution()->getName());
}
$this->persistUser($user);
/* Validate license for highschool */
if ($this->getParameter('abacus_environment') === 'highschool') {
if ($user->hasRole(User::role_teacher)) {
$isTemporarilyGrantedAccess = $this->getParameter('abacus.systime.require_license') === 0;
if (!$isTemporarilyGrantedAccess && !$ssoProvider->canHighschoolTeacherAccess($user)) {
return $this->render('User/Security/nolicense_abacus.html.twig', [
'username' => $user->getUsername()
]);
}
}
}
return $this->loginUniUser($user);
}
/* Login existing user */
private function loginUniUser(User $user): RedirectResponse
{
$env = $this->getParameter('abacus_environment');
try {
if ($user->hasRole(User::role_student) && $env=="primaryschool") {
if (!is_numeric($user->getClassLevel())) {
$user->setClassLevel(9);
}
}
else if ($user->hasRole(User::role_student) && $env === "highschool") {
$this->textbookVoterService->updateAccess($user);
$this->selfStudyVoterService->updateAccess($user);
}
} catch (\Exception $e) {
}
$response = $this->redirectToRoute('login_redirect_route');
$this->loginManager->loginUser(DefaultFirewall::NAME, $user, $response);
$this->createDemoClassroom->create($user);
return $response;
}
public function uniLoginAction(): Response
{
return $this->render('User/Security/uni_login.html.twig');
}
private function persistUser($user)
{
$this->entityManager->persist($user);
$this->entityManager->flush();
}
private function shouldShowAlternativeLogin(?AuthenticationException $error): bool
{
$scope = $this->parameterBag->get('abacus.scope');
if ($scope === 'gale') {
// Never use UniLogin for Gale
return true;
}
if ($this->request->query->has("alternative_login")) {
return true;
}
// Failed alternative login attempt - keep using alternative login
if ($error !== null && $this->request->getSession()->get('login_method') === 'alternative') {
return true;
}
if ($error instanceof InvalidCsrfTokenException) {
// Csrf token should only be used for alternative login
return true;
}
// Trying to access /easyadmin and not logged in - use alternative login
if ($target = $this->getTargetPath($this->request->getSession(), DefaultFirewall::NAME)) {
$targetParts = parse_url($target);
if (str_starts_with($targetParts['path'] ?? '', '/easyadmin')) {
return true;
}
}
return false;
}
}