Completed
Pull Request — master (#267)
by Guilherme
04:53 queued 23s
created

SessionManagementController   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 372
rs 3.1913
c 0
b 0
f 0
wmc 66

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getClient() 0 7 1
A checkSessionAction() 0 3 1
F endSessionAction() 0 74 18
C unparseUrl() 0 14 11
B getOriginsAction() 0 43 3
A addStateToUri() 0 15 3
A getSecurityHelper() 0 6 1
A getLogoutClient() 0 10 3
A checkIdToken() 0 18 4
A getIdToken() 0 11 3
A shouldGetLogoutConsent() 0 12 4
A alwaysGetRedirectConsent() 0 3 1
A findClientByPostLogoutRedirectUri() 0 6 1
A getSubjectIdentifier() 0 6 1
A checkIdTokenSub() 0 16 3
A getIdTokenClient() 0 10 2
A alwaysGetLogoutConsent() 0 3 1
A validatePostLogoutUri() 0 16 3
A getPostLogoutInfo() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like SessionManagementController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SessionManagementController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the login-cidadao project or it's bundles.
4
 *
5
 * (c) Guilherme Donato <guilhermednt on github>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LoginCidadao\OpenIDBundle\Controller;
12
13
use LoginCidadao\CoreBundle\Helper\SecurityHelper;
14
use LoginCidadao\CoreBundle\Model\PersonInterface;
15
use LoginCidadao\OAuthBundle\Model\ClientInterface;
16
use LoginCidadao\OpenIDBundle\Entity\ClientMetadata;
17
use LoginCidadao\OpenIDBundle\Entity\ClientMetadataRepository;
18
use LoginCidadao\OpenIDBundle\Exception\IdTokenSubMismatchException;
19
use LoginCidadao\OpenIDBundle\Exception\IdTokenValidationException;
20
use LoginCidadao\OpenIDBundle\Form\EndSessionForm;
21
use LoginCidadao\OpenIDBundle\Service\SubjectIdentifierService;
22
use LoginCidadao\OpenIDBundle\Storage\PublicKey;
23
use Symfony\Component\HttpFoundation\Request;
24
use Symfony\Component\HttpFoundation\JsonResponse;
25
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
26
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
27
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
28
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
29
use Symfony\Component\HttpFoundation\Response;
30
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
31
32
/**
33
 * @Route("/openid/connect")
34
 * @codeCoverageIgnore
35
 */
36
class SessionManagementController extends Controller
37
{
38
39
    /**
40
     * @Route("/session/check", name="oidc_check_session_iframe")
41
     * @Method({"GET"})
42
     * @Template
43
     */
44
    public function checkSessionAction(Request $request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

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

44
    public function checkSessionAction(/** @scrutinizer ignore-unused */ Request $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
45
    {
46
        return array();
47
    }
48
49
    /**
50
     * @Route("/session/origins", name="oidc_get_origins")
51
     * @Method({"GET"})
52
     * @Template
53
     */
54
    public function getOriginsAction(Request $request)
55
    {
56
        $client = $this->getClient($request->get('client_id'));
57
58
        $uris = array();
59
        $uris[] = $client->getSiteUrl();
60
        $uris[] = $client->getTermsOfUseUrl();
61
        $uris[] = $client->getLandingPageUrl();
62
        if ($client->getMetadata()) {
63
            $meta = $client->getMetadata();
64
            $uris[] = $meta->getClientUri();
65
            $uris[] = $meta->getInitiateLoginUri();
66
            $uris[] = $meta->getPolicyUri();
67
            $uris[] = $meta->getSectorIdentifierUri();
68
            $uris[] = $meta->getTosUri();
69
            $uris = array_merge(
70
                $uris,
71
                $meta->getRedirectUris(),
72
                $meta->getRequestUris()
73
            );
74
        }
75
76
        $result = array_unique(
77
            array_map(
78
                function ($value) {
79
                    if ($value === null) {
80
                        return;
81
                    }
82
                    $uri = parse_url($value);
83
84
                    $uri['fragment'] = '';
85
                    $uri['path'] = '';
86
                    $uri['query'] = '';
87
                    $uri['user'] = '';
88
                    $uri['pass'] = '';
89
90
                    return $this->unparseUrl($uri);
91
                },
92
                array_filter($uris)
93
            )
94
        );
95
96
        return new JsonResponse(array_values($result));
97
    }
98
99
    /**
100
     * @Route("/session/end", name="oidc_end_session_endpoint")
101
     * @Template
102
     */
103
    public function endSessionAction(Request $request)
104
    {
105
        $alwaysGetRedirectConsent = $this->alwaysGetRedirectConsent();
106
107
        $view = 'LoginCidadaoOpenIDBundle:SessionManagement:endSession.html.twig';
108
        $finishedView = 'LoginCidadaoOpenIDBundle:SessionManagement:endSession.finished.html.twig';
109
        try {
110
            $idToken = $request->get('id_token_hint');
111
            $postLogoutUri = $request->get('post_logout_redirect_uri', null);
112
            $loggedOut = !$this->isGranted('IS_AUTHENTICATED_REMEMBERED');
113
            try {
114
                $getLogoutConsent = $this->shouldGetLogoutConsent($idToken, $loggedOut);
115
            } catch (IdTokenSubMismatchException $e) {
116
                $getLogoutConsent = true;
117
            }
118
119
            list($postLogoutUri, $postLogoutHost) = $this->getPostLogoutInfo($request, $postLogoutUri, $idToken);
120
        } catch (IdTokenValidationException $e) {
121
            return $this->render($finishedView, ['error' => 'openid.session.end.invalid_id_token']);
122
        }
123
124
        $getRedirectConsent = $alwaysGetRedirectConsent && $postLogoutUri;
125
        $authorizedRedirect = !$getLogoutConsent && !$getRedirectConsent;
126
        $authorizedLogout = !$getLogoutConsent;
127
        $formChecked = false;
128
129
        $form = $this->createForm(
130
            new EndSessionForm(),
131
            ['logout' => true, 'redirect' => true],
132
            [
133
                'getLogoutConsent' => $getLogoutConsent,
134
                'getRedirectConsent' => $getRedirectConsent,
135
            ]
136
        );
137
        $form->handleRequest($request);
138
        if ($form->isValid()) {
139
            $data = $form->getData();
140
141
            $authorizedRedirect = false === $getRedirectConsent || $data['redirect'];
142
            $authorizedLogout = false === $getLogoutConsent || $data['logout'];
143
            $formChecked = true;
144
        }
145
146
        $params = [
147
            'form' => $form->createView(),
148
            'client' => $this->getLogoutClient($idToken),
149
            'postLogoutUri' => $postLogoutUri,
150
            'postLogoutHost' => $postLogoutHost,
151
            'getLogoutConsent' => $getLogoutConsent,
152
            'getRedirectConsent' => $getRedirectConsent,
153
            'loggedOut' => $loggedOut,
154
        ];
155
156
        if (($getLogoutConsent || $getRedirectConsent)
157
            && !$authorizedRedirect
158
            && $formChecked
159
        ) {
160
            $view = $finishedView;
161
        }
162
163
        $response = null;
164
        if ($postLogoutUri && $authorizedRedirect) {
165
            $response = $this->redirect($postLogoutUri);
166
        }
167
168
        if ($authorizedLogout && !$loggedOut) {
169
            if (!$response) {
170
                $params['loggedOut'] = true;
171
                $response = $this->render($view, $params);
172
            }
173
            $response = $this->getSecurityHelper()->logout($request, $response);
174
        }
175
176
        return $response ?: $this->render($view, $params);
177
    }
178
179
    /**
180
     * @param string $clientId
181
     * @return \LoginCidadao\OAuthBundle\Entity\Client|object
182
     */
183
    private function getClient($clientId)
184
    {
185
        $clientId = explode('_', $clientId);
186
        $id = $clientId[0];
187
188
        return $this->getDoctrine()->getManager()
189
            ->getRepository('LoginCidadaoOAuthBundle:Client')->find($id);
190
    }
191
192
    private function unparseUrl($parsed_url)
193
    {
194
        $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'].'://' : '';
195
        $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
196
        $port = isset($parsed_url['port']) ? ':'.$parsed_url['port'] : '';
197
        $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
198
        $pass = isset($parsed_url['pass']) ? ':'.$parsed_url['pass'] : '';
199
        $pass = ($user || $pass) ? "$pass@" : '';
200
        $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
201
        $query = isset($parsed_url['query']) ? '?'.$parsed_url['query'] : '';
202
        $fragment = isset($parsed_url['fragment']) ? '#'.$parsed_url['fragment']
203
            : '';
204
205
        return "$scheme$user$pass$host$port$path$query$fragment";
206
    }
207
208
    /**
209
     * @param mixed $idToken a JWT ID Token as a \JOSE_JWT object or string
210
     * @return bool true if $idToken is valid, false otherwise
211
     * @throws IdTokenSubMismatchException
212
     * @throws IdTokenValidationException
213
     */
214
    private function checkIdToken($idToken)
215
    {
216
        $idToken = $this->getIdToken($idToken);
217
218
        /** @var PublicKey $publicKeyStorage */
219
        $publicKeyStorage = $this->get('oauth2.storage.public_key');
220
        try {
221
            $idToken->verify($publicKeyStorage->getPublicKey($idToken->claims['aud']));
222
223
            if (false === $this->checkIdTokenSub($this->getUser(), $idToken)) {
224
                throw new IdTokenSubMismatchException('Invalid subject identifier', Response::HTTP_BAD_REQUEST);
225
            }
226
227
            return true;
228
        } catch (IdTokenSubMismatchException $e) {
229
            throw $e;
230
        } catch (\JOSE_Exception_VerificationFailed|\Exception $e) {
231
            throw new IdTokenValidationException($e->getMessage(), Response::HTTP_BAD_REQUEST, $e);
232
        }
233
    }
234
235
    /**
236
     * @param PersonInterface $person
237
     * @param mixed $idToken
238
     * @return bool
239
     */
240
    private function checkIdTokenSub(PersonInterface $person = null, $idToken)
241
    {
242
        if (null === $person) {
243
            // User is logged out
244
            return true;
245
        }
246
247
        if (!($person instanceof PersonInterface)) {
0 ignored issues
show
introduced by
$person is always a sub-type of LoginCidadao\CoreBundle\Model\PersonInterface.
Loading history...
248
            return false;
249
        }
250
251
        $client = $this->getClient($idToken->claims['aud']);
252
253
        $sub = $this->getSubjectIdentifier($person, $client);
254
255
        return $idToken->claims['sub'] == $sub;
256
    }
257
258
    /**
259
     * Enforces that the ID Token is a \JOSE_JWT object
260
     * @param mixed $idToken
261
     * @return \JOSE_JWE|\JOSE_JWT
262
     */
263
    private function getIdToken($idToken)
264
    {
265
        if (!($idToken instanceof \JOSE_JWT)) {
266
            try {
267
                $idToken = \JOSE_JWT::decode($idToken);
268
            } catch (\JOSE_Exception_InvalidFormat $e) {
269
                throw new BadRequestHttpException($e->getMessage(), $e);
270
            }
271
        }
272
273
        return $idToken;
274
    }
275
276
    private function validatePostLogoutUri($postLogoutUri, $idToken)
277
    {
278
        if ($postLogoutUri === null) {
279
            return false;
280
        }
281
282
        $postLogoutUri = ClientMetadata::canonicalizeUri($postLogoutUri);
283
284
        if (!$idToken) {
285
            return count($this->findClientByPostLogoutRedirectUri($postLogoutUri)) > 0;
286
        }
287
288
        $idToken = $this->getIdToken($idToken);
289
        $client = $this->getClient($idToken->claims['aud']);
290
291
        return false !== array_search($postLogoutUri, $client->getMetadata()->getPostLogoutRedirectUris());
292
    }
293
294
    private function addStateToUri($postLogoutUri, $state)
295
    {
296
        if ($state) {
297
            $url = parse_url($postLogoutUri);
298
            if (array_key_exists('query', $url)) {
299
                parse_str($url['query'], $query);
300
            } else {
301
                $query = [];
302
            }
303
            $query['state'] = $state;
304
            $url['query'] = http_build_query($query);
305
306
            return $this->unparseUrl($url);
307
        } else {
308
            return $postLogoutUri;
309
        }
310
    }
311
312
    /**
313
     * @return bool
314
     */
315
    private function alwaysGetLogoutConsent()
316
    {
317
        return $this->getParameter('rp_initiated_logout.logout.always_get_consent');
318
    }
319
320
    /**
321
     * @return bool
322
     */
323
    private function alwaysGetRedirectConsent()
324
    {
325
        return $this->getParameter('rp_initiated_logout.redirect.always_get_consent');
326
    }
327
328
    /**
329
     * @param string|\JOSE_JWT $idToken
330
     * @return \LoginCidadao\OAuthBundle\Entity\Client|false
331
     */
332
    private function getIdTokenClient($idToken)
333
    {
334
        if ($idToken === null) {
335
            return false;
336
        }
337
338
        $idToken = $this->getIdToken($idToken);
339
        $client = $this->getClient($idToken->claims['aud']);
340
341
        return $client;
342
    }
343
344
    /**
345
     * @return SecurityHelper
346
     */
347
    private function getSecurityHelper()
348
    {
349
        /** @var SecurityHelper $securityHelper */
350
        $securityHelper = $this->get('lc.security.helper');
351
352
        return $securityHelper;
353
    }
354
355
    private function getSubjectIdentifier(PersonInterface $person, ClientInterface $client)
356
    {
357
        /** @var SubjectIdentifierService $service */
358
        $service = $this->get('oidc.subject_identifier.service');
359
360
        return $service->getSubjectIdentifier($person, $client->getMetadata());
361
    }
362
363
    private function findClientByPostLogoutRedirectUri($postLogoutUri)
364
    {
365
        /** @var ClientMetadataRepository $repo */
366
        $repo = $this->get('oidc.client_metadata.repository');
367
368
        return $repo->findByPostLogoutRedirectUri($postLogoutUri);
369
    }
370
371
    private function shouldGetLogoutConsent($idToken, $loggedOut)
372
    {
373
        $getLogoutConsent = $loggedOut ? false : $this->alwaysGetLogoutConsent();
374
375
        if ($idToken) {
376
            if (false === $this->checkIdToken($idToken)) {
0 ignored issues
show
introduced by
The condition false === $this->checkIdToken($idToken) is always false.
Loading history...
377
                // We didn't receive a valid ID Token, therefore we should ask user for consent
378
                $getLogoutConsent = true;
379
            }
380
        }
381
382
        return $getLogoutConsent;
383
    }
384
385
    private function getLogoutClient($idToken)
386
    {
387
        $client = null;
388
        if ($idToken) {
389
            if ($this->checkIdToken($idToken)) {
390
                $client = $this->getIdTokenClient($idToken);
391
            }
392
        }
393
394
        return $client;
395
    }
396
397
    private function getPostLogoutInfo(Request $request, $postLogoutUri, $idToken)
398
    {
399
        $postLogoutHost = null;
400
        if ($this->validatePostLogoutUri($postLogoutUri, $idToken)) {
401
            $postLogoutUri = $this->addStateToUri($postLogoutUri, $request->get('state', null));
402
            $postLogoutHost = parse_url($postLogoutUri)['host'];
403
        } else {
404
            $postLogoutUri = null;
405
        }
406
407
        return [$postLogoutUri, $postLogoutHost];
408
    }
409
}
410