Completed
Push — feature/implement-state-handli... ( d0163b...537647 )
by Michiel
01:51
created

createResponseFailureResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 11
Ratio 100 %

Importance

Changes 0
Metric Value
dl 11
loc 11
rs 9.9
c 0
b 0
f 0
cc 1
nc 1
nop 3
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\Controller\GatewayController;
27
use Surfnet\StepupGateway\GatewayBundle\Exception\ResponseFailureException;
28
use Surfnet\StepupGateway\SamlStepupProviderBundle\Exception\InvalidSubjectException;
29
use Surfnet\StepupGateway\SamlStepupProviderBundle\Exception\NotConnectedServiceProviderException;
30
use Surfnet\StepupGateway\SamlStepupProviderBundle\Exception\SecondfactorVerfificationRequiredException;
31
use Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider;
32
use Surfnet\StepupGateway\SamlStepupProviderBundle\Saml\ProxyResponseFactory;
33
use Surfnet\StepupGateway\SamlStepupProviderBundle\Saml\StateHandler;
34
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\Gateway\ConsumeAssertionService;
35
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\Gateway\LoginService;
36
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\Gateway\SecondFactorVerificationService;
37
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
38
use Symfony\Component\HttpFoundation\Request;
39
use Symfony\Component\HttpFoundation\Response;
40
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
41
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
42
43
/**
44
 * Handling of GSSP registration and verification.
45
 *
46
 * See docs/GatewayState.md for a high-level diagram on how this controller
47
 * interacts with outside actors and other parts of Stepup.
48
 *
49
 * Should be refactored, {@see https://www.pivotaltracker.com/story/show/90169776}
50
 *
51
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
52
 * @SuppressWarnings(PHPMD.NPathComplexity)
53
 */
54
class SamlProxyController extends Controller
55
{
56
    /**
57
     * Proxy a GSSP authentication request to the remote GSSP SSO endpoint.
58
     *
59
     * The user is about to be sent to the remote GSSP application for
60
     * registration. Verification is not initiated with a SAML AUthnRequest,
61
     * see sendSecondFactorVerificationAuthnRequestAction().
62
     *
63
     * The service provider in this context is SelfService (when registering
64
     * a token) or RA (when vetting a token).
65
     *
66
     * @param string $provider
67
     * @param Request $httpRequest
68
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
69
     */
70
    public function singleSignOnAction($provider, Request $httpRequest)
71
    {
72
        $provider = $this->getProvider($provider);
73
74
        /** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */
75
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
76
        $gsspLoginService = $this->getGsspLoginService();
77
78
        $logger = $this->get('logger');
79
        $logger->notice('Received AuthnRequest, started processing');
80
81
        try {
82
            $proxyRequest = $gsspLoginService->singleSignOn($provider, $httpRequest);
83
        } catch (NotConnectedServiceProviderException $e) {
84
            throw new AccessDeniedHttpException();
85
        }
86
87
        return $redirectBinding->createResponseFor($proxyRequest);
88
    }
89
90
    /**
91
     * Start a GSSP single sign-on.
92
     *
93
     * The user has selected a second factor token and the token happens to be
94
     * a GSSP token. The SecondFactorController therefor did an internal
95
     * redirect (see SecondFactorController::verifyGssfAction) to this method.
96
     *
97
     * In this method, an authn request is created. This authn request is sent
98
     * directly to the remote GSSP SSO URL, and the response is handled in
99
     * consumeAssertionAction().
100
     *
101
     * @param string $provider
102
     * @param string $subjectNameId
103
     * @param string $responseContextServiceId
104
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
105
     */
106
    public function sendSecondFactorVerificationAuthnRequestAction($provider, $subjectNameId, $responseContextServiceId)
107
    {
108
        $provider = $this->getProvider($provider);
109
110
        $gsspSecondFactorVerificationService = $this->getGsspSecondFactorVerificationService();
111
112
        $authnRequest = $gsspSecondFactorVerificationService->sendSecondFactorVerificationAuthnRequest(
113
            $provider,
114
            $subjectNameId,
115
            $responseContextServiceId
116
        );
117
118
        /** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */
119
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
120
121
        return $redirectBinding->createResponseFor($authnRequest);
122
    }
123
124
    /**
125
     * Process an assertion received from the remote GSSP application.
126
     *
127
     * The GSSP application sent an assertion back to the gateway. When
128
     * successful, the user is sent back to:
129
     *
130
     *  1. in case of registration: back to the originating SP (SelfService or RA)
131
     *  2. in case of verification: internal redirect to SecondFactorController
132
     *
133
     * @param string $provider
134
     * @param Request $httpRequest
135
     * @return \Symfony\Component\HttpFoundation\Response
136
     * @throws Exception
137
     */
138
    public function consumeAssertionAction($provider, Request $httpRequest)
139
    {
140
        $provider = $this->getProvider($provider);
141
142
        $consumeAssertionService = $this->getGsspConsumeAssertionService();
143
        $proxyResponseFactory = $this->getProxyResponseFactory($provider);
144
145
        try {
146
            $response = $consumeAssertionService->consumeAssertion($provider, $httpRequest, $proxyResponseFactory);
147
        } catch (ResponseFailureException $e) {
148
            $response = $this->createResponseFailureResponse(
149
                $provider,
150
                $this->getDestination($provider->getStateHandler()),
151
                $e->getMessage()
152
            );
153
            return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
154
        } catch (InvalidSubjectException $e) {
155
            return $this->renderSamlResponse(
156
                'recoverableError',
157
                $provider->getStateHandler(),
158
                $this->createAuthnFailedResponse(
159
                    $provider,
160
                    $this->getDestination($provider->getStateHandler())
161
                )
162
            );
163
        } catch (SecondfactorVerfificationRequiredException $e) {
164
            // The provider state handler has no access to the session object, hence we use the proxy state handler
165
            $stateHandler = $this->get('gateway.proxy.state_handler');
166
            return $this->forward(
167
                'SurfnetStepupGatewayGatewayBundle:SecondFactor:gssfVerified',
168
                [
169
                    // The authentication mode is loaded from session, based on the request id
170
                    'authenticationMode' => $stateHandler->getAuthenticationModeForRequestId(
171
                        $consumeAssertionService->getReceivedRequestId()
172
                    ),
173
                ]
174
            );
175
        } catch (Exception $e) {
176
            throw $e;
177
        }
178
179
        return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
180
    }
181
182
    /**
183
     * @param string $provider
184
     * @return XMLResponse
185
     */
186
    public function metadataAction($provider)
187
    {
188
        $provider = $this->getProvider($provider);
189
190
        /** @var \Surfnet\SamlBundle\Metadata\MetadataFactory $factory */
191
        $factory = $this->get('gssp.provider.' . $provider->getName() . '.metadata.factory');
192
193
        return new XMLResponse($factory->generate());
194
    }
195
196
    /**
197
     * @param string $provider
198
     * @return \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider
199
     */
200
    private function getProvider($provider)
201
    {
202
        /** @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ProviderRepository $providerRepository */
203
        $providerRepository = $this->get('gssp.provider_repository');
204
205
        if (!$providerRepository->has($provider)) {
206
            throw new NotFoundHttpException(
207
                sprintf('Requested GSSP "%s" does not exist or is not registered', $provider)
208
            );
209
        }
210
211
        return $providerRepository->get($provider);
212
    }
213
214
    /**
215
     * @param StateHandler $stateHandler
216
     * @return string
217
     */
218
    private function getDestination(StateHandler $stateHandler)
219
    {
220
        if ($stateHandler->secondFactorVerificationRequested()) {
221
            // This can either be a SFO or 'regular' SSO authentication. Both use a ResponseContext service of their own
222
            $responseContextServiceId = $stateHandler->getResponseContextServiceId();
223
            // GSSP verification action, return to SP from GatewayController state!
224
            $destination = $this->get($responseContextServiceId)->getDestination();
225
        } else {
226
            // GSSP registration action, return to SP remembered in ssoAction().
227
            $serviceProvider = $this->getServiceProvider(
228
                $stateHandler->getRequestServiceProvider()
229
            );
230
231
            $destination = $serviceProvider->determineAcsLocation(
232
                $stateHandler->getRequestAssertionConsumerServiceUrl(),
233
                $this->get('logger')
234
            );
235
        }
236
237
        return $destination;
238
    }
239
240
    /**
241
     * @param string $view
242
     * @param StateHandler $stateHandler
243
     * @param SAMLResponse $response
244
     * @return Response
245
     */
246
    public function renderSamlResponse($view, StateHandler $stateHandler, SAMLResponse $response)
247
    {
248
        $parameters = [
249
            'acu' => $response->getDestination(),
250
            'response' => $this->getResponseAsXML($response),
251
            'relayState' => $stateHandler->getRelayState(),
252
        ];
253
254
        $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...
255
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:' . $view . '.html.twig',
256
            $parameters
257
        );
258
259
        // clear the state so we can call again :)
260
        $stateHandler->clear();
261
262
        return $response;
263
    }
264
265
    /**
266
     * @param SAMLResponse $response
267
     * @return string
268
     */
269
    private function getResponseAsXML(SAMLResponse $response)
270
    {
271
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
272
    }
273
274
    /**
275
     * Response that indicates that an error occurred in the responder (the gateway). Used to indicate that we could
276
     * not process the response we received from the upstream GSSP
277
     *
278
     * @param Provider $provider
279
     * @param string $destination
280
     * @return SAMLResponse
281
     */
282 View Code Duplication
    private function createResponseFailureResponse(Provider $provider, $destination, $message)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
283
    {
284
        $response = $this->createResponse($provider, $destination);
285
        $response->setStatus([
286
            'Code' => Constants::STATUS_RESPONDER,
287
            'SubCode' => Constants::STATUS_AUTHN_FAILED,
288
            'Message' => $message
289
        ]);
290
291
        return $response;
292
    }
293
294
    /**
295
     * Response that indicates that the authentication could not be performed correctly. In this context it means
296
     * that the upstream GSSP did not responsd with the same NameID as we request to authenticate in the AuthnRequest
297
     *
298
     * @param Provider $provider
299
     * @param string $destination
300
     * @return SAMLResponse
301
     */
302 View Code Duplication
    private function createAuthnFailedResponse(Provider $provider, $destination)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
303
    {
304
        $response = $this->createResponse($provider, $destination);
305
        $response->setStatus(
306
            [
307
                'Code' => Constants::STATUS_RESPONDER,
308
                'SubCode' => Constants::STATUS_AUTHN_FAILED,
309
            ]
310
        );
311
312
        return $response;
313
    }
314
315
    /**
316
     * Creates a standard response with default status Code (success)
317
     *
318
     * @param Provider $provider
319
     * @param string $destination
320
     * @return SAMLResponse
321
     */
322
    private function createResponse(Provider $provider, $destination)
323
    {
324
        $context = $this->getResponseContext();
325
326
        $response = new SAMLResponse();
327
        $response->setDestination($destination);
328
        $response->setIssuer($context->getIssuer());
329
        $response->setIssueInstant((new DateTime('now'))->getTimestamp());
330
        $response->setInResponseTo($provider->getStateHandler()->getRequestId());
331
332
        return $response;
333
    }
334
335
    /**
336
     * @param string $serviceProvider
337
     * @return \Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider
338
     */
339
    private function getServiceProvider($serviceProvider)
340
    {
341
        /**
342
         * @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders
343
         */
344
        $connectedServiceProviders = $this->get('gssp.connected_service_providers');
345
        return $connectedServiceProviders->getConfigurationOf($serviceProvider);
346
    }
347
348
    /**
349
     * @return LoginService
350
     */
351
    private function getGsspLoginService()
352
    {
353
        return $this->get('gssp.service.gssp.login');
354
    }
355
356
    /**
357
     * @return SecondFactorVerificationService
358
     */
359
    private function getGsspSecondFactorVerificationService()
360
    {
361
        return $this->get('gssp.service.gssp.second_factor_verification');
362
    }
363
364
    /**
365
     * @return ConsumeAssertionService
366
     */
367
    private function getGsspConsumeAssertionService()
368
    {
369
        return $this->get('gssp.service.gssp.consume_assertion');
370
    }
371
372
    /**
373
     * @param Provider $provider
374
     * @return ProxyResponseFactory
375
     */
376
    private function getProxyResponseFactory(Provider $provider)
377
    {
378
        return $this->get('gssp.provider.' . $provider->getName() . '.response_proxy');
379
    }
380
381
    /**
382
     * @return \Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext
383
     */
384 View Code Duplication
    public function getResponseContext()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
385
    {
386
        $stateHandler = $this->get('gateway.proxy.state_handler');
387
388
        $responseContextServiceId = $stateHandler->getResponseContextServiceId();
389
390
        if (!$responseContextServiceId) {
391
            return $this->get(GatewayController::RESPONSE_CONTEXT_SERVICE_ID);
392
        }
393
394
        return $this->get($responseContextServiceId);
395
    }
396
}
397