<?php
namespace UserBundle\Controller;
use CoreBundle\Security\SSO\Exception\NoTokenException;
use CoreBundle\Service\AuthService;
use CoreBundle\Service\CreateDemoClassroom;
use CoreBundle\Service\EventLogger;
use CoreBundle\Service\SelfStudyVoterService;
use CoreBundle\Service\TextbookVoterService;
use Doctrine\ORM\EntityManagerInterface;
use FOS\UserBundle\Controller\SecurityController as BaseController;
use FOS\UserBundle\Security\LoginManagerInterface;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
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\Util\TargetPathTrait;
use UserBundle\Entity\User;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class SecurityController extends BaseController
{
use TargetPathTrait;
private EntityManagerInterface $em;
private EventLogger $eventLogger;
private TextbookVoterService $textbookVoterService;
private SelfStudyVoterService $selfStudyVoterService;
private LoginManagerInterface $loginManager;
private CreateDemoClassroom $createDemoClassroom;
private AuthService $authService;
private CsrfTokenManagerInterface $tokenManager;
private Request $request;
private RequestStack $requestStack;
private ParameterBagInterface $parameterBag;
public function __construct(
EntityManagerInterface $em,
EventLogger $eventLogger,
TextbookVoterService $textbookVoterService,
SelfStudyVoterService $selfStudyVoterService,
LoginManagerInterface $loginManager,
CreateDemoClassroom $createDemoClassroom,
AuthService $authService,
CsrfTokenManagerInterface $tokenManager,
RequestStack $requestStack,
AuthenticationUtils $authenticationUtils,
ParameterBagInterface $parameterBag
)
{
parent::__construct($authenticationUtils);
$this->em = $em;
$this->eventLogger = $eventLogger;
$this->textbookVoterService = $textbookVoterService;
$this->selfStudyVoterService = $selfStudyVoterService;
$this->loginManager = $loginManager;
$this->createDemoClassroom = $createDemoClassroom;
$this->authService = $authService;
$this->tokenManager = $tokenManager;
$this->requestStack = $requestStack;
$this->parameterBag = $parameterBag;
}
// route : /cms or /testlogin redirects to /login?alternative_login=1
public function alternativeLogin(Request $request): RedirectResponse
{
$request->getSession()->set('login_method', 'alternative');
return $this->redirectToRoute("fos_user_security_login", [
"alternative_login" => true,
]);
}
// route /login (fos_user_security_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);
}
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
$user = $ssoProvider->findUserIfExists();
if ($user instanceof User) {
/* Existing user */
/* Update institution and role */
$user = $ssoProvider->updateUser($user);
$user->setLastLoginWithUnilogin(new \DateTime()); // save timestamp
$this->eventLogger->log('login_' . $_SERVER['HTTP_USER_AGENT'], $user);
}
else {
/* New 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('main', $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->em->persist($user);
$this->em->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(), 'main')) {
$targetParts = parse_url($target);
if (str_starts_with($targetParts['path'] ?? '', '/easyadmin')) {
return true;
}
}
return false;
}
}