Passed
Push — develop ( d2d0cb...6c46b1 )
by Pieter van der
02:42
created

sendAuthenticationCancelledByUser()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 0
dl 0
loc 19
rs 9.9332
c 0
b 0
f 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\GatewayBundle\Controller;
20
21
use SAML2\Response as SAMLResponse;
22
use Surfnet\StepupGateway\GatewayBundle\Container\ContainerController;
23
use Surfnet\StepupGateway\GatewayBundle\Exception\InvalidArgumentException;
24
use Surfnet\StepupGateway\GatewayBundle\Exception\RequesterFailureException;
25
use Surfnet\StepupGateway\GatewayBundle\Exception\ResponseFailureException;
26
use Surfnet\StepupGateway\GatewayBundle\Exception\RuntimeException;
27
use Surfnet\StepupGateway\GatewayBundle\Saml\ResponseContext;
28
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\ConsumeAssertionService;
29
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\FailedResponseService;
30
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\LoginService;
31
use Surfnet\StepupGateway\GatewayBundle\Service\Gateway\RespondService;
32
use Surfnet\StepupGateway\SecondFactorOnlyBundle\Adfs\ResponseHelper;
33
use Symfony\Component\HttpFoundation\Request;
34
use Symfony\Component\HttpFoundation\Response;
35
use Symfony\Component\HttpKernel\Exception\HttpException;
36
use Symfony\Component\Routing\Annotation\Route;
37
38
/**
39
 * Entry point for the Stepup login flow.
40
 *
41
 * See docs/GatewayState.md for a high-level diagram on how this controller
42
 * interacts with outside actors and other parts of Stepup.
43
 *
44
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
45
 */
46
class GatewayController extends ContainerController
47
{
48
    public const RESPONSE_CONTEXT_SERVICE_ID = 'gateway.proxy.response_context';
49
    public const MODE_SFO = 'sfo';
50
    public const MODE_SSO = 'sso';
51
52
    /**
53
     * Receive an AuthnRequest from a service provider.
54
     *
55
     * The service provider is either a Stepup component (SelfService, RA) or
56
     * an external service provider.
57
     *
58
     * This single sign-on action will start a new SAML request to the remote
59
     * IDP configured in Stepup (most likely to be an instance of OpenConext
60
     * EngineBlock).
61
     *
62
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
63
     */
64
    #[Route(
65
        path: '/authentication/single-sign-on',
66
        name: 'gateway_identityprovider_sso',
67
        methods: ['GET', 'POST']
68
    )]
69
    public function sso(Request $httpRequest)
70
    {
71
        /** @var \Psr\Log\LoggerInterface $logger */
72
        $logger = $this->get('logger');
73
74
        $redirectBinding = $this->get('surfnet_saml.http.redirect_binding');
75
        $gatewayLoginService = $this->getGatewayLoginService();
76
77
        $logger->notice('Received AuthnRequest, started processing');
78
79
        try {
80
            $proxyRequest = $gatewayLoginService->singleSignOn($httpRequest);
81
        } catch (RequesterFailureException) {
82
            $response = $this->getGatewayFailedResponseService()->createRequesterFailureResponse(
83
                $this->getResponseContext(self::MODE_SSO),
84
            );
85
86
            return $this->renderSamlResponse('consume_assertion', $response, $httpRequest, self::MODE_SSO);
87
        }
88
89
        return $redirectBinding->createResponseFor($proxyRequest);
90
    }
91
92
    #[Route(
93
        path: '/authentication/single-sign-on/{idpKey}',
94
        name: 'gateway_identityprovider_sso_proxy',
95
        methods: ['POST']
96
    )]
97
    public function proxySso(): never
98
    {
99
        throw new HttpException(418, 'Not Yet Implemented');
100
    }
101
102
    /**
103
     * Receive an AuthnResponse from an identity provider.
104
     *
105
     * The AuthnRequest started in ssoAction() resulted in an AuthnResponse
106
     * from the IDP. This method handles the assertion and forwards the user
107
     * using an internal redirect to the SecondFactorController to start the
108
     * actual second factor verification.
109
     */
110
    #[Route(
111
        path: '/authentication/consume-assertion',
112
        name: 'gateway_serviceprovider_consume_assertion',
113
        methods: ['POST']
114
    )]
115
    public function consumeAssertion(Request $request): Response
116
    {
117
        $responseContext = $this->getResponseContext(self::MODE_SSO);
118
        $gatewayLoginService = $this->getGatewayConsumeAssertionService();
119
120
        try {
121
            $gatewayLoginService->consumeAssertion($request, $responseContext);
122
        } catch (ResponseFailureException) {
123
            $response = $this->getGatewayFailedResponseService()->createResponseFailureResponse($responseContext);
124
125
            return $this->renderSamlResponse('unprocessable_response', $response, $request, self::MODE_SSO);
126
        }
127
128
        // Forward to the selectSecondFactorForVerificationSsoAction, this in turn will forward to the correct
129
        // verification action (based on authentication type sso/sfo)
130
        return $this->forward('Surfnet\StepupGateway\GatewayBundle\Controller\SecondFactorController::selectSecondFactorForVerificationSso');
131
    }
132
133
    /**
134
     * Send a SAML response back to the service provider.
135
     *
136
     * Second factor verification handled by SecondFactorController is
137
     * finished. The user was forwarded back to this action with an internal
138
     * redirect. This method sends a AuthnResponse back to the service
139
     * provider in response to the AuthnRequest received in ssoAction().
140
     */
141
    public function respond(Request $request): Response
142
    {
143
        $responseContext = $this->getResponseContext(self::MODE_SSO);
144
        $gatewayLoginService = $this->getGatewayRespondService();
145
146
        $response = $gatewayLoginService->respond($responseContext);
147
        $gatewayLoginService->resetRespondState($responseContext);
148
149
        return $this->renderSamlResponse('consume_assertion', $response, $request, self::MODE_SSO);
150
    }
151
152
    /**
153
     * This action is also used from the context of SecondFactorOnly authentications.
154
     */
155
    public function sendLoaCannotBeGiven(Request $request): Response
156
    {
157
        if (!$request->get('authenticationMode', false)) {
158
            throw new RuntimeException('Unable to determine the authentication mode in the sendLoaCannotBeGiven action');
159
        }
160
        $authenticationMode = $request->get('authenticationMode');
161
        $this->supportsAuthenticationMode($authenticationMode);
162
        $responseContext = $this->getResponseContext($authenticationMode);
163
        $gatewayLoginService = $this->getGatewayFailedResponseService();
164
165
        $response = $gatewayLoginService->sendLoaCannotBeGiven($responseContext);
166
167
        return $this->renderSamlResponse('consume_assertion', $response, $request, $authenticationMode);
168
    }
169
170
    public function sendAuthenticationCancelledByUser(): Response
171
    {
172
        // The authentication mode is read from the parent request, in the meantime a forward was followed, making
173
        // reading the auth mode from the current request impossible.
174
        // @see: \Surfnet\StepupGateway\GatewayBundle\Controller\SecondFactorController::cancelAuthenticationAction
175
        $requestStack = $this->get('request_stack');
176
        $request = $requestStack->getParentRequest();
177
        if (!$request->get('authenticationMode', false)) {
178
            throw new RuntimeException('Unable to determine the authentication mode in the sendAuthenticationCancelledByUser action');
179
        }
180
        $authenticationMode = $request->get('authenticationMode');
181
182
        $this->supportsAuthenticationMode($authenticationMode);
183
        $responseContext = $this->getResponseContext($authenticationMode);
184
        $gatewayLoginService = $this->getGatewayFailedResponseService();
185
186
        $response = $gatewayLoginService->sendAuthenticationCancelledByUser($responseContext);
187
188
        return $this->renderSamlResponse('consume_assertion', $response, $request, $authenticationMode);
189
    }
190
191
    public function renderSamlResponse(
192
        string $view,
193
        SAMLResponse $response,
194
        Request $request,
195
        string $authenticationMode,
196
    ): Response {
197
        $logger = $this->get('logger');
198
        /** @var ResponseHelper $responseHelper */
199
        $responseHelper = $this->get('second_factor_only.adfs.response_helper');
200
201
        $this->supportsAuthenticationMode($authenticationMode);
202
        $responseContext = $this->getResponseContext($authenticationMode);
203
204
        $parameters = [
205
            'acu' => $responseContext->getDestination(),
206
            'response' => $this->getResponseAsXML($response),
207
            'relayState' => $responseContext->getRelayState(),
208
        ];
209
210
        // Test if we should add ADFS response parameters
211
        $inResponseTo = $responseContext->getInResponseTo();
212
        if ($responseHelper->isAdfsResponse($inResponseTo)) {
213
            $adfsParameters = $responseHelper->retrieveAdfsParameters();
214
            $logMessage = 'Responding with additional ADFS parameters, in response to request: "%s", with view: "%s"';
215
            if (!$response->isSuccess()) {
216
                $logMessage = 'Responding with an AuthnFailed SamlResponse with ADFS parameters, in response to AR: "%s", with view: "%s"';
217
            }
218
            $logger->notice(sprintf($logMessage, $inResponseTo, $view));
219
            $parameters['adfs'] = $adfsParameters;
220
            $parameters['acu'] = $responseContext->getDestinationForAdfs();
221
        }
222
223
        $httpResponse = $this->render($view, $parameters);
224
225
        if ($response->isSuccess()) {
226
            $ssoCookieService = $this->get('gateway.service.sso_2fa_cookie');
227
            $ssoCookieService->handleSsoOn2faCookieStorage($responseContext, $request, $httpResponse);
228
        }
229
230
        return $httpResponse;
231
    }
232
233
    /**
234
     * @param string $view
235
     */
236
    public function render($view, array $parameters = [], ?Response $response = null): Response
237
    {
238
        return parent::render(
239
            '@default/gateway/'.$view.'.html.twig',
240
            $parameters,
241
            $response,
242
        );
243
    }
244
245
    public function getResponseContext($authenticationMode): ResponseContext
246
    {
247
        return match ($authenticationMode) {
248
            self::MODE_SFO => $this->get($this->get('gateway.proxy.sfo.state_handler')->getResponseContextServiceId()),
249
            self::MODE_SSO => $this->get($this->get('gateway.proxy.sso.state_handler')->getResponseContextServiceId()),
250
            default => throw new RuntimeException('Invalid authentication mode requested'),
251
        };
252
    }
253
254
    private function getResponseAsXML(SAMLResponse $response): string
255
    {
256
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
0 ignored issues
show
Bug introduced by
The method saveXML() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

256
        return base64_encode($response->toUnsignedXML()->ownerDocument->/** @scrutinizer ignore-call */ saveXML());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
257
    }
258
259
    /**
260
     * @return LoginService
261
     */
262
    private function getGatewayLoginService()
263
    {
264
        return $this->get('gateway.service.gateway.login');
265
    }
266
267
    /**
268
     * @return ConsumeAssertionService
269
     */
270
    private function getGatewayConsumeAssertionService()
271
    {
272
        return $this->get('gateway.service.gateway.consume_assertion');
273
    }
274
275
    /**
276
     * @return RespondService
277
     */
278
    private function getGatewayRespondService()
279
    {
280
        return $this->get('gateway.service.gateway.respond');
281
    }
282
283
    /**
284
     * @return FailedResponseService
285
     */
286
    private function getGatewayFailedResponseService()
287
    {
288
        return $this->get('gateway.service.gateway.failed_response');
289
    }
290
291
    private function supportsAuthenticationMode($authenticationMode): void
292
    {
293
        if (self::MODE_SSO !== $authenticationMode && self::MODE_SFO !== $authenticationMode) {
294
            throw new InvalidArgumentException('Invalid authentication mode requested');
295
        }
296
    }
297
}
298