GatewayController::respond()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 9
rs 10
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
    #[\Symfony\Component\Routing\Attribute\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
    #[\Symfony\Component\Routing\Attribute\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
    #[\Symfony\Component\Routing\Attribute\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, string $authenticationMode): Response
156
    {
157
        $this->supportsAuthenticationMode($authenticationMode);
158
        $responseContext = $this->getResponseContext($authenticationMode);
159
        $gatewayLoginService = $this->getGatewayFailedResponseService();
160
161
        $response = $gatewayLoginService->sendLoaCannotBeGiven($responseContext);
162
163
        return $this->renderSamlResponse('consume_assertion', $response, $request, $authenticationMode);
164
    }
165
166
    public function sendAuthenticationCancelledByUser(): Response
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::cancelAuthentication
171
        $requestStack = $this->get('request_stack');
172
        $request = $requestStack->getParentRequest();
173
174
        if ($request === null) {
175
            throw new RuntimeException('No request');
176
        }
177
178
        $authenticationMode = $request->query->getString('authenticationMode');
179
        if ($authenticationMode === '') {
180
            $authenticationMode = $request->request->getString('authenticationMode');
181
        }
182
183
        if ($authenticationMode === '') {
184
            throw new RuntimeException('Unable to determine the authentication mode in the sendAuthenticationCancelledByUser action');
185
        }
186
187
        $this->supportsAuthenticationMode($authenticationMode);
188
        $responseContext = $this->getResponseContext($authenticationMode);
189
        $gatewayLoginService = $this->getGatewayFailedResponseService();
190
191
        $response = $gatewayLoginService->sendAuthenticationCancelledByUser($responseContext);
192
193
        return $this->renderSamlResponse('consume_assertion', $response, $request, $authenticationMode);
194
    }
195
196
    public function renderSamlResponse(
197
        string $view,
198
        SAMLResponse $response,
199
        Request $request,
200
        string $authenticationMode,
201
    ): Response {
202
        $logger = $this->get('logger');
203
        /** @var ResponseHelper $responseHelper */
204
        $responseHelper = $this->get('second_factor_only.adfs.response_helper');
205
206
        $this->supportsAuthenticationMode($authenticationMode);
207
        $responseContext = $this->getResponseContext($authenticationMode);
208
209
        $parameters = [
210
            'acu' => $responseContext->getDestination(),
211
            'response' => $this->getResponseAsXML($response),
212
            'relayState' => $responseContext->getRelayState(),
213
        ];
214
215
        // Test if we should add ADFS response parameters
216
        $inResponseTo = $responseContext->getInResponseTo();
217
        if ($responseHelper->isAdfsResponse($inResponseTo)) {
218
            $adfsParameters = $responseHelper->retrieveAdfsParameters();
219
            $logMessage = 'Responding with additional ADFS parameters, in response to request: "%s", with view: "%s"';
220
            if (!$response->isSuccess()) {
221
                $logMessage = 'Responding with an AuthnFailed SamlResponse with ADFS parameters, in response to AR: "%s", with view: "%s"';
222
            }
223
            $logger->notice(sprintf($logMessage, $inResponseTo, $view));
224
            $parameters['adfs'] = $adfsParameters;
225
            $parameters['acu'] = $responseContext->getDestinationForAdfs();
226
        }
227
228
        $httpResponse = $this->render($view, $parameters);
229
230
        if ($response->isSuccess()) {
231
            $ssoCookieService = $this->get('gateway.service.sso_2fa_cookie');
232
            $ssoCookieService->handleSsoOn2faCookieStorage($responseContext, $request, $httpResponse);
233
        }
234
235
        return $httpResponse;
236
    }
237
238
    /**
239
     * @param string $view
240
     */
241
    public function render($view, array $parameters = [], ?Response $response = null): Response
242
    {
243
        return parent::render(
244
            '@default/gateway/'.$view.'.html.twig',
245
            $parameters,
246
            $response,
247
        );
248
    }
249
250
    public function getResponseContext($authenticationMode): ResponseContext
251
    {
252
        return match ($authenticationMode) {
253
            self::MODE_SFO => $this->get($this->get('gateway.proxy.sfo.state_handler')->getResponseContextServiceId()),
254
            self::MODE_SSO => $this->get($this->get('gateway.proxy.sso.state_handler')->getResponseContextServiceId()),
255
            default => throw new RuntimeException('Invalid authentication mode requested'),
256
        };
257
    }
258
259
    private function getResponseAsXML(SAMLResponse $response): string
260
    {
261
        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

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