Completed
Push — feature/refactor-gateway-contr... ( 4996d0...48680d )
by
unknown
02:33
created

getGsspConsumeAssertionService()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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\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\ConsumeAssertionService;
34
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\LoginService;
35
use Surfnet\StepupGateway\SamlStepupProviderBundle\Service\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();
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $gsspSecondFactorVerificationService exceeds the maximum configured length of 30.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
109
110
        $authnRequest = $gsspSecondFactorVerificationService->sendSecondFactorVerificationAuthnRequest($provider, $subjectNameId);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 130 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
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 View Code Duplication
    public function renderSamlResponse($view, StateHandler $stateHandler, SAMLResponse $response)
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...
229
    {
230
        $response = $this->render($view, [
231
            'acu'        => $response->getDestination(),
232
            'response'   => $this->getResponseAsXML($response),
233
            'relayState' => $stateHandler->getRelayState()
234
        ]);
235
236
        // clear the state so we can call again :)
237
        $stateHandler->clear();
238
239
        return $response;
240
    }
241
242
    /**
243
     * @param string   $view
244
     * @param array    $parameters
245
     * @param Response $response
0 ignored issues
show
Documentation introduced by
Should the type for parameter $response not be null|Response?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
246
     * @return Response
247
     */
248
    public function render($view, array $parameters = array(), Response $response = null)
249
    {
250
        return parent::render(
251
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:' . $view . '.html.twig',
252
            $parameters,
253
            $response
254
        );
255
    }
256
257
    /**
258
     * @param SAMLResponse $response
259
     * @return string
260
     */
261
    private function getResponseAsXML(SAMLResponse $response)
262
    {
263
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
264
    }
265
266
    /**
267
     * Response that indicates that an error occurred in the responder (the gateway). Used to indicate that we could
268
     * not process the response we received from the upstream GSSP
269
     *
270
     * @param Provider $provider
271
     * @param string $destination
272
     * @return SAMLResponse
273
     */
274
    private function createResponseFailureResponse(Provider $provider, $destination)
275
    {
276
        $response = $this->createResponse($provider, $destination);
277
        $response->setStatus(['Code' => Constants::STATUS_RESPONDER]);
278
279
        return $response;
280
    }
281
282
    /**
283
     * Response that indicates that the authentication could not be performed correctly. In this context it means
284
     * that the upstream GSSP did not responsd with the same NameID as we request to authenticate in the AuthnRequest
285
     *
286
     * @param Provider $provider
287
     * @param string $destination
288
     * @return SAMLResponse
289
     */
290
    private function createAuthnFailedResponse(Provider $provider, $destination)
291
    {
292
        $response = $this->createResponse($provider, $destination);
293
        $response->setStatus([
294
            'Code'    => Constants::STATUS_RESPONDER,
295
            'SubCode' => Constants::STATUS_AUTHN_FAILED
296
        ]);
297
298
        return $response;
299
    }
300
301
    /**
302
     * Creates a standard response with default status Code (success)
303
     *
304
     * @param Provider $provider
305
     * @param string $destination
306
     * @return SAMLResponse
307
     */
308
    private function createResponse(Provider $provider, $destination)
309
    {
310
        $response = new SAMLResponse();
311
        $response->setDestination($destination);
312
        $response->setIssuer($provider->getIdentityProvider()->getEntityId());
313
        $response->setIssueInstant((new DateTime('now'))->getTimestamp());
314
        $response->setInResponseTo($provider->getStateHandler()->getRequestId());
315
316
        return $response;
317
    }
318
319
    /**
320
     * @param string $serviceProvider
321
     * @return \Surfnet\StepupGateway\GatewayBundle\Entity\ServiceProvider
322
     */
323
    private function getServiceProvider($serviceProvider)
324
    {
325
        /**
326
         * @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 125 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
327
         */
328
        $connectedServiceProviders = $this->get('gssp.connected_service_providers');
329
        return $connectedServiceProviders->getConfigurationOf($serviceProvider);
330
    }
331
332
    /**
333
     * @return LoginService
334
     */
335
    private function getGsspLoginService()
336
    {
337
        return $this->get('gssp.service.gssp.login');
338
    }
339
340
    /**
341
     * @return SecondFactorVerificationService
342
     */
343
    private function getGsspSecondFactorVerificationService()
344
    {
345
        return $this->get('gssp.service.gssp.second_factor_verification');
346
    }
347
348
    /**
349
     * @return ConsumeAssertionService
350
     */
351
    private function getGsspConsumeAssertionService()
352
    {
353
        return $this->get('gssp.service.gssp.consume_assertion');
354
    }
355
356
    /**
357
     * @param Provider $provider
358
     * @return ProxyResponseFactory
359
     */
360
    private function getProxyResponseFactory(Provider $provider)
361
    {
362
        return $this->get('gssp.provider.' . $provider->getName() . '.response_proxy');
363
    }
364
}
365