Completed
Push — master ( bfc3f0...8fffc5 )
by Paweł
21:06 queued 08:01
created

AuthController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
ccs 6
cts 6
cp 1
cc 1
nc 1
nop 4
crap 1
1
<?php
2
3
/*
4
 * This file is part of the Superdesk Web Publisher Core Bundle.
5
 *
6
 * Copyright 2016 Sourcefabric z.ú. and contributors.
7
 *
8
 * For the full copyright and license information, please see the
9
 * AUTHORS and LICENSE files distributed with this source code.
10
 *
11
 * @copyright 2016 Sourcefabric z.ú
12
 * @license http://www.superdesk.org/license
13
 */
14
15
namespace SWP\Bundle\CoreBundle\Controller;
16
17
use FOS\UserBundle\Model\UserManagerInterface;
18
use GuzzleHttp;
19
use Nelmio\ApiDocBundle\Annotation\Operation;
20
use Nelmio\ApiDocBundle\Annotation\Model;
21
use Psr\Log\LoggerInterface;
22
use RuntimeException;
23
use Swagger\Annotations as SWG;
24
use SWP\Bundle\CoreBundle\Factory\ApiKeyFactory;
25
use SWP\Bundle\CoreBundle\Repository\ApiKeyRepositoryInterface;
26
use SWP\Component\Common\Response\SingleResourceResponseInterface;
27
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
28
use Symfony\Component\Form\FormFactoryInterface;
29
use Symfony\Component\Lock\Factory;
30
use Symfony\Component\Routing\Annotation\Route;
31
use SWP\Bundle\CoreBundle\Form\Type\SuperdeskCredentialAuthenticationType;
32
use SWP\Bundle\CoreBundle\Form\Type\UserAuthenticationType;
33
use SWP\Bundle\CoreBundle\Model\ApiKeyInterface;
34
use SWP\Bundle\CoreBundle\Model\UserInterface;
35
use SWP\Component\Common\Response\ResponseContext;
36
use SWP\Component\Common\Response\SingleResourceResponse;
37
use Symfony\Component\HttpFoundation\Request;
38
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
39
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
40
use Symfony\Component\Security\Core\User\UserProviderInterface;
41
42
class AuthController extends AbstractController
43
{
44
    protected $formFactory;
45
46 2
    protected $apiKeyRepository;
47
48 2
    protected $apiKeyFactory;
49 2
50 2
    protected $lockFactory;
51 2
52
    public function __construct(
53 2
        FormFactoryInterface $formFactory,
54 1
        ApiKeyRepositoryInterface $apiKeyRepository,
55 1
        ApiKeyFactory $apiKeyFactory,
56
        Factory $lockFactory
57
    ) {
58 2
        $this->formFactory = $formFactory;
59 1
        $this->apiKeyRepository = $apiKeyRepository;
60 1
        $this->apiKeyFactory = $apiKeyFactory;
61
        $this->lockFactory = $lockFactory;
62
    }
63
64
    /**
65 1
     * @Operation(
66 1
     *     tags={"auth"},
67
     *     summary="Look for user matching provided credentials",
68 1
     *     @SWG\Parameter(
69
     *         name="body",
70
     *         in="body",
71
     *         description="",
72
     *         @SWG\Schema(
73
     *             ref=@Model(type=UserAuthenticationType::class)
74
     *         )
75
     *     ),
76
     *     @SWG\Response(
77
     *         response="200",
78
     *         description="Returned on success.",
79
     *         @Model(type=\SWP\Bundle\CoreBundle\Model\User::class, groups={"api"})
80
     *     ),
81
     *     @SWG\Response(
82
     *         response="401",
83
     *         description="No user found or not authorized."
84
     *     )
85
     * )
86
     *
87
     * @Route("/api/{version}/auth/", options={"expose"=true}, defaults={"version"="v2"}, methods={"POST"}, name="swp_api_auth")
88
     */
89
    public function authenticateAction(Request $request, UserProviderInterface $userProvider, UserPasswordEncoderInterface $userPasswordEncoder)
90
    {
91
        $form = $this->formFactory->createNamed('', UserAuthenticationType::class, []);
92
        $form->handleRequest($request);
93
        if ($form->isSubmitted() && $form->isValid()) {
94
            $formData = $form->getData();
95
96
            try {
97
                $user = $userProvider->loadUserByUsername($formData['username']);
98
            } catch (UsernameNotFoundException $e) {
99
                $user = null;
100
            }
101
102
            if ((null !== $user) && $userPasswordEncoder->isPasswordValid($user, $formData['password'])) {
103
                return $this->returnApiTokenResponse($user);
0 ignored issues
show
Compatibility introduced by
$user of type object<Symfony\Component...ore\User\UserInterface> is not a sub-type of object<SWP\Bundle\CoreBundle\Model\UserInterface>. It seems like you assume a child interface of the interface Symfony\Component\Security\Core\User\UserInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
104
            }
105
        }
106
107
        return new SingleResourceResponse([
108
            'status' => 401,
109
            'message' => 'Unauthorized',
110
        ], new ResponseContext(401));
111
    }
112
113
    /**
114
     * @Operation(
115
     *     tags={"auth"},
116
     *     summary="Authorize using Superdesk credentials",
117
     *     @SWG\Parameter(
118
     *         name="body",
119
     *         in="body",
120
     *         description="",
121
     *         @SWG\Schema(
122
     *             ref=@Model(type=SuperdeskCredentialAuthenticationType::class)
123
     *         )
124
     *     ),
125
     *     @SWG\Response(
126
     *         response="200",
127
     *         description="Returned on success.",
128
     *         @Model(type=\SWP\Bundle\CoreBundle\Model\User::class, groups={"api"})
129
     *     ),
130
     *     @SWG\Response(
131
     *         response="401",
132
     *         description="No user found or not authorized."
133
     *     )
134
     * )
135
     *
136
     * @Route("/api/{version}/auth/superdesk/", options={"expose"=true}, methods={"POST"}, defaults={"version"="v2"}, name="swp_api_auth_superdesk")
137 1
     */
138
    public function authenticateWithSuperdeskAction(
139 1
        Request $request,
140 1
        LoggerInterface $logger,
141 1
        array $superdeskServers,
142
        UserProviderInterface $userProvider,
143
        UserManagerInterface $userManager
144
    ) {
145
        $form = $this->formFactory->createNamed('', SuperdeskCredentialAuthenticationType::class, []);
146
        $form->handleRequest($request);
147
        if ($form->isSubmitted() && $form->isValid()) {
148 1
            $formData = $form->getData();
149 1
            $authorizedSuperdeskHosts = $superdeskServers;
150 1
            $superdeskUser = null;
151
            $client = new GuzzleHttp\Client();
152
153 1
            foreach ($authorizedSuperdeskHosts as $baseUrl) {
154
                try {
155 1
                    $apiRequest = new GuzzleHttp\Psr7\Request('GET', sprintf('%s/api/sessions/%s', $baseUrl, $formData['sessionId']), [
156 1
                        'Authorization' => $formData['token'],
157
                    ]);
158 1
159
                    $apiResponse = $client->send($apiRequest);
160
                    if (200 !== $apiResponse->getStatusCode()) {
161
                        $logger->warning(sprintf('[%s] Unsuccessful response from Superdesk Server: %s', $apiResponse->getStatusCode(), $apiResponse->getBody()->getContents()));
162
163
                        continue;
164
                    }
165
166
                    $content = json_decode($apiResponse->getBody()->getContents(), true);
167
                    if (is_array($content) && array_key_exists('user', $content)) {
168
                        $superdeskUser = $content['user'];
169
170
                        break;
171
                    }
172
                } catch (GuzzleHttp\Exception\ClientException $e) {
173
                    $logger->warning(sprintf('Error when logging in Superdesk: %s', $e->getMessage()));
174
175
                    continue;
176
                }
177
            }
178
179
            if (null === $superdeskUser) {
180
                return new SingleResourceResponse([
181
                    'status' => 401,
182
                    'message' => 'Unauthorized (user not found in Superdesk)',
183
                ], new ResponseContext(401));
184
            }
185
186
            $publisherUser = $userProvider->findOneByEmail($superdeskUser['email']);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Symfony\Component\Securi...r\UserProviderInterface as the method findOneByEmail() does only exist in the following implementations of said interface: SWP\Bundle\CoreBundle\Se...y\Provider\UserProvider.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
187
            if (null === $publisherUser) {
188
                try {
189
                    $publisherUser = $userProvider->loadUserByUsername($superdeskUser['username']);
190
                } catch (UsernameNotFoundException $e) {
191
                    $publisherUser = null;
192
                }
193
            }
194
195
            if (null === $publisherUser) {
196
                /** @var UserInterface $publisherUser */
197
                $publisherUser = $userManager->createUser();
198
                $publisherUser->setUsername($superdeskUser['username']);
199
                $publisherUser->setEmail($superdeskUser['email']);
200
                $publisherUser->setRoles(['ROLE_INTERNAL_API']);
201
                $publisherUser->setFirstName(\array_key_exists('first_name', $superdeskUser) ? $superdeskUser['first_name'] : 'Anon.');
202
                $publisherUser->setLastName(\array_key_exists('last_name', $superdeskUser) ? $superdeskUser['last_name'] : '');
203
                $publisherUser->setPlainPassword(password_hash(random_bytes(36), PASSWORD_BCRYPT));
204
                $publisherUser->setEnabled(true);
205
                $userManager->updateUser($publisherUser);
206
            }
207
208
            if (null !== $publisherUser) {
209
                return $this->returnApiTokenResponse($publisherUser, str_replace('Basic ', '', $formData['token']));
210
            }
211
        }
212
213
        return new SingleResourceResponse([
214
            'status' => 401,
215
            'message' => 'Unauthorized',
216
        ], new ResponseContext(401));
217
    }
218
219
    private function returnApiTokenResponse(UserInterface $user, string $token = null): SingleResourceResponseInterface
220
    {
221
        /** @var ApiKeyInterface $apiKey */
222
        $apiKey = $this->generateOrGetApiKey($user, $token);
223
224
        return new SingleResourceResponse([
225
            'token' => [
226
                'api_key' => $apiKey->getApiKey(),
227
                'valid_to' => $apiKey->getValidTo(),
228
            ],
229
            'user' => $user,
230
        ]);
231
    }
232
233
    private function generateOrGetApiKey(UserInterface $user, $token): ?ApiKeyInterface
234
    {
235
        $apiKey = null;
236
        if (null !== $token) {
237
            $apiKey = $this->apiKeyRepository->getValidToken($token)->getQuery()->getOneOrNullResult();
238
        } else {
239
            $validKeys = $this->apiKeyRepository->getValidTokenForUser($user)->getQuery()->getResult();
240
            if (count($validKeys) > 0) {
241
                $apiKey = reset($validKeys);
242
            }
243
        }
244
245
        if (null === $apiKey) {
246
            $apiKey = $this->apiKeyFactory->create($user, $token);
247
248
            try {
249
                $lock = $this->lockFactory->createLock(md5(json_encode(['type' => 'user_api_key', 'user' => $user->getId()])), 2);
250
                if (!$lock->acquire()) {
251
                    throw new RuntimeException('Other api key is created right now for this user');
252
                }
253
                $this->apiKeyRepository->add($apiKey);
254
                $lock->release();
255
            } catch (RuntimeException $e) {
256
                sleep(2);
257
258
                return $this->generateOrGetApiKey($user, $token);
259
            }
260
        }
261
262
        return $apiKey;
263
    }
264
}
265