Completed
Push — develop ( 63e537...fefeee )
by Michiel
01:58 queued 11s
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 $provider
102
     * @param $subjectNameId
103
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
104
     */
105
    public function sendSecondFactorVerificationAuthnRequestAction($provider, $subjectNameId)
106
    {
107
        $provider = $this->getProvider($provider);
108
109
        $gsspSecondFactorVerificationService = $this->getGsspSecondFactorVerificationService();
110
111
        $authnRequest = $gsspSecondFactorVerificationService->sendSecondFactorVerificationAuthnRequest($provider, $subjectNameId);
112
113
        /** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */
114
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
115
116
        return $redirectBinding->createResponseFor($authnRequest);
117
    }
118
119
    /**
120
     * Process an assertion received from the remote GSSP application.
121
     *
122
     * The GSSP application sent an assertion back to the gateway. When
123
     * successful, the user is sent back to:
124
     *
125
     *  1. in case of registration: back to the originating SP (SelfService or RA)
126
     *  2. in case of verification: internal redirect to SecondFactorController
127
     *
128
     * @param string $provider
129
     * @param Request $httpRequest
130
     * @return \Symfony\Component\HttpFoundation\Response
131
     * @throws Exception
132
     */
133
    public function consumeAssertionAction($provider, Request $httpRequest)
134
    {
135
        $provider = $this->getProvider($provider);
136
137
        $consumeAssertionService = $this->getGsspConsumeAssertionService();
138
        $proxyResponseFactory = $this->getProxyResponseFactory($provider);
139
140
        try {
141
            $response = $consumeAssertionService->consumeAssertion($provider, $httpRequest, $proxyResponseFactory);
142
        } catch (ResponseFailureException $e) {
143
            $response = $this->createResponseFailureResponse(
144
                $provider,
145
                $this->getDestination($provider->getStateHandler()),
146
                $e->getMessage()
147
            );
148
            return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
149
150
        } catch (InvalidSubjectException $e) {
151
            return $this->renderSamlResponse(
152
                'recoverableError',
153
                $provider->getStateHandler(),
154
                $this->createAuthnFailedResponse(
155
                    $provider,
156
                    $this->getDestination($provider->getStateHandler())
157
                )
158
            );
159
        } catch (SecondfactorVerfificationRequiredException $e) {
160
            return $this->forward('SurfnetStepupGatewayGatewayBundle:SecondFactor:gssfVerified');
161
        } catch (Exception $e) {
162
            throw $e;
163
        }
164
165
        return $this->renderSamlResponse('consumeAssertion', $provider->getStateHandler(), $response);
166
    }
167
168
    /**
169
     * @param string $provider
170
     * @return XMLResponse
171
     */
172
    public function metadataAction($provider)
173
    {
174
        $provider = $this->getProvider($provider);
175
176
        /** @var \Surfnet\SamlBundle\Metadata\MetadataFactory $factory */
177
        $factory = $this->get('gssp.provider.' . $provider->getName() . '.metadata.factory');
178
179
        return new XMLResponse($factory->generate());
180
    }
181
182
    /**
183
     * @param string $provider
184
     * @return \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider
185
     */
186
    private function getProvider($provider)
187
    {
188
        /** @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ProviderRepository $providerRepository */
189
        $providerRepository = $this->get('gssp.provider_repository');
190
191
        if (!$providerRepository->has($provider)) {
192
            throw new NotFoundHttpException(
193
                sprintf('Requested GSSP "%s" does not exist or is not registered', $provider)
194
            );
195
        }
196
197
        return $providerRepository->get($provider);
198
    }
199
200
    /**
201
     * @param StateHandler $stateHandler
202
     * @return string
203
     */
204
    private function getDestination(StateHandler $stateHandler)
205
    {
206
        if ($stateHandler->secondFactorVerificationRequested()) {
207
            // GSSP verification action, return to SP from GatewayController state!
208
            $destination = $this->get('gateway.proxy.response_context')->getDestination();
209
        } else {
210
            // GSSP registration action, return to SP remembered in ssoAction().
211
            $serviceProvider = $this->getServiceProvider(
212
                $stateHandler->getRequestServiceProvider()
213
            );
214
215
            $destination = $serviceProvider->determineAcsLocation(
216
                $stateHandler->getRequestAssertionConsumerServiceUrl(),
217
                $this->get('logger')
218
            );
219
        }
220
221
        return $destination;
222
    }
223
224
    /**
225
     * @param string         $view
226
     * @param StateHandler   $stateHandler
227
     * @param SAMLResponse $response
228
     * @return Response
229
     */
230
    public function renderSamlResponse($view, StateHandler $stateHandler, SAMLResponse $response)
231
    {
232
        $parameters = [
233
            'acu'        => $response->getDestination(),
234
            'response'   => $this->getResponseAsXML($response),
235
            'relayState' => $stateHandler->getRelayState()
236
        ];
237
238
        $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...
239
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:' . $view . '.html.twig',
240
            $parameters
241
        );
242
243
        // clear the state so we can call again :)
244
        $stateHandler->clear();
245
246
        return $response;
247
    }
248
249
    /**
250
     * @param SAMLResponse $response
251
     * @return string
252
     */
253
    private function getResponseAsXML(SAMLResponse $response)
254
    {
255
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
256
    }
257
258
    /**
259
     * Response that indicates that an error occurred in the responder (the gateway). Used to indicate that we could
260
     * not process the response we received from the upstream GSSP
261
     *
262
     * @param Provider $provider
263
     * @param string $destination
264
     * @return SAMLResponse
265
     */
266 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...
267
    {
268
        $response = $this->createResponse($provider, $destination);
269
        $response->setStatus([
270
            'Code' => Constants::STATUS_RESPONDER,
271
            'SubCode' => Constants::STATUS_AUTHN_FAILED,
272
            'Message' => $message
273
        ]);
274
275
        return $response;
276
    }
277
278
    /**
279
     * Response that indicates that the authentication could not be performed correctly. In this context it means
280
     * that the upstream GSSP did not responsd with the same NameID as we request to authenticate in the AuthnRequest
281
     *
282
     * @param Provider $provider
283
     * @param string $destination
284
     * @return SAMLResponse
285
     */
286 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...
287
    {
288
        $response = $this->createResponse($provider, $destination);
289
        $response->setStatus([
290
            'Code'    => Constants::STATUS_RESPONDER,
291
            'SubCode' => Constants::STATUS_AUTHN_FAILED
292
        ]);
293
294
        return $response;
295
    }
296
297
    /**
298
     * Creates a standard response with default status Code (success)
299
     *
300
     * @param Provider $provider
301
     * @param string $destination
302
     * @return SAMLResponse
303
     */
304
    private function createResponse(Provider $provider, $destination)
305
    {
306
        $context = $this->getResponseContext();
307
308
        $response = new SAMLResponse();
309
        $response->setDestination($destination);
310
        $response->setIssuer($context->getIssuer());
311
        $response->setIssueInstant((new DateTime('now'))->getTimestamp());
312
        $response->setInResponseTo($provider->getStateHandler()->getRequestId());
313
314
        return $response;
315
    }
316
317
    /**
318
     * @param string $serviceProvider
319
     * @return \Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider
320
     */
321
    private function getServiceProvider($serviceProvider)
322
    {
323
        /**
324
         * @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders
325
         */
326
        $connectedServiceProviders = $this->get('gssp.connected_service_providers');
327
        return $connectedServiceProviders->getConfigurationOf($serviceProvider);
328
    }
329
330
    /**
331
     * @return LoginService
332
     */
333
    private function getGsspLoginService()
334
    {
335
        return $this->get('gssp.service.gssp.login');
336
    }
337
338
    /**
339
     * @return SecondFactorVerificationService
340
     */
341
    private function getGsspSecondFactorVerificationService()
342
    {
343
        return $this->get('gssp.service.gssp.second_factor_verification');
344
    }
345
346
    /**
347
     * @return ConsumeAssertionService
348
     */
349
    private function getGsspConsumeAssertionService()
350
    {
351
        return $this->get('gssp.service.gssp.consume_assertion');
352
    }
353
354
    /**
355
     * @param Provider $provider
356
     * @return ProxyResponseFactory
357
     */
358
    private function getProxyResponseFactory(Provider $provider)
359
    {
360
        return $this->get('gssp.provider.' . $provider->getName() . '.response_proxy');
361
    }
362
363
    /**
364
     * @return \Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext
365
     */
366 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...
367
    {
368
        $stateHandler = $this->get('gateway.proxy.state_handler');
369
370
        $responseContextServiceId = $stateHandler->getResponseContextServiceId();
371
372
        if (!$responseContextServiceId) {
373
            return $this->get(GatewayController::RESPONSE_CONTEXT_SERVICE_ID);
374
        }
375
376
        return $this->get($responseContextServiceId);
377
    }
378
}
379