Failed Conditions
Branch issue#666 (91903a)
by Guilherme
08:25
created

alwaysGetRedirectConsent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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
 */
35
class SessionManagementController extends Controller
36
{
37
38
    /**
39
     * @Route("/session/check", name="oidc_check_session_iframe")
40
     * @Method({"GET"})
41
     * @Template
42
     */
43
    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

43
    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...
44
    {
45
        return array();
46
    }
47
48
    /**
49
     * @Route("/session/origins", name="oidc_get_origins")
50
     * @Method({"GET"})
51
     * @Template
52
     */
53
    public function getOriginsAction(Request $request)
54
    {
55
        $client = $this->getClient($request->get('client_id'));
56
57
        $uris = array();
58
        $uris[] = $client->getSiteUrl();
59
        $uris[] = $client->getTermsOfUseUrl();
60
        $uris[] = $client->getLandingPageUrl();
61
        if ($client->getMetadata()) {
62
            $meta = $client->getMetadata();
63
            $uris[] = $meta->getClientUri();
64
            $uris[] = $meta->getInitiateLoginUri();
65
            $uris[] = $meta->getPolicyUri();
66
            $uris[] = $meta->getSectorIdentifierUri();
67
            $uris[] = $meta->getTosUri();
68
            $uris = array_merge(
69
                $uris,
70
                $meta->getRedirectUris(),
71
                $meta->getRequestUris()
72
            );
73
        }
74
75
        $result = array_unique(
76
            array_map(
77
                function ($value) {
78
                    if ($value === null) {
79
                        return;
80
                    }
81
                    $uri = parse_url($value);
82
83
                    $uri['fragment'] = '';
84
                    $uri['path'] = '';
85
                    $uri['query'] = '';
86
                    $uri['user'] = '';
87
                    $uri['pass'] = '';
88
89
                    return $this->unparseUrl($uri);
90
                },
91
                array_filter($uris)
92
            )
93
        );
94
95
        return new JsonResponse(array_values($result));
96
    }
97
98
    /**
99
     * @Route("/session/end", name="oidc_end_session_endpoint")
100
     * @Template
101
     */
102
    public function endSessionAction(Request $request)
103
    {
104
        $alwaysGetRedirectConsent = $this->alwaysGetRedirectConsent();
105
106
        $view = 'LoginCidadaoOpenIDBundle:SessionManagement:endSession.html.twig';
107
        $finishedView = 'LoginCidadaoOpenIDBundle:SessionManagement:endSession.finished.html.twig';
108
        try {
109
            $idToken = $request->get('id_token_hint');
110
            $postLogoutUri = $request->get('post_logout_redirect_uri', null);
111
            $loggedOut = !$this->isGranted('IS_AUTHENTICATED_REMEMBERED');
112
            try {
113
                $getLogoutConsent = $this->shouldGetLogoutConsent($idToken, $loggedOut);
114
            } catch (IdTokenSubMismatchException $e) {
115
                $getLogoutConsent = true;
116
            }
117
118
            list($postLogoutUri, $postLogoutHost) = $this->getPostLogoutInfo($request, $postLogoutUri, $idToken);
119
        } catch (IdTokenValidationException $e) {
120
            return $this->render($finishedView, ['error' => 'openid.session.end.invalid_id_token']);
121
        }
122
123
        $getRedirectConsent = $alwaysGetRedirectConsent && $postLogoutUri;
124
        $authorizedRedirect = !$getLogoutConsent && !$getRedirectConsent;
125
        $authorizedLogout = !$getLogoutConsent;
126
        $formChecked = false;
127
128
        $form = $this->createForm(
129
            new EndSessionForm(),
130
            ['logout' => true, 'redirect' => true],
131
            [
132
                'getLogoutConsent' => $getLogoutConsent,
133
                'getRedirectConsent' => $getRedirectConsent,
134
            ]
135
        );
136
        $form->handleRequest($request);
137
        if ($form->isValid()) {
138
            $data = $form->getData();
139
140
            $authorizedRedirect = false === $getRedirectConsent || $data['redirect'];
141
            $authorizedLogout = false === $getLogoutConsent || $data['logout'];
142
            $formChecked = true;
143
        }
144
145
        $params = [
146
            'form' => $form->createView(),
147
            'client' => $this->getLogoutClient($idToken),
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getLogoutClient($idToken) targeting LoginCidadao\OpenIDBundl...ller::getLogoutClient() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
148
            'postLogoutUri' => $postLogoutUri,
149
            'postLogoutHost' => $postLogoutHost,
150
            'getLogoutConsent' => $getLogoutConsent,
151
            'getRedirectConsent' => $getRedirectConsent,
152
            'loggedOut' => $loggedOut,
153
        ];
154
155
        if (($getLogoutConsent || $getRedirectConsent)
156
            && !$authorizedRedirect
157
            && $formChecked
158
        ) {
159
            $view = $finishedView;
160
        }
161
162
        $response = null;
163
        if ($postLogoutUri && $authorizedRedirect) {
164
            $response = $this->redirect($postLogoutUri);
165
        }
166
167
        if ($authorizedLogout && !$loggedOut) {
168
            if (!$response) {
169
                $params['loggedOut'] = true;
170
                $response = $this->render($view, $params);
171
            }
172
            $response = $this->getSecurityHelper()->logout($request, $response);
173
        }
174
175
        return $response ?: $this->render($view, $params);
176
    }
177
178
    /**
179
     * @param string $clientId
180
     * @return \LoginCidadao\OAuthBundle\Entity\Client|object
181
     */
182
    private function getClient($clientId)
183
    {
184
        $clientId = explode('_', $clientId);
185
        $id = $clientId[0];
186
187
        return $this->getDoctrine()->getManager()
188
            ->getRepository('LoginCidadaoOAuthBundle:Client')->find($id);
189
    }
190
191
    private function unparseUrl($parsed_url)
192
    {
193
        $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'].'://' : '';
194
        $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
195
        $port = isset($parsed_url['port']) ? ':'.$parsed_url['port'] : '';
196
        $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
197
        $pass = isset($parsed_url['pass']) ? ':'.$parsed_url['pass'] : '';
198
        $pass = ($user || $pass) ? "$pass@" : '';
199
        $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
200
        $query = isset($parsed_url['query']) ? '?'.$parsed_url['query'] : '';
201
        $fragment = isset($parsed_url['fragment']) ? '#'.$parsed_url['fragment']
202
            : '';
203
204
        return "$scheme$user$pass$host$port$path$query$fragment";
205
    }
206
207
    /**
208
     * @param mixed $idToken a JWT ID Token as a \JOSE_JWT object or string
209
     * @return bool true if $idToken is valid, false otherwise
210
     * @throws IdTokenSubMismatchException
211
     * @throws IdTokenValidationException
212
     */
213
    private function checkIdToken($idToken)
214
    {
215
        $idToken = $this->getIdToken($idToken);
216
217
        /** @var PublicKey $publicKeyStorage */
218
        $publicKeyStorage = $this->get('oauth2.storage.public_key');
219
        try {
220
            @$idToken->verify($publicKeyStorage->getPublicKey($idToken->claims['aud']));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for verify(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

220
            /** @scrutinizer ignore-unhandled */ @$idToken->verify($publicKeyStorage->getPublicKey($idToken->claims['aud']));

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
221
222
            if (false === $this->checkIdTokenSub($this->getUser(), $idToken)) {
223
                throw new IdTokenSubMismatchException('Invalid subject identifier', Response::HTTP_BAD_REQUEST);
224
            }
225
226
            return true;
227
        } catch (IdTokenSubMismatchException $e) {
228
            throw $e;
229
        } catch (\JOSE_Exception_VerificationFailed $e) {
230
            throw new IdTokenValidationException($e->getMessage(), Response::HTTP_BAD_REQUEST, $e);
231
        } catch (\Exception $e) {
232
            throw new IdTokenValidationException($e->getMessage(), Response::HTTP_BAD_REQUEST, $e);
233
        }
234
    }
235
236
    /**
237
     * @param PersonInterface $person
238
     * @param mixed $idToken
239
     * @return bool
240
     */
241
    private function checkIdTokenSub(PersonInterface $person = null, $idToken)
242
    {
243
        if (null === $person) {
244
            // User is logged out
245
            return true;
246
        }
247
248
        if (!($person instanceof PersonInterface)) {
0 ignored issues
show
introduced by
The condition ! $person instanceof Log...e\Model\PersonInterface can never be true.
Loading history...
249
            return false;
250
        }
251
252
        $client = $this->getClient($idToken->claims['aud']);
253
254
        $sub = $this->getSubjectIdentifier($person, $client);
255
256
        return $idToken->claims['sub'] == $sub;
257
    }
258
259
    /**
260
     * Enforces that the ID Token is a \JOSE_JWT object
261
     * @param mixed $idToken
262
     * @return \JOSE_JWE|\JOSE_JWT
263
     */
264
    private function getIdToken($idToken)
265
    {
266
        if (!($idToken instanceof \JOSE_JWT)) {
267
            try {
268
                $idToken = \JOSE_JWT::decode($idToken);
269
            } catch (\JOSE_Exception_InvalidFormat $e) {
270
                throw new BadRequestHttpException($e->getMessage(), $e);
271
            }
272
        }
273
274
        return $idToken;
275
    }
276
277
    private function validatePostLogoutUri($postLogoutUri, $idToken)
278
    {
279
        if ($postLogoutUri === null) {
280
            return false;
281
        }
282
283
        $postLogoutUri = ClientMetadata::canonicalizeUri($postLogoutUri);
284
285
        if (!$idToken) {
286
            return count($this->findClientByPostLogoutRedirectUri($postLogoutUri)) > 0;
287
        }
288
289
        $idToken = $this->getIdToken($idToken);
290
        $client = $this->getClient($idToken->claims['aud']);
291
292
        return false !== array_search($postLogoutUri, $client->getMetadata()->getPostLogoutRedirectUris());
293
    }
294
295
    private function addStateToUri($postLogoutUri, $state)
296
    {
297
        if ($state) {
298
            $url = parse_url($postLogoutUri);
299
            if (array_key_exists('query', $url)) {
300
                parse_str($url['query'], $query);
301
            } else {
302
                $query = [];
303
            }
304
            $query['state'] = $state;
305
            $url['query'] = http_build_query($query);
306
307
            return $this->unparseUrl($url);
308
        } else {
309
            return $postLogoutUri;
310
        }
311
    }
312
313
    /**
314
     * @return bool
315
     */
316
    private function alwaysGetLogoutConsent()
317
    {
318
        return $this->getParameter('rp_initiated_logout.logout.always_get_consent');
319
    }
320
321
    /**
322
     * @return bool
323
     */
324
    private function alwaysGetRedirectConsent()
325
    {
326
        return $this->getParameter('rp_initiated_logout.redirect.always_get_consent');
327
    }
328
329
    /**
330
     * @param string|\JOSE_JWT $idToken
331
     * @return \LoginCidadao\OAuthBundle\Entity\Client|false
332
     */
333
    private function getIdTokenClient($idToken)
334
    {
335
        if ($idToken === null) {
0 ignored issues
show
introduced by
The condition $idToken === null can never be true.
Loading history...
336
            return false;
337
        }
338
339
        $idToken = $this->getIdToken($idToken);
340
        $client = $this->getClient($idToken->claims['aud']);
341
342
        return $client;
343
    }
344
345
    /**
346
     * @return SecurityHelper
347
     */
348
    private function getSecurityHelper()
349
    {
350
        /** @var SecurityHelper $securityHelper */
351
        $securityHelper = $this->get('lc.security.helper');
352
353
        return $securityHelper;
354
    }
355
356
    private function getSubjectIdentifier(PersonInterface $person, ClientInterface $client)
357
    {
358
        /** @var SubjectIdentifierService $service */
359
        $service = $this->get('oidc.subject_identifier.service');
360
361
        return $service->getSubjectIdentifier($person, $client->getMetadata());
362
    }
363
364
    private function findClientByPostLogoutRedirectUri($postLogoutUri)
365
    {
366
        /** @var ClientMetadataRepository $repo */
367
        $repo = $this->get('oidc.client_metadata.repository');
368
369
        return $repo->findByPostLogoutRedirectUri($postLogoutUri);
370
    }
371
372
    private function shouldGetLogoutConsent($idToken, $loggedOut)
373
    {
374
        $getLogoutConsent = $loggedOut ? false : $this->alwaysGetLogoutConsent();
375
376
        if ($idToken) {
377
            if (false === $this->checkIdToken($idToken)) {
378
                // We didn't receive a valid ID Token, therefore we should ask user for consent
379
                $getLogoutConsent = true;
380
            }
381
        }
382
383
        return $getLogoutConsent;
384
    }
385
386
    private function getLogoutClient($idToken)
387
    {
388
        $client = null;
389
        if ($idToken) {
390
            if ($this->checkIdToken($idToken)) {
391
                $client = $this->getIdTokenClient($idToken);
392
            }
393
        }
394
395
        return $client;
396
    }
397
398
    private function getPostLogoutInfo(Request $request, $postLogoutUri, $idToken)
399
    {
400
        $postLogoutHost = null;
401
        if ($this->validatePostLogoutUri($postLogoutUri, $idToken)) {
402
            $postLogoutUri = $this->addStateToUri($postLogoutUri, $request->get('state', null));
403
            $postLogoutHost = parse_url($postLogoutUri)['host'];
404
        } else {
405
            $postLogoutUri = null;
406
        }
407
408
        return [$postLogoutUri, $postLogoutHost];
409
    }
410
}
411