Completed
Push — master ( 009411...7fe99b )
by Rafał
18:49 queued 07:10
created

AuthController   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 22

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 22
dl 0
loc 223
ccs 25
cts 25
cp 1
rs 10
c 0
b 0
f 0
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\Model;
20
use Nelmio\ApiDocBundle\Annotation\Operation;
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\Form\Type\SuperdeskCredentialAuthenticationType;
26
use SWP\Bundle\CoreBundle\Form\Type\UserAuthenticationType;
27
use SWP\Bundle\CoreBundle\Model\ApiKeyInterface;
28
use SWP\Bundle\CoreBundle\Model\UserInterface;
29
use SWP\Bundle\CoreBundle\Repository\ApiKeyRepositoryInterface;
30
use SWP\Component\Common\Response\ResponseContext;
31
use SWP\Component\Common\Response\SingleResourceResponse;
32
use SWP\Component\Common\Response\SingleResourceResponseInterface;
33
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
34
use Symfony\Component\Form\FormFactoryInterface;
35
use Symfony\Component\HttpFoundation\Request;
36
use Symfony\Component\Lock\Factory;
37
use Symfony\Component\Routing\Annotation\Route;
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);
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' => <<<'MESSAGE'
183
Unauthorized (user not found in Superdesk). 
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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