Completed
Push — master ( d59749...4b47e7 )
by Michiel
04:27 queued 02:03
created

SamlProxyController::consumeAssertionAction()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 9.0808
c 0
b 0
f 0
cc 5
nc 5
nop 2
1
<?php
2
3
/**
4
 * Copyright 2014 SURFnet bv
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 Surfnet\StepupGateway\SamlStepupProviderBundle\Controller;
20
21
use DateTime;
22
use Exception;
23
use SAML2\Constants;
24
use SAML2\Response as SAMLResponse;
25
use Surfnet\SamlBundle\Http\XMLResponse;
26
use Surfnet\StepupGateway\GatewayBundle\Exception\ResponseFailureException;
27
use Surfnet\StepupGateway\SamlStepupProviderBundle\Exception\InvalidSubjectException;
28
use Surfnet\StepupGateway\SamlStepupProviderBundle\Exception\NotConnectedServiceProviderException;
29
use Surfnet\StepupGateway\SamlStepupProviderBundle\Exception\SecondfactorVerfificationRequiredException;
30
use Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider;
31
use Surfnet\StepupGateway\SamlStepupProviderBundle\Saml\ProxyResponseFactory;
32
use Surfnet\StepupGateway\SamlStepupProviderBundle\Saml\StateHandler;
33
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\Gateway\ConsumeAssertionService;
34
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\Gateway\LoginService;
35
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\Gateway\SecondFactorVerificationService;
36
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
37
use Symfony\Component\HttpFoundation\Request;
38
use Symfony\Component\HttpFoundation\Response;
39
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
40
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
41
42
/**
43
 * Handling of GSSP registration and verification.
44
 *
45
 * See docs/GatewayState.md for a high-level diagram on how this controller
46
 * interacts with outside actors and other parts of Stepup.
47
 *
48
 * Should be refactored, {@see https://www.pivotaltracker.com/story/show/90169776}
49
 *
50
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
51
 * @SuppressWarnings(PHPMD.NPathComplexity)
52
 */
53
class SamlProxyController extends Controller
54
{
55
    /**
56
     * Proxy a GSSP authentication request to the remote GSSP SSO endpoint.
57
     *
58
     * The user is about to be sent to the remote GSSP application for
59
     * registration. Verification is not initiated with a SAML AUthnRequest,
60
     * see sendSecondFactorVerificationAuthnRequestAction().
61
     *
62
     * The service provider in this context is SelfService (when registering
63
     * a token) or RA (when vetting a token).
64
     *
65
     * @param string  $provider
66
     * @param Request $httpRequest
67
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
68
     */
69
    public function singleSignOnAction($provider, Request $httpRequest)
70
    {
71
        $provider = $this->getProvider($provider);
72
73
        /** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */
74
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
75
        $gsspLoginService = $this->getGsspLoginService();
76
77
        $logger = $this->get('logger');
78
        $logger->notice('Received AuthnRequest, started processing');
79
80
        try {
81
            $proxyRequest = $gsspLoginService->singleSignOn($provider, $httpRequest);
82
        } catch (NotConnectedServiceProviderException $e) {
83
            throw new AccessDeniedHttpException();
84
        }
85
86
        return $redirectBinding->createResponseFor($proxyRequest);
87
    }
88
89
    /**
90
     * Start a GSSP single sign-on.
91
     *
92
     * The user has selected a second factor token and the token happens to be
93
     * a GSSP token. The SecondFactorController therefor did an internal
94
     * redirect (see SecondFactorController::verifyGssfAction) to this method.
95
     *
96
     * In this method, an authn request is created. This authn request is sent
97
     * directly to the remote GSSP SSO URL, and the response is handled in
98
     * consumeAssertionAction().
99
     *
100
     * @param $provider
101
     * @param $subjectNameId
102
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
103
     */
104
    public function sendSecondFactorVerificationAuthnRequestAction($provider, $subjectNameId)
105
    {
106
        $provider = $this->getProvider($provider);
107
108
        $gsspSecondFactorVerificationService = $this->getGsspSecondFactorVerificationService();
109
110
        $authnRequest = $gsspSecondFactorVerificationService->sendSecondFactorVerificationAuthnRequest($provider, $subjectNameId);
111
112
        /** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */
113
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
114
115
        return $redirectBinding->createResponseFor($authnRequest);
116
    }
117
118
    /**
119
     * Process an assertion received from the remote GSSP application.
120
     *
121
     * The GSSP application sent an assertion back to the gateway. When
122
     * successful, the user is sent back to:
123
     *
124
     *  1. in case of registration: back to the originating SP (SelfService or RA)
125
     *  2. in case of verification: internal redirect to SecondFactorController
126
     *
127
     * @param string $provider
128
     * @param Request $httpRequest
129
     * @return \Symfony\Component\HttpFoundation\Response
130
     * @throws Exception
131
     */
132
    public function consumeAssertionAction($provider, Request $httpRequest)
133
    {
134
        $provider = $this->getProvider($provider);
135
136
        $consumeAssertionService = $this->getGsspConsumeAssertionService();
137
        $proxyResponseFactory = $this->getProxyResponseFactory($provider);
138
139
        try {
140
            $response = $consumeAssertionService->consumeAssertion($provider, $httpRequest, $proxyResponseFactory);
141
        } catch (ResponseFailureException $e) {
142
            $response = $this->createResponseFailureResponse(
143
                $provider,
144
                $this->getDestination($provider->getStateHandler())
145
            );
146
            return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
147
148
        } catch (InvalidSubjectException $e) {
149
            return $this->renderSamlResponse(
150
                'recoverableError',
151
                $provider->getStateHandler(),
152
                $this->createAuthnFailedResponse(
153
                    $provider,
154
                    $this->getDestination($provider->getStateHandler())
155
                )
156
            );
157
        } catch (SecondfactorVerfificationRequiredException $e) {
158
            return $this->forward('SurfnetStepupGatewayGatewayBundle:SecondFactor:gssfVerified');
159
        } catch (Exception $e) {
160
            throw $e;
161
        }
162
163
        return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
164
    }
165
166
    /**
167
     * @param string $provider
168
     * @return XMLResponse
169
     */
170
    public function metadataAction($provider)
171
    {
172
        $provider = $this->getProvider($provider);
173
174
        /** @var \Surfnet\SamlBundle\Metadata\MetadataFactory $factory */
175
        $factory = $this->get('gssp.provider.' . $provider->getName() . '.metadata.factory');
176
177
        return new XMLResponse($factory->generate());
178
    }
179
180
    /**
181
     * @param string $provider
182
     * @return \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider
183
     */
184
    private function getProvider($provider)
185
    {
186
        /** @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ProviderRepository $providerRepository */
187
        $providerRepository = $this->get('gssp.provider_repository');
188
189
        if (!$providerRepository->has($provider)) {
190
            throw new NotFoundHttpException(
191
                sprintf('Requested GSSP "%s" does not exist or is not registered', $provider)
192
            );
193
        }
194
195
        return $providerRepository->get($provider);
196
    }
197
198
    /**
199
     * @param StateHandler $stateHandler
200
     * @return string
201
     */
202
    private function getDestination(StateHandler $stateHandler)
203
    {
204
        if ($stateHandler->secondFactorVerificationRequested()) {
205
            // GSSP verification action, return to SP from GatewayController state!
206
            $destination = $this->get('gateway.proxy.response_context')->getDestination();
207
        } else {
208
            // GSSP registration action, return to SP remembered in ssoAction().
209
            $serviceProvider = $this->getServiceProvider(
210
                $stateHandler->getRequestServiceProvider()
211
            );
212
213
            $destination = $serviceProvider->determineAcsLocation(
214
                $stateHandler->getRequestAssertionConsumerServiceUrl(),
215
                $this->get('logger')
216
            );
217
        }
218
219
        return $destination;
220
    }
221
222
    /**
223
     * @param string         $view
224
     * @param StateHandler   $stateHandler
225
     * @param SAMLResponse $response
226
     * @return Response
227
     */
228
    public function renderSamlResponse($view, StateHandler $stateHandler, SAMLResponse $response)
229
    {
230
        $parameters = [
231
            'acu'        => $response->getDestination(),
232
            'response'   => $this->getResponseAsXML($response),
233
            'relayState' => $stateHandler->getRelayState()
234
        ];
235
236
        $response = parent::render(
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (render() instead of renderSamlResponse()). Are you sure this is correct? If so, you might want to change this to $this->render().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
237
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:' . $view . '.html.twig',
238
            $parameters,
239
            $response
0 ignored issues
show
Documentation introduced by
$response is of type object<SAML2\Response>, but the function expects a null|object<Symfony\Comp...ttpFoundation\Response>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
240
        );
241
242
        // clear the state so we can call again :)
243
        $stateHandler->clear();
244
245
        return $response;
246
    }
247
248
    /**
249
     * @param SAMLResponse $response
250
     * @return string
251
     */
252
    private function getResponseAsXML(SAMLResponse $response)
253
    {
254
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
255
    }
256
257
    /**
258
     * Response that indicates that an error occurred in the responder (the gateway). Used to indicate that we could
259
     * not process the response we received from the upstream GSSP
260
     *
261
     * @param Provider $provider
262
     * @param string $destination
263
     * @return SAMLResponse
264
     */
265
    private function createResponseFailureResponse(Provider $provider, $destination)
266
    {
267
        $response = $this->createResponse($provider, $destination);
268
        $response->setStatus(['Code' => Constants::STATUS_RESPONDER]);
269
270
        return $response;
271
    }
272
273
    /**
274
     * Response that indicates that the authentication could not be performed correctly. In this context it means
275
     * that the upstream GSSP did not responsd with the same NameID as we request to authenticate in the AuthnRequest
276
     *
277
     * @param Provider $provider
278
     * @param string $destination
279
     * @return SAMLResponse
280
     */
281
    private function createAuthnFailedResponse(Provider $provider, $destination)
282
    {
283
        $response = $this->createResponse($provider, $destination);
284
        $response->setStatus([
285
            'Code'    => Constants::STATUS_RESPONDER,
286
            'SubCode' => Constants::STATUS_AUTHN_FAILED
287
        ]);
288
289
        return $response;
290
    }
291
292
    /**
293
     * Creates a standard response with default status Code (success)
294
     *
295
     * @param Provider $provider
296
     * @param string $destination
297
     * @return SAMLResponse
298
     */
299
    private function createResponse(Provider $provider, $destination)
300
    {
301
        $response = new SAMLResponse();
302
        $response->setDestination($destination);
303
        $response->setIssuer($provider->getIdentityProvider()->getEntityId());
304
        $response->setIssueInstant((new DateTime('now'))->getTimestamp());
305
        $response->setInResponseTo($provider->getStateHandler()->getRequestId());
306
307
        return $response;
308
    }
309
310
    /**
311
     * @param string $serviceProvider
312
     * @return \Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider
313
     */
314
    private function getServiceProvider($serviceProvider)
315
    {
316
        /**
317
         * @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders
318
         */
319
        $connectedServiceProviders = $this->get('gssp.connected_service_providers');
320
        return $connectedServiceProviders->getConfigurationOf($serviceProvider);
321
    }
322
323
    /**
324
     * @return LoginService
325
     */
326
    private function getGsspLoginService()
327
    {
328
        return $this->get('gssp.service.gssp.login');
329
    }
330
331
    /**
332
     * @return SecondFactorVerificationService
333
     */
334
    private function getGsspSecondFactorVerificationService()
335
    {
336
        return $this->get('gssp.service.gssp.second_factor_verification');
337
    }
338
339
    /**
340
     * @return ConsumeAssertionService
341
     */
342
    private function getGsspConsumeAssertionService()
343
    {
344
        return $this->get('gssp.service.gssp.consume_assertion');
345
    }
346
347
    /**
348
     * @param Provider $provider
349
     * @return ProxyResponseFactory
350
     */
351
    private function getProxyResponseFactory(Provider $provider)
352
    {
353
        return $this->get('gssp.provider.' . $provider->getName() . '.response_proxy');
354
    }
355
}
356