Completed
Push — bugfix/use-correct-response-co... ( 85ee05...645e83 )
by Michiel
01:40
created

SamlProxyController::getResponseContext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 12
Ratio 100 %

Importance

Changes 0
Metric Value
dl 12
loc 12
rs 9.8666
c 0
b 0
f 0
cc 2
nc 2
nop 0
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
155
        } catch (InvalidSubjectException $e) {
156
            return $this->renderSamlResponse(
157
                'recoverableError',
158
                $provider->getStateHandler(),
159
                $this->createAuthnFailedResponse(
160
                    $provider,
161
                    $this->getDestination($provider->getStateHandler())
162
                )
163
            );
164
        } catch (SecondfactorVerfificationRequiredException $e) {
165
            return $this->forward('SurfnetStepupGatewayGatewayBundle:SecondFactor:gssfVerified');
166
        } catch (Exception $e) {
167
            throw $e;
168
        }
169
170
        return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
171
    }
172
173
    /**
174
     * @param string $provider
175
     * @return XMLResponse
176
     */
177
    public function metadataAction($provider)
178
    {
179
        $provider = $this->getProvider($provider);
180
181
        /** @var \Surfnet\SamlBundle\Metadata\MetadataFactory $factory */
182
        $factory = $this->get('gssp.provider.' . $provider->getName() . '.metadata.factory');
183
184
        return new XMLResponse($factory->generate());
185
    }
186
187
    /**
188
     * @param string $provider
189
     * @return \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider
190
     */
191
    private function getProvider($provider)
192
    {
193
        /** @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ProviderRepository $providerRepository */
194
        $providerRepository = $this->get('gssp.provider_repository');
195
196
        if (!$providerRepository->has($provider)) {
197
            throw new NotFoundHttpException(
198
                sprintf('Requested GSSP "%s" does not exist or is not registered', $provider)
199
            );
200
        }
201
202
        return $providerRepository->get($provider);
203
    }
204
205
    /**
206
     * @param StateHandler $stateHandler
207
     * @return string
208
     */
209
    private function getDestination(StateHandler $stateHandler)
210
    {
211
        if ($stateHandler->secondFactorVerificationRequested()) {
212
            // This can either be a SFO or 'regular' SSO authentication. Both use a ResponseContext service of their own
213
            $responseContextServiceId = $stateHandler->getResponseContextServiceId();
214
            // GSSP verification action, return to SP from GatewayController state!
215
            $destination = $this->get($responseContextServiceId)->getDestination();
216
        } else {
217
            // GSSP registration action, return to SP remembered in ssoAction().
218
            $serviceProvider = $this->getServiceProvider(
219
                $stateHandler->getRequestServiceProvider()
220
            );
221
222
            $destination = $serviceProvider->determineAcsLocation(
223
                $stateHandler->getRequestAssertionConsumerServiceUrl(),
224
                $this->get('logger')
225
            );
226
        }
227
228
        return $destination;
229
    }
230
231
    /**
232
     * @param string $view
233
     * @param StateHandler $stateHandler
234
     * @param SAMLResponse $response
235
     * @return Response
236
     */
237
    public function renderSamlResponse($view, StateHandler $stateHandler, SAMLResponse $response)
238
    {
239
        $parameters = [
240
            'acu' => $response->getDestination(),
241
            'response' => $this->getResponseAsXML($response),
242
            'relayState' => $stateHandler->getRelayState(),
243
        ];
244
245
        $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...
246
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:' . $view . '.html.twig',
247
            $parameters
248
        );
249
250
        // clear the state so we can call again :)
251
        $stateHandler->clear();
252
253
        return $response;
254
    }
255
256
    /**
257
     * @param SAMLResponse $response
258
     * @return string
259
     */
260
    private function getResponseAsXML(SAMLResponse $response)
261
    {
262
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
263
    }
264
265
    /**
266
     * Response that indicates that an error occurred in the responder (the gateway). Used to indicate that we could
267
     * not process the response we received from the upstream GSSP
268
     *
269
     * @param Provider $provider
270
     * @param string $destination
271
     * @return SAMLResponse
272
     */
273 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...
274
    {
275
        $response = $this->createResponse($provider, $destination);
276
        $response->setStatus([
277
            'Code' => Constants::STATUS_RESPONDER,
278
            'SubCode' => Constants::STATUS_AUTHN_FAILED,
279
            'Message' => $message
280
        ]);
281
282
        return $response;
283
    }
284
285
    /**
286
     * Response that indicates that the authentication could not be performed correctly. In this context it means
287
     * that the upstream GSSP did not responsd with the same NameID as we request to authenticate in the AuthnRequest
288
     *
289
     * @param Provider $provider
290
     * @param string $destination
291
     * @return SAMLResponse
292
     */
293 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...
294
    {
295
        $response = $this->createResponse($provider, $destination);
296
        $response->setStatus(
297
            [
298
                'Code' => Constants::STATUS_RESPONDER,
299
                'SubCode' => Constants::STATUS_AUTHN_FAILED,
300
            ]
301
        );
302
303
        return $response;
304
    }
305
306
    /**
307
     * Creates a standard response with default status Code (success)
308
     *
309
     * @param Provider $provider
310
     * @param string $destination
311
     * @return SAMLResponse
312
     */
313
    private function createResponse(Provider $provider, $destination)
314
    {
315
        $context = $this->getResponseContext();
316
317
        $response = new SAMLResponse();
318
        $response->setDestination($destination);
319
        $response->setIssuer($context->getIssuer());
320
        $response->setIssueInstant((new DateTime('now'))->getTimestamp());
321
        $response->setInResponseTo($provider->getStateHandler()->getRequestId());
322
323
        return $response;
324
    }
325
326
    /**
327
     * @param string $serviceProvider
328
     * @return \Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider
329
     */
330
    private function getServiceProvider($serviceProvider)
331
    {
332
        /**
333
         * @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders
334
         */
335
        $connectedServiceProviders = $this->get('gssp.connected_service_providers');
336
        return $connectedServiceProviders->getConfigurationOf($serviceProvider);
337
    }
338
339
    /**
340
     * @return LoginService
341
     */
342
    private function getGsspLoginService()
343
    {
344
        return $this->get('gssp.service.gssp.login');
345
    }
346
347
    /**
348
     * @return SecondFactorVerificationService
349
     */
350
    private function getGsspSecondFactorVerificationService()
351
    {
352
        return $this->get('gssp.service.gssp.second_factor_verification');
353
    }
354
355
    /**
356
     * @return ConsumeAssertionService
357
     */
358
    private function getGsspConsumeAssertionService()
359
    {
360
        return $this->get('gssp.service.gssp.consume_assertion');
361
    }
362
363
    /**
364
     * @param Provider $provider
365
     * @return ProxyResponseFactory
366
     */
367
    private function getProxyResponseFactory(Provider $provider)
368
    {
369
        return $this->get('gssp.provider.' . $provider->getName() . '.response_proxy');
370
    }
371
372
    /**
373
     * @return \Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext
374
     */
375 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...
376
    {
377
        $stateHandler = $this->get('gateway.proxy.state_handler');
378
379
        $responseContextServiceId = $stateHandler->getResponseContextServiceId();
380
381
        if (!$responseContextServiceId) {
382
            return $this->get(GatewayController::RESPONSE_CONTEXT_SERVICE_ID);
383
        }
384
385
        return $this->get($responseContextServiceId);
386
    }
387
}
388