SessionManagementController::checkSessionAction()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
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
 * @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(EndSessionForm::class, ['logout' => true, 'redirect' => true], [
130
            'getLogoutConsent' => $getLogoutConsent,
131
            'getRedirectConsent' => $getRedirectConsent,
132
        ]);
133
        $form->handleRequest($request);
134
        if ($form->isValid()) {
135
            $data = $form->getData();
136
137
            $authorizedRedirect = false === $getRedirectConsent || $data['redirect'];
138
            $authorizedLogout = false === $getLogoutConsent || $data['logout'];
139
            $formChecked = true;
140
        }
141
142
        $params = [
143
            'form' => $form->createView(),
144
            'client' => $this->getLogoutClient($idToken),
145
            'postLogoutUri' => $postLogoutUri,
146
            'postLogoutHost' => $postLogoutHost,
147
            'getLogoutConsent' => $getLogoutConsent,
148
            'getRedirectConsent' => $getRedirectConsent,
149
            'loggedOut' => $loggedOut,
150
        ];
151
152
        if (($getLogoutConsent || $getRedirectConsent)
153
            && !$authorizedRedirect
154
            && $formChecked
155
        ) {
156
            $view = $finishedView;
157
        }
158
159
        $response = null;
160
        if ($postLogoutUri && $authorizedRedirect) {
161
            $response = $this->redirect($postLogoutUri);
162
        }
163
164
        if ($authorizedLogout && !$loggedOut) {
165
            if (!$response) {
166
                $params['loggedOut'] = true;
167
                $response = $this->render($view, $params);
168
            }
169
            $response = $this->getSecurityHelper()->logout($request, $response);
170
        }
171
172
        return $response ?: $this->render($view, $params);
173
    }
174
175
    /**
176
     * @param string $clientId
177
     * @return \LoginCidadao\OAuthBundle\Entity\Client|object
178
     */
179
    private function getClient($clientId)
180
    {
181
        $clientId = explode('_', $clientId);
182
        $id = $clientId[0];
183
184
        return $this->getDoctrine()->getManager()
185
            ->getRepository('LoginCidadaoOAuthBundle:Client')->find($id);
186
    }
187
188
    private function unparseUrl($parsed_url)
189
    {
190
        $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'].'://' : '';
191
        $host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
192
        $port = isset($parsed_url['port']) ? ':'.$parsed_url['port'] : '';
193
        $user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
194
        $pass = isset($parsed_url['pass']) ? ':'.$parsed_url['pass'] : '';
195
        $pass = ($user || $pass) ? "$pass@" : '';
196
        $path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
197
        $query = isset($parsed_url['query']) ? '?'.$parsed_url['query'] : '';
198
        $fragment = isset($parsed_url['fragment']) ? '#'.$parsed_url['fragment']
199
            : '';
200
201
        return "$scheme$user$pass$host$port$path$query$fragment";
202
    }
203
204
    /**
205
     * @param mixed $idToken a JWT ID Token as a \JOSE_JWT object or string
206
     * @return bool true if $idToken is valid, false otherwise
207
     * @throws IdTokenSubMismatchException
208
     * @throws IdTokenValidationException
209
     */
210
    private function checkIdToken($idToken)
211
    {
212
        $idToken = $this->getIdToken($idToken);
213
214
        /** @var PublicKey $publicKeyStorage */
215
        $publicKeyStorage = $this->get('oauth2.storage.public_key');
216
        try {
217
            $idToken->verify($publicKeyStorage->getPublicKey($idToken->claims['aud']));
218
219
            if (false === $this->checkIdTokenSub($this->getUser(), $idToken)) {
220
                throw new IdTokenSubMismatchException('Invalid subject identifier', Response::HTTP_BAD_REQUEST);
221
            }
222
223
            return true;
224
        } catch (IdTokenSubMismatchException $e) {
225
            throw $e;
226
        } catch (\JOSE_Exception_VerificationFailed|\Exception $e) {
227
            throw new IdTokenValidationException($e->getMessage(), Response::HTTP_BAD_REQUEST, $e);
228
        }
229
    }
230
231
    /**
232
     * @param PersonInterface $person
233
     * @param mixed $idToken
234
     * @return bool
235
     */
236
    private function checkIdTokenSub(PersonInterface $person = null, $idToken)
237
    {
238
        if (null === $person) {
239
            // User is logged out
240
            return true;
241
        }
242
243
        if (!($person instanceof PersonInterface)) {
0 ignored issues
show
introduced by
$person is always a sub-type of LoginCidadao\CoreBundle\Model\PersonInterface.
Loading history...
244
            return false;
245
        }
246
247
        $client = $this->getClient($idToken->claims['aud']);
248
249
        $sub = $this->getSubjectIdentifier($person, $client);
250
251
        return $idToken->claims['sub'] == $sub;
252
    }
253
254
    /**
255
     * Enforces that the ID Token is a \JOSE_JWT object
256
     * @param mixed $idToken
257
     * @return \JOSE_JWE|\JOSE_JWT
258
     */
259
    private function getIdToken($idToken)
260
    {
261
        if (!($idToken instanceof \JOSE_JWT)) {
262
            try {
263
                $idToken = \JOSE_JWT::decode($idToken);
264
            } catch (\JOSE_Exception_InvalidFormat $e) {
265
                throw new BadRequestHttpException($e->getMessage(), $e);
266
            }
267
        }
268
269
        return $idToken;
270
    }
271
272
    private function validatePostLogoutUri($postLogoutUri, $idToken)
273
    {
274
        if ($postLogoutUri === null) {
275
            return false;
276
        }
277
278
        $postLogoutUri = ClientMetadata::canonicalizeUri($postLogoutUri);
279
280
        if (!$idToken) {
281
            return count($this->findClientByPostLogoutRedirectUri($postLogoutUri)) > 0;
282
        }
283
284
        $idToken = $this->getIdToken($idToken);
285
        $client = $this->getClient($idToken->claims['aud']);
286
287
        return false !== array_search($postLogoutUri, $client->getMetadata()->getPostLogoutRedirectUris());
288
    }
289
290
    private function addStateToUri($postLogoutUri, $state)
291
    {
292
        if ($state) {
293
            $url = parse_url($postLogoutUri);
294
            if (array_key_exists('query', $url)) {
295
                parse_str($url['query'], $query);
296
            } else {
297
                $query = [];
298
            }
299
            $query['state'] = $state;
300
            $url['query'] = http_build_query($query);
301
302
            return $this->unparseUrl($url);
303
        } else {
304
            return $postLogoutUri;
305
        }
306
    }
307
308
    /**
309
     * @return bool
310
     */
311
    private function alwaysGetLogoutConsent()
312
    {
313
        return $this->getParameter('rp_initiated_logout.logout.always_get_consent');
314
    }
315
316
    /**
317
     * @return bool
318
     */
319
    private function alwaysGetRedirectConsent()
320
    {
321
        return $this->getParameter('rp_initiated_logout.redirect.always_get_consent');
322
    }
323
324
    /**
325
     * @param string|\JOSE_JWT $idToken
326
     * @return \LoginCidadao\OAuthBundle\Entity\Client|false
327
     */
328
    private function getIdTokenClient($idToken)
329
    {
330
        if ($idToken === null) {
331
            return false;
332
        }
333
334
        $idToken = $this->getIdToken($idToken);
335
        $client = $this->getClient($idToken->claims['aud']);
336
337
        return $client;
338
    }
339
340
    /**
341
     * @return SecurityHelper
342
     */
343
    private function getSecurityHelper()
344
    {
345
        /** @var SecurityHelper $securityHelper */
346
        $securityHelper = $this->get('lc.security.helper');
347
348
        return $securityHelper;
349
    }
350
351
    private function getSubjectIdentifier(PersonInterface $person, ClientInterface $client)
352
    {
353
        /** @var SubjectIdentifierService $service */
354
        $service = $this->get('oidc.subject_identifier.service');
355
356
        return $service->getSubjectIdentifier($person, $client->getMetadata());
357
    }
358
359
    private function findClientByPostLogoutRedirectUri($postLogoutUri)
360
    {
361
        /** @var ClientMetadataRepository $repo */
362
        $repo = $this->get('oidc.client_metadata.repository');
363
364
        return $repo->findByPostLogoutRedirectUri($postLogoutUri);
365
    }
366
367
    private function shouldGetLogoutConsent($idToken, $loggedOut)
368
    {
369
        $getLogoutConsent = $loggedOut ? false : $this->alwaysGetLogoutConsent();
370
371
        if ($idToken) {
372
            if (false === $this->checkIdToken($idToken)) {
0 ignored issues
show
introduced by
The condition false === $this->checkIdToken($idToken) is always false.
Loading history...
373
                // We didn't receive a valid ID Token, therefore we should ask user for consent
374
                $getLogoutConsent = true;
375
            }
376
        }
377
378
        return $getLogoutConsent;
379
    }
380
381
    private function getLogoutClient($idToken)
382
    {
383
        $client = null;
384
        if ($idToken) {
385
            if ($this->checkIdToken($idToken)) {
386
                $client = $this->getIdTokenClient($idToken);
387
            }
388
        }
389
390
        return $client;
391
    }
392
393
    private function getPostLogoutInfo(Request $request, $postLogoutUri, $idToken)
394
    {
395
        $postLogoutHost = null;
396
        if ($this->validatePostLogoutUri($postLogoutUri, $idToken)) {
397
            $postLogoutUri = $this->addStateToUri($postLogoutUri, $request->get('state', null));
398
            $postLogoutHost = parse_url($postLogoutUri)['host'];
399
        } else {
400
            $postLogoutUri = null;
401
        }
402
403
        return [$postLogoutUri, $postLogoutHost];
404
    }
405
}
406