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