Passed
Push — master ( b3dfcb...0033b2 )
by Yannick
10:56
created

SecurityController::checkSession()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\ExtraFieldValues;
10
use Chamilo\CoreBundle\Entity\Legal;
11
use Chamilo\CoreBundle\Repository\TrackELoginRecordRepository;
12
use Chamilo\CoreBundle\ServiceHelper\UserHelper;
13
use Chamilo\CoreBundle\Settings\SettingsManager;
14
use DateTime;
15
use Doctrine\ORM\EntityManager;
16
use Doctrine\ORM\EntityManagerInterface;
17
use OTPHP\TOTP;
18
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
19
use Symfony\Component\HttpFoundation\JsonResponse;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\Routing\Attribute\Route;
23
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
24
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
25
use Symfony\Component\Serializer\SerializerInterface;
26
use Symfony\Contracts\Translation\TranslatorInterface;
27
28
class SecurityController extends AbstractController
29
{
30
    public function __construct(
31
        private SerializerInterface $serializer,
32
        private TrackELoginRecordRepository $trackELoginRecordRepository,
33
        private EntityManagerInterface $entityManager,
34
        private SettingsManager $settingsManager,
35
        private TokenStorageInterface $tokenStorage,
36
        private AuthorizationCheckerInterface $authorizationChecker,
37
        private readonly UserHelper $userHelper,
38
    ) {}
39
40
    #[Route('/login_json', name: 'login_json', methods: ['POST'])]
41
    public function loginJson(Request $request, EntityManager $entityManager, SettingsManager $settingsManager, TokenStorageInterface $tokenStorage, TranslatorInterface $translator): Response
42
    {
43
        if (!$this->isGranted('IS_AUTHENTICATED_FULLY')) {
44
            return $this->json(
45
                [
46
                    'error' => 'Invalid login request: check that the Content-Type header is "application/json".',
47
                ],
48
                400
49
            );
50
        }
51
52
        $user = $this->userHelper->getCurrent();
53
54
        if (1 !== $user->getActive()) {
55
            if (0 === $user->getActive()) {
56
                $message = $translator->trans('Account not activated.');
57
            } else {
58
                $message = $translator->trans('Invalid credentials. Please try again or contact support if you continue to experience issues.');
59
            }
60
61
            $tokenStorage->setToken(null);
62
            $request->getSession()->invalidate();
63
64
            return $this->json(['error' => $message], 401);
65
        }
66
67
        if ($user->getMfaEnabled()) {
68
            $totpCode = null;
69
            $data = json_decode($request->getContent(), true);
70
            if (isset($data['totp'])) {
71
                $totpCode = $data['totp'];
72
            }
73
74
            if (null === $totpCode || !$this->isTOTPValid($user, $totpCode)) {
75
                $tokenStorage->setToken(null);
76
                $request->getSession()->invalidate();
77
78
                return $this->json([
79
                    'requires2FA' => true,
80
                ], 200);
81
            }
82
        }
83
84
        if (null !== $user->getExpirationDate() && $user->getExpirationDate() <= new DateTime()) {
85
            $message = $translator->trans('Your account has expired.');
86
87
            $tokenStorage->setToken(null);
88
            $request->getSession()->invalidate();
89
90
            return $this->json(['error' => $message], 401);
91
        }
92
93
        $extraFieldValuesRepository = $this->entityManager->getRepository(ExtraFieldValues::class);
94
        $legalTermsRepo = $this->entityManager->getRepository(Legal::class);
95
        if ($user->hasRole('ROLE_STUDENT')
96
            && 'true' === $this->settingsManager->getSetting('allow_terms_conditions')
97
            && 'login' === $this->settingsManager->getSetting('load_term_conditions_section')
98
        ) {
99
            $termAndConditionStatus = false;
100
            $extraValue = $extraFieldValuesRepository->findLegalAcceptByItemId($user->getId());
101
            if (!empty($extraValue['value'])) {
102
                $result = $extraValue['value'];
103
                $userConditions = explode(':', $result);
104
                $version = $userConditions[0];
105
                $langId = (int) $userConditions[1];
106
                $realVersion = $legalTermsRepo->getLastVersion($langId);
107
                $termAndConditionStatus = ($version >= $realVersion);
108
            }
109
110
            if (false === $termAndConditionStatus) {
111
                $tempTermAndCondition = ['user_id' => $user->getId()];
112
113
                $this->tokenStorage->setToken(null);
114
                $request->getSession()->invalidate();
115
116
                $request->getSession()->start();
117
                $request->getSession()->set('term_and_condition', $tempTermAndCondition);
118
119
                $responseData = [
120
                    'redirect' => '/main/auth/inscription.php',
121
                    'load_terms' => true,
122
                ];
123
124
                return new JsonResponse($responseData, Response::HTTP_OK);
125
            }
126
            $request->getSession()->remove('term_and_condition');
127
        }
128
129
        $data = null;
130
        if ($user) {
131
            $data = $this->serializer->serialize($user, 'jsonld', ['groups' => ['user_json:read']]);
132
        }
133
134
        return new JsonResponse($data, Response::HTTP_OK, [], true);
135
    }
136
137
    #[Route('/check-session', name: 'check_session', methods: ['GET'])]
138
    public function checkSession(): JsonResponse
139
    {
140
        if ($this->authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
141
            $user = $this->userHelper->getCurrent();
142
            $data = $this->serializer->serialize($user, 'jsonld', ['groups' => ['user_json:read']]);
143
144
            return new JsonResponse(['isAuthenticated' => true, 'user' => json_decode($data)], Response::HTTP_OK);
145
        }
146
147
        throw $this->createAccessDeniedException();
148
    }
149
150
    /**
151
     * Validates the provided TOTP code for the given user.
152
     */
153
    private function isTOTPValid($user, string $totpCode): bool
154
    {
155
        $decryptedSecret = $this->decryptTOTPSecret($user->getMfaSecret(), $_ENV['APP_SECRET']);
156
        $totp = TOTP::create($decryptedSecret);
157
158
        return $totp->verify($totpCode);
159
    }
160
161
    /**
162
     * Decrypts the stored TOTP secret.
163
     */
164
    private function decryptTOTPSecret(string $encryptedSecret, string $encryptionKey): string
165
    {
166
        $cipherMethod = 'aes-256-cbc';
167
168
        try {
169
            list($iv, $encryptedData) = explode('::', base64_decode($encryptedSecret), 2);
170
            $decryptedSecret = openssl_decrypt($encryptedData, $cipherMethod, $encryptionKey, 0, $iv);
171
172
            return $decryptedSecret;
173
        } catch (\Exception $e) {
174
            error_log("Exception caught during decryption: " . $e->getMessage());
175
            return '';
176
        }
177
    }
178
}
179