SamlListener::setAuthenticationFailedResponse()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
/**
4
 * Copyright 2015 SURFnet B.V.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace OpenConext\ProfileBundle\Security\Firewall;
20
21
use Exception;
22
use OpenConext\ProfileBundle\Saml\StateHandler;
23
use OpenConext\ProfileBundle\Security\Authentication\SamlInteractionProvider;
24
use OpenConext\ProfileBundle\Security\Authentication\Token\SamlToken;
25
use SAML2\Response\Exception\PreconditionNotMetException;
26
use Surfnet\SamlBundle\Http\Exception\AuthnFailedSamlResponseException;
27
use Surfnet\SamlBundle\SAML2\Response\Assertion\InResponseTo;
28
use Symfony\Component\HttpFoundation\RedirectResponse;
29
use Symfony\Component\HttpFoundation\Request;
30
use Symfony\Component\HttpFoundation\Response;
31
use Symfony\Component\HttpFoundation\Session\SessionInterface;
32
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
33
use \Psr\Log\LoggerInterface;
34
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
35
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
36
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
37
use Symfony\Component\Security\Core\Exception\AuthenticationException;
38
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
39
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
40
use Twig_Environment as Twig;
41
42
/**
43
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
44
 */
45
class SamlListener implements ListenerInterface
46
{
47
    /**
48
     * @var SessionInterface
49
     */
50
    private $session;
51
52
    /**
53
     * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
54
     */
55
    private $urlMatcher;
56
57
    /**
58
     * @var \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface
59
     */
60
    private $tokenStorage;
61
62
    /**
63
     * @var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
64
     */
65
    private $authenticationManager;
66
67
    /**
68
     * @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger
69
     */
70
    private $logger;
71
72
    /**
73
     * @var \OpenConext\ProfileBundle\Security\Authentication\SamlInteractionProvider
74
     */
75
    private $samlInteractionProvider;
76
77
    /**
78
     * @var \OpenConext\ProfileBundle\Saml\StateHandler
79
     */
80
    private $stateHandler;
81
82
    /**
83
     * @var \Twig_Environment
84
     */
85
    private $twig;
86
87
    public function __construct(
88
        SessionInterface $session,
89
        UrlMatcherInterface $urlMatcher,
90
        TokenStorageInterface $tokenStorage,
91
        AuthenticationManagerInterface $authenticationManager,
92
        SamlInteractionProvider $samlInteractionProvider,
93
        StateHandler $stateHandler,
94
        LoggerInterface $logger,
95
        Twig $twig
96
    ) {
97
        $this->session                  = $session;
98
        $this->urlMatcher               = $urlMatcher;
99
        $this->tokenStorage             = $tokenStorage;
100
        $this->authenticationManager    = $authenticationManager;
101
        $this->samlInteractionProvider  = $samlInteractionProvider;
102
        $this->stateHandler             = $stateHandler;
103
        $this->logger                   = $logger;
0 ignored issues
show
Documentation Bug introduced by
$logger is of type object<Psr\Log\LoggerInterface>, but the property $logger was declared to be of type object<Surfnet\SamlBundl...mlAuthenticationLogger>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
104
        $this->twig = $twig;
105
    }
106
107
    public function handle(GetResponseEvent $event)
108
    {
109
        try {
110
            $this->handleEvent($event);
111
        } catch (\Exception $e) {
112
            $this->samlInteractionProvider->reset();
113
            throw $e;
114
        }
115
    }
116
117
    /**
118
     * @param GetResponseEvent $event
119
     */
120
    private function handleEvent(GetResponseEvent $event)
121
    {
122
        if ($this->tokenStorage->getToken()) {
123
            return;
124
        }
125
126
        if (!$this->isAcsRequest($event->getRequest()) ||
127
            !$this->samlInteractionProvider->isSamlAuthenticationInitiated()) {
128
            $this->sendAuthnRequest($event);
129
130
            return;
131
        }
132
133
        $expectedInResponseTo = $this->stateHandler->getRequestId();
134
        $logger = $this->logger;
135
136
        try {
137
            $assertion = $this->samlInteractionProvider->processSamlResponse($event->getRequest());
138
        } catch (PreconditionNotMetException $e) {
139
            $logger->notice(sprintf('SAML response precondition not met: "%s"', $e->getMessage()));
140
            $this->setPreconditionExceptionResponse($e, $event);
141
142
            return;
143
        } catch (Exception $e) {
144
            $logger->error(sprintf('Failed SAMLResponse Parsing: "%s"', $e->getMessage()));
145
            throw new AuthenticationException('Failed SAMLResponse parsing', 0, $e);
146
        }
147
148
        if (!InResponseTo::assertEquals($assertion, $expectedInResponseTo)) {
149
            $logger->error('Unknown or unexpected InResponseTo in SAMLResponse');
150
            throw new AuthenticationException('Unknown or unexpected InResponseTo in SAMLResponse');
151
        }
152
153
        $logger->notice('Successfully processed SAMLResponse, attempting to authenticate');
154
155
        $token = new SamlToken();
156
        $token->assertion = $assertion;
157
158
        try {
159
            $authToken = $this->authenticationManager->authenticate($token);
160
        } catch (AuthenticationException $failed) {
161
            $logger->error(sprintf('Authentication Failed, reason: "%s"', $failed->getMessage()));
162
            $this->setAuthenticationFailedResponse($event);
163
164
            return;
165
        }
166
167
        $this->tokenStorage->setToken($authToken);
168
169
        // migrate the session to prevent session hijacking
170
        $this->session->migrate();
171
172
        $event->setResponse(new RedirectResponse($this->stateHandler->getCurrentRequestUri()));
173
        $logger->notice('Authentication succeeded, redirecting to original location');
174
    }
175
176
    /**
177
     * Check if this is a request to the ACS location.
178
     *
179
     * @return bool
180
     */
181
    private function isAcsRequest(Request $request)
182
    {
183
        try {
184
            $params = $this->urlMatcher->match($request->getPathInfo());
185
        } catch (ResourceNotFoundException $e) {
186
            return false;
187
        }
188
189
        return $params['_route'] === 'profile.saml_consume_assertion';
190
    }
191
192
    /**
193
     * @param GetResponseEvent $event
194
     */
195
    private function sendAuthnRequest(GetResponseEvent $event)
196
    {
197
        $this->stateHandler->setCurrentRequestUri($event->getRequest()->getUri());
198
199
        $event->setResponse($this->samlInteractionProvider->initiateSamlRequest());
200
201
        $logger = $this->logger;
202
        $logger->notice('Sending AuthnRequest');
203
    }
204
205
    /**
206
     * @param PreconditionNotMetException $exception
207
     * @param GetResponseEvent $event
208
     */
209
    private function setPreconditionExceptionResponse(PreconditionNotMetException $exception, GetResponseEvent $event)
210
    {
211
        $template = null;
0 ignored issues
show
Unused Code introduced by
$template is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
212
213
        if ($exception instanceof AuthnFailedSamlResponseException) {
214
            $template = 'SurfnetStepupSelfServiceSelfServiceBundle:Saml/Exception:authnFailed.html.twig';
215
        } else {
216
            $template = 'SurfnetStepupSelfServiceSelfServiceBundle:Saml/Exception:preconditionNotMet.html.twig';
217
        }
218
219
        $html = $this->twig->render($template, ['exception' => $exception]);
220
        $event->setResponse(new Response($html, Response::HTTP_UNAUTHORIZED));
221
    }
222
223
    /**
224
     * Deny authentication by default
225
     * @param GetResponseEvent $event
226
     */
227
    private function setAuthenticationFailedResponse(GetResponseEvent $event)
228
    {
229
        $response = new Response();
230
        $response->setStatusCode(Response::HTTP_FORBIDDEN);
231
        $event->setResponse($response);
232
    }
233
}
234