Passed
Push — master ( 785c70...b3d31e )
by Angel Fernando Quiroz
09:13
created

LdapAuthenticator::getUserProvider()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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