Completed
Push — feature/merge-conflicts ( 06c8b5 )
by Michiel
02:51
created

GatewayController::renderSamlResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
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\GatewayBundle\Controller;
20
21
use SAML2\Response as SAMLResponse;
22
use Surfnet\StepupGateway\GatewayBundle\Exception\InvalidArgumentException;
23
use Surfnet\StepupGateway\GatewayBundle\Exception\RequesterFailureException;
24
use Surfnet\StepupGateway\GatewayBundle\Exception\ResponseFailureException;
25
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
26
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
27
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\ConsumeAssertionService;
28
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\FailedResponseService;
29
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\LoginService;
30
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\RespondService;
31
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
32
use Symfony\Component\HttpFoundation\Request;
33
use Symfony\Component\HttpFoundation\Response;
34
use Symfony\Component\HttpKernel\Exception\HttpException;
35
use function parent;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Cannot use parent as parent because 'parent' is a special class name
Loading history...
36
37
/**
38
 * Entry point for the Stepup login flow.
39
 *
40
 * See docs/GatewayState.md for a high-level diagram on how this controller
41
 * interacts with outside actors and other parts of Stepup.
42
 *
43
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
44
 */
45
class GatewayController extends Controller
46
{
47
    const RESPONSE_CONTEXT_SERVICE_ID = 'gateway.proxy.response_context';
48
    const MODE_SFO = 'sfo';
49
    const MODE_SSO = 'sso';
50
51
    /**
52
     * Receive an AuthnRequest from a service provider.
53
     *
54
     * The service provider is either a Stepup component (SelfService, RA) or
55
     * an external service provider.
56
     *
57
     * This single sign-on action will start a new SAML request to the remote
58
     * IDP configured in Stepup (most likely to be an instance of OpenConext
59
     * EngineBlock).
60
     *
61
     * @param Request $httpRequest
62
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
63
     */
64
    public function ssoAction(Request $httpRequest)
65
    {
66
        /** @var \Psr\Log\LoggerInterface $logger */
67
        $logger = $this->get('logger');
68
69
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
70
        $gatewayLoginService = $this->getGatewayLoginService();
71
72
        $logger->notice('Received AuthnRequest, started processing');
73
74
        try {
75
            $proxyRequest = $gatewayLoginService->singleSignOn($httpRequest);
76
        } catch (RequesterFailureException $e) {
77
            $response = $this->getGatewayFailedResponseService()->createRequesterFailureResponse(
78
                $this->getResponseContext(self::MODE_SSO)
79
            );
80
81
            return $this->renderSamlResponse('consume_assertion', $response, self::MODE_SSO);
82
        }
83
84
        return $redirectBinding->createResponseFor($proxyRequest);
85
    }
86
87
    /**
88
     *
89
     */
90
    public function proxySsoAction()
91
    {
92
        throw new HttpException(418, 'Not Yet Implemented');
93
    }
94
95
    /**
96
     * Receive an AuthnResponse from an identity provider.
97
     *
98
     * The AuthnRequest started in ssoAction() resulted in an AuthnResponse
99
     * from the IDP. This method handles the assertion and forwards the user
100
     * using an internal redirect to the SecondFactorController to start the
101
     * actual second factor verification.
102
     *
103
     * @param Request $request
104
     * @return \Symfony\Component\HttpFoundation\Response
105
     */
106
    public function consumeAssertionAction(Request $request)
107
    {
108
        $responseContext = $this->getResponseContext(self::MODE_SSO);
109
        $gatewayLoginService = $this->getGatewayConsumeAssertionService();
110
111
        try {
112
            $gatewayLoginService->consumeAssertion($request, $responseContext);
113
        } catch (ResponseFailureException $e) {
114
            $response = $this->getGatewayFailedResponseService()->createResponseFailureResponse($responseContext);
115
116
            return $this->renderSamlResponse('unprocessable_response', $response, self::MODE_SSO);
117
        }
118
119
        // Forward to the selectSecondFactorForVerificationSsoAction, this in turn will forward to the correct
120
        // verification action (based on authentication type sso/sfo)
121
        return $this->forward('SurfnetStepupGatewayGatewayBundle:SecondFactor:selectSecondFactorForVerificationSso');
122
    }
123
124
    /**
125
     * Send a SAML response back to the service provider.
126
     *
127
     * Second factor verification handled by SecondFactorController is
128
     * finished. The user was forwarded back to this action with an internal
129
     * redirect. This method sends a AuthnResponse back to the service
130
     * provider in response to the AuthnRequest received in ssoAction().
131
     */
132
    public function respondAction()
133
    {
134
        $responseContext = $this->getResponseContext(self::MODE_SSO);
135
        $gatewayLoginService = $this->getGatewayRespondService();
136
137
        $response = $gatewayLoginService->respond($responseContext);
138
        $gatewayLoginService->resetRespondState($responseContext);
139
140
        return $this->renderSamlResponse('consume_assertion', $response, self::MODE_SSO);
141
    }
142
143
    /**
144
     * This action is also used from the context of SecondFactorOnly authentications
145
     * @param $authenticationMode
146
     * @return Response
147
     */
148
    public function sendLoaCannotBeGivenAction(Request $request)
149
    {
150
        if (!$request->get('authenticationMode', false)) {
151
            throw new RuntimeException('Unable to determine the authentication mode in the sendLoaCannotBeGiven action');
152
        }
153
        $authenticationMode = $request->get('authenticationMode');
154
        $this->supportsAuthenticationMode($authenticationMode);
155
        $responseContext = $this->getResponseContext($authenticationMode);
156
        $gatewayLoginService = $this->getGatewayFailedResponseService();
157
158
        $response = $gatewayLoginService->sendLoaCannotBeGiven($responseContext);
159
160
        return $this->renderSamlResponse('consume_assertion', $response, $authenticationMode);
161
    }
162
163
    /**
164
     * @return Response
165
     */
166
    public function sendAuthenticationCancelledByUserAction()
167
    {
168
        // The authentication mode is read from the parent request, in the meantime a forward was followed, making
169
        // reading the auth mode from the current request impossible.
170
        // @see: \Surfnet\StepupGateway\GatewayBundle\Controller\SecondFactorController::cancelAuthenticationAction
171
        $requestStack = $this->get('request_stack');
172
        $request = $requestStack->getParentRequest();
173
        if (!$request->get('authenticationMode', false)) {
174
            throw new RuntimeException('Unable to determine the authentication mode in the sendAuthenticationCancelledByUser action');
175
        }
176
        $authenticationMode = $request->get('authenticationMode');
177
178
        $this->supportsAuthenticationMode($authenticationMode);
179
        $responseContext = $this->getResponseContext($authenticationMode);
180
        $gatewayLoginService = $this->getGatewayFailedResponseService();
181
182
        $response = $gatewayLoginService->sendAuthenticationCancelledByUser($responseContext);
183
184
        return $this->renderSamlResponse('consume_assertion', $response, $authenticationMode);
185
    }
186
187
    /**
188
     * @param string $view
189
     * @param SAMLResponse $response
190
     * @param $authenticationMode
191
     * @return Response
192
     */
193
    public function renderSamlResponse($view, SAMLResponse $response, $authenticationMode)
194
    {
195
        $this->supportsAuthenticationMode($authenticationMode);
196
        $responseContext = $this->getResponseContext($authenticationMode);
197
198
        return $this->render($view, [
199
            'acu'        => $responseContext->getDestination(),
200
            'response'   => $this->getResponseAsXML($response),
201
            'relayState' => $responseContext->getRelayState()
202
        ]);
203
    }
204
205
    /**
206
     * @param string   $view
207
     * @param array    $parameters
208
     * @param Response $response
209
     * @return Response
210
     */
211
    public function render($view, array $parameters = array(), Response $response = null): Response
212
    {
213
        return parent::render(
214
            'SurfnetStepupGatewayGatewayBundle:gateway:' . $view . '.html.twig',
215
            $parameters,
216
            $response
217
        );
218
    }
219
220
    /**
221
     * @return ResponseContext
222
     */
223
    public function getResponseContext($authenticationMode)
224
    {
225
        switch ($authenticationMode) {
226
            case self::MODE_SFO:
227
                return $this->get($this->get('gateway.proxy.sfo.state_handler')->getResponseContextServiceId());
228
                break;
229
            case self::MODE_SSO:
230
                return $this->get($this->get('gateway.proxy.sso.state_handler')->getResponseContextServiceId());
231
                break;
232
        }
233
    }
234
235
    /**
236
     * @param SAMLResponse $response
237
     * @return string
238
     */
239
    private function getResponseAsXML(SAMLResponse $response)
240
    {
241
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
242
    }
243
244
    /**
245
     * @return LoginService
246
     */
247
    private function getGatewayLoginService()
248
    {
249
        return $this->get('gateway.service.gateway.login');
250
    }
251
252
    /**
253
     * @return ConsumeAssertionService
254
     */
255
    private function getGatewayConsumeAssertionService()
256
    {
257
        return $this->get('gateway.service.gateway.consume_assertion');
258
    }
259
260
    /**
261
     * @return RespondService
262
     */
263
    private function getGatewayRespondService()
264
    {
265
        return $this->get('gateway.service.gateway.respond');
266
    }
267
268
    /**
269
     * @return FailedResponseService
270
     */
271
    private function getGatewayFailedResponseService()
272
    {
273
        return $this->get('gateway.service.gateway.failed_response');
274
    }
275
276
    private function supportsAuthenticationMode($authenticationMode)
277
    {
278
        if (!($authenticationMode === self::MODE_SSO || $authenticationMode === self::MODE_SFO)) {
279
            throw new InvalidArgumentException('Invalid authentication mode requested');
280
        }
281
    }
282
}
283