Passed
Push — master ( 37676a...b0c137 )
by Angel Fernando Quiroz
13:06 queued 02:44
created

LdapAuthenticator::supports()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 7
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 12
rs 9.6111
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
declare(strict_types=1);
6
7
namespace Chamilo\CoreBundle\Security\Authenticator\Ldap;
8
9
use Chamilo\CoreBundle\Controller\SecurityController;
10
use Chamilo\CoreBundle\Entity\User;
11
use Chamilo\CoreBundle\Helpers\AccessUrlHelper;
12
use Chamilo\CoreBundle\Helpers\AuthenticationConfigHelper;
13
use Chamilo\CoreBundle\Repository\Node\UserRepository;
14
use Doctrine\ORM\EntityManagerInterface;
15
use stdClass;
16
use Symfony\Component\HttpFoundation\JsonResponse;
17
use Symfony\Component\HttpFoundation\Request;
18
use Symfony\Component\HttpFoundation\Response;
19
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
20
use Symfony\Component\Ldap\Ldap;
21
use Symfony\Component\Ldap\Security\LdapBadge;
22
use Symfony\Component\Ldap\Security\LdapUser;
23
use Symfony\Component\Ldap\Security\LdapUserProvider;
24
use Symfony\Component\PropertyAccess\Exception\AccessException;
25
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
26
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
27
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
28
use Symfony\Component\Security\Core\Exception\AuthenticationException;
29
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
30
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
31
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
32
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
33
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
34
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
35
use Symfony\Contracts\Translation\TranslatorInterface;
36
37
/**
38
 * Based on Symfony\Component\Ldap\Security\LdapAuthenticator.
39
 */
40
class LdapAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
41
{
42
    private LdapBadge $ldapBadge;
43
    private LdapUserProvider $userProvider;
44
45
    private array $dataCorrespondence = [];
46
47
    private bool $isEnabled = false;
48
49
    public function __construct(
50
        protected readonly AuthenticationConfigHelper $authConfigHelper,
51
        private readonly AccessUrlHelper $accessUrlHelper,
52
        private readonly UserRepository $userRepo,
53
        private readonly EntityManagerInterface $entityManager,
54
        private readonly SecurityController $securityController,
55
        private readonly TokenStorageInterface $tokenStorage,
56
        private readonly TranslatorInterface $translator,
57
        Ldap $ldap,
58
    ) {
59
        $params = $this->authConfigHelper->getLdapConfig(
60
            $this->accessUrlHelper->getCurrent()
61
        );
62
63
        if (!$params['enabled']) {
64
            return;
65
        }
66
67
        $this->isEnabled = true;
68
69
        $dnString = $params['dn_string'] ?? '{user_identifier}';
70
        $searchDn = $params['search_dn'] ?? '';
71
        $searchPassword = $params['search_password'] ?? '';
72
        $queryString = $params['query_string'] ?? null;
73
74
        $this->dataCorrespondence = array_filter($params['data_correspondence']) ?: [];
75
76
        $this->ldapBadge = new LdapBadge(Ldap::class, $dnString, $searchDn, $searchPassword, $queryString);
77
78
        $this->userProvider = new LdapUserProvider(
79
            $ldap,
80
            $params['base_dn'],
81
            $searchDn ?: '',
82
            $searchPassword ?: null,
83
            ['ROLE_STUDENT'],
84
            $params['uid_key'] ?? null,
85
            $params['filter'] ?? null,
86
            $params['password_attribute'] ?? null,
87
            array_values($this->dataCorrespondence + [$params['password_attribute']]),
88
        );
89
    }
90
91
    public function supports(Request $request): ?bool
92
    {
93
        if (
94
            !str_contains($request->getRequestFormat() ?? '', 'json')
95
            && !str_contains($request->getContentTypeFormat() ?? '', 'json')
96
        ) {
97
            return false;
98
        }
99
100
        return 'login_ldap_check' === $request->attributes->get('_route')
101
            && $request->headers->has('Authorization')
102
            && $request->isMethod('POST');
103
    }
104
105
    public function authenticate(Request $request): Passport
106
    {
107
        try {
108
            if (!$this->isEnabled) {
109
                throw new BadRequestHttpException('Authentication method not enabled.');
110
            }
111
112
            $data = json_decode($request->getContent());
113
114
            if (!$data instanceof stdClass) {
115
                throw new BadRequestHttpException('Invalid JSON.');
116
            }
117
118
            $credentials = $this->getCredentials($data);
119
        } catch (BadRequestHttpException $e) {
120
            $request->setRequestFormat('json');
121
122
            throw $e;
123
        }
124
125
        $userBadge = new UserBadge($credentials['username'], $this->userProvider->loadUserByIdentifier(...));
126
127
        $passport = new Passport(
128
            $userBadge,
129
            new PasswordCredentials($credentials['password']),
130
            [new RememberMeBadge((array) $data)]
131
        );
132
        // $passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
133
        $passport->addBadge($this->ldapBadge);
134
135
        return $passport;
136
    }
137
138
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
139
    {
140
        return $this->securityController->loginJson(
141
            $request,
142
            $this->tokenStorage,
143
            $this->translator
144
        );
145
    }
146
147
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
148
    {
149
        $errorMessage = strtr($exception->getMessageKey(), $exception->getMessageData());
150
151
        return new JsonResponse(['error' => $errorMessage], Response::HTTP_UNAUTHORIZED);
152
    }
153
154
    public function createToken(Passport $passport, string $firewallName): TokenInterface
155
    {
156
        /** @var LdapUser $ldapUser */
157
        $ldapUser = $passport->getUser();
158
159
        $user = $this->createUser($ldapUser);
160
161
        return new UsernamePasswordToken(
162
            $user,
163
            $firewallName,
164
            $user->getRoles()
165
        );
166
    }
167
168
    private function getCredentials(stdClass $data): array
169
    {
170
        $credentials = [];
171
172
        try {
173
            $credentials['username'] = $data->username;
174
175
            if (!\is_string($credentials['username'])) {
176
                throw new BadRequestHttpException(\sprintf('The key "%s" must be a string.', 'username'));
177
            }
178
        } catch (AccessException $e) {
179
            throw new BadRequestHttpException(\sprintf('The key "%s" must be provided.', 'username'), $e);
180
        }
181
182
        try {
183
            $credentials['password'] = $data->password;
184
            $data->password = null;
185
186
            if (!\is_string($credentials['password'])) {
187
                throw new BadRequestHttpException(\sprintf('The key "%s" must be a string.', 'password'));
188
            }
189
        } catch (AccessException $e) {
190
            throw new BadRequestHttpException(\sprintf('The key "%s" must be provided.', 'password'), $e);
191
        }
192
193
        if ('' === $credentials['username'] || '' === $credentials['password']) {
194
            trigger_deprecation('symfony/security', '6.2', 'Passing an empty string as username or password parameter is deprecated.');
0 ignored issues
show
Bug introduced by
The function trigger_deprecation was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

194
            /** @scrutinizer ignore-call */ 
195
            trigger_deprecation('symfony/security', '6.2', 'Passing an empty string as username or password parameter is deprecated.');
Loading history...
195
        }
196
197
        return $credentials;
198
    }
199
200
    private function createUser(LdapUser $ldapUser): User
201
    {
202
        $currentAccessUrl = $this->accessUrlHelper->getCurrent();
203
204
        $user = $this->userRepo->findOneBy(['username' => $ldapUser->getUserIdentifier()]);
205
206
        if (!$user) {
207
            $user = (new User())
208
                ->setCreatorId($this->userRepo->getRootUser()->getId())
209
            ;
210
        }
211
212
        $ldapFields = $ldapUser->getExtraFields();
213
214
        if (isset($this->dataCorrespondence['firstname'])) {
215
            $user->setFirstname(
216
                $ldapFields[$this->dataCorrespondence['firstname']][0]
217
            );
218
        } else {
219
            $user->setFirstname('');
220
        }
221
222
        if (isset($this->dataCorrespondence['lastname'])) {
223
            $user->setLastname(
224
                $ldapFields[$this->dataCorrespondence['lastname']][0]
225
            );
226
        } else {
227
            $user->setLastname('');
228
        }
229
230
        if (isset($this->dataCorrespondence['email'])) {
231
            $user->setEmail(
232
                $ldapFields[$this->dataCorrespondence['email']][0]
233
            );
234
        } else {
235
            $user->setEmail('');
236
        }
237
238
        if (isset($this->dataCorrespondence['active'])) {
239
            $user->setActive(
240
                (int) $ldapFields[$this->dataCorrespondence['active']][0]
241
            );
242
        }
243
244
        if (isset($this->dataCorrespondence['role'])) {
245
            $user->setRoles(
246
                [$ldapFields[$this->dataCorrespondence['role']][0]]
247
            );
248
        } else {
249
            $user->setRoles($ldapUser->getRoles());
250
        }
251
252
        if (isset($this->dataCorrespondence['locale'])) {
253
            $user->setLocale(
254
                $ldapFields[$this->dataCorrespondence['locale']][0]
255
            );
256
        }
257
258
        if (isset($this->dataCorrespondence['phone'])) {
259
            $user->setPhone($this->dataCorrespondence['phone']);
260
        }
261
262
        $user
263
            ->setUsername($ldapUser->getUserIdentifier())
264
            ->setPlainPassword($ldapUser->getPassword())
265
            ->addAuthSourceByAuthentication('extldap', $currentAccessUrl)
266
        ;
267
268
        $this->userRepo->updateUser($user);
269
270
        $currentAccessUrl->addUser($user);
271
272
        $this->entityManager->flush();
273
274
        return $user;
275
    }
276
277
    public function isInteractive(): bool
278
    {
279
        return true;
280
    }
281
}
282