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_Const; |
24
|
|
|
use SAML2_Response; |
25
|
|
|
use Surfnet\SamlBundle\Http\XMLResponse; |
26
|
|
|
use Surfnet\SamlBundle\SAML2\AuthnRequest; |
27
|
|
|
use Surfnet\SamlBundle\SAML2\AuthnRequestFactory; |
28
|
|
|
use Surfnet\StepupGateway\GatewayBundle\Saml\AssertionAdapter; |
29
|
|
|
use Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider; |
30
|
|
|
use Surfnet\StepupGateway\SamlStepupProviderBundle\Saml\StateHandler; |
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\AccessDeniedHttpException; |
35
|
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
39
|
|
|
* @SuppressWarnings(PHPMD.NPathComplexity) |
40
|
|
|
* |
41
|
|
|
* Should be refactored, {@see https://www.pivotaltracker.com/story/show/90169776} |
42
|
|
|
*/ |
43
|
|
|
class SamlProxyController extends Controller |
44
|
|
|
{ |
45
|
|
|
/** |
46
|
|
|
* @param string $provider |
47
|
|
|
* @param Request $httpRequest |
48
|
|
|
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response |
49
|
|
|
*/ |
50
|
|
|
public function singleSignOnAction($provider, Request $httpRequest) |
51
|
|
|
{ |
52
|
|
|
$provider = $this->getProvider($provider); |
53
|
|
|
|
54
|
|
|
/** @var \Psr\Log\LoggerInterface $logger */ |
55
|
|
|
$logger = $this->get('logger'); |
56
|
|
|
$logger->notice('Received AuthnRequest, started processing'); |
57
|
|
|
|
58
|
|
|
/** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */ |
59
|
|
|
$redirectBinding = $this->get('surfnet_saml.http.redirect_binding'); |
60
|
|
|
|
61
|
|
|
try { |
62
|
|
|
$originalRequest = $redirectBinding->processRequest($httpRequest); |
|
|
|
|
63
|
|
|
} catch (Exception $e) { |
64
|
|
|
$logger->critical(sprintf('Could not process Request, error: "%s"', $e->getMessage())); |
65
|
|
|
|
66
|
|
|
return $this->render('unrecoverableError'); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$originalRequestId = $originalRequest->getRequestId(); |
70
|
|
|
$logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
71
|
|
|
$logger->notice(sprintf( |
72
|
|
|
'AuthnRequest processing complete, received AuthnRequest from "%s", request ID: "%s"', |
73
|
|
|
$originalRequest->getServiceProvider(), |
74
|
|
|
$originalRequest->getRequestId() |
75
|
|
|
)); |
76
|
|
|
|
77
|
|
|
$logger->debug('Checking if SP "%s" is supported'); |
78
|
|
|
/** |
79
|
|
|
* @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders |
|
|
|
|
80
|
|
|
*/ |
81
|
|
|
$connectedServiceProviders = $this->get('gssp.connected_service_providers'); |
82
|
|
|
if (!$connectedServiceProviders->isConnected($originalRequest->getServiceProvider())) { |
83
|
|
|
$logger->warning(sprintf( |
84
|
|
|
'Received AuthnRequest from SP "%s", while SP is not allowed to use this for SSO', |
85
|
|
|
$originalRequest->getServiceProvider() |
86
|
|
|
)); |
87
|
|
|
|
88
|
|
|
throw new AccessDeniedHttpException(); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** @var StateHandler $stateHandler */ |
92
|
|
|
$stateHandler = $provider->getStateHandler(); |
93
|
|
|
$stateHandler |
94
|
|
|
->setRequestId($originalRequestId) |
95
|
|
|
->setRequestServiceProvider($originalRequest->getServiceProvider()) |
96
|
|
|
->setRelayState($httpRequest->get(AuthnRequest::PARAMETER_RELAY_STATE, '')); |
97
|
|
|
|
98
|
|
|
$proxyRequest = AuthnRequestFactory::createNewRequest( |
99
|
|
|
$provider->getServiceProvider(), |
100
|
|
|
$provider->getRemoteIdentityProvider() |
101
|
|
|
); |
102
|
|
|
|
103
|
|
|
// if a Specific subject is given to authenticate we should proxy that and verify in the response |
104
|
|
|
// that that subject indeed was authenticated |
105
|
|
|
$nameId = $originalRequest->getNameId(); |
106
|
|
|
if ($nameId) { |
107
|
|
|
$proxyRequest->setSubject($nameId, $originalRequest->getNameIdFormat()); |
108
|
|
|
$stateHandler->setSubject($nameId); |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
$proxyRequest->setScoping([$originalRequest->getServiceProvider()]); |
112
|
|
|
$stateHandler->setGatewayRequestId($proxyRequest->getRequestId()); |
113
|
|
|
|
114
|
|
|
$logger->notice(sprintf( |
115
|
|
|
'Sending Proxy AuthnRequest with request ID: "%s" for original AuthnRequest "%s" to GSSP "%s" at "%s"', |
116
|
|
|
$proxyRequest->getRequestId(), |
117
|
|
|
$originalRequest->getRequestId(), |
118
|
|
|
$provider->getName(), |
119
|
|
|
$provider->getRemoteIdentityProvider()->getSsoUrl() |
120
|
|
|
)); |
121
|
|
|
|
122
|
|
|
return $redirectBinding->createRedirectResponseFor($proxyRequest); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
public function sendSecondFactorVerificationAuthnRequestAction($provider, $subjectNameId) |
126
|
|
|
{ |
127
|
|
|
$provider = $this->getProvider($provider); |
128
|
|
|
$stateHandler = $provider->getStateHandler(); |
129
|
|
|
|
130
|
|
|
$originalRequestId = $this->get('gateway.proxy.response_context')->getInResponseTo(); |
131
|
|
|
|
132
|
|
|
$authnRequest = AuthnRequestFactory::createNewRequest( |
133
|
|
|
$provider->getServiceProvider(), |
134
|
|
|
$provider->getRemoteIdentityProvider() |
135
|
|
|
); |
136
|
|
|
$authnRequest->setSubject($subjectNameId); |
137
|
|
|
|
138
|
|
|
$stateHandler |
139
|
|
|
->setRequestId($originalRequestId) |
140
|
|
|
->setGatewayRequestId($authnRequest->getRequestId()) |
141
|
|
|
->setSubject($subjectNameId) |
142
|
|
|
->markRequestAsSecondFactorVerification(); |
143
|
|
|
|
144
|
|
|
/** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
145
|
|
|
$logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
146
|
|
|
$logger->notice(sprintf( |
147
|
|
|
'Sending AuthnRequest to verify Second Factor with request ID: "%s" to GSSP "%s" at "%s" for subject "%s"', |
148
|
|
|
$authnRequest->getRequestId(), |
149
|
|
|
$provider->getName(), |
150
|
|
|
$provider->getRemoteIdentityProvider()->getSsoUrl(), |
151
|
|
|
$subjectNameId |
152
|
|
|
)); |
153
|
|
|
|
154
|
|
|
/** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */ |
155
|
|
|
$redirectBinding = $this->get('surfnet_saml.http.redirect_binding'); |
156
|
|
|
|
157
|
|
|
return $redirectBinding->createRedirectResponseFor($authnRequest); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* @param string $provider |
162
|
|
|
* @param Request $httpRequest |
163
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
164
|
|
|
*/ |
165
|
|
|
public function consumeAssertionAction($provider, Request $httpRequest) |
166
|
|
|
{ |
167
|
|
|
$provider = $this->getProvider($provider); |
168
|
|
|
$stateHandler = $provider->getStateHandler(); |
169
|
|
|
$originalRequestId = $stateHandler->getRequestId(); |
170
|
|
|
|
171
|
|
|
/** @var \Surfnet\SamlBundle\Monolog\SamlAuthenticationLogger $logger */ |
172
|
|
|
$logger = $this->get('surfnet_saml.logger')->forAuthentication($originalRequestId); |
173
|
|
|
|
174
|
|
|
$action = $stateHandler->hasSubject() ? 'Second Factor Verification' : 'Proxy Response'; |
175
|
|
|
$logger->notice( |
176
|
|
|
sprintf('Received SAMLResponse, attempting to process for %s', $action) |
177
|
|
|
); |
178
|
|
|
|
179
|
|
|
try { |
180
|
|
|
/** @var \SAML2_Assertion $assertion */ |
181
|
|
|
$assertion = $this->get('surfnet_saml.http.post_binding')->processResponse( |
182
|
|
|
$httpRequest, |
183
|
|
|
$provider->getRemoteIdentityProvider(), |
184
|
|
|
$provider->getServiceProvider() |
185
|
|
|
); |
186
|
|
|
} catch (Exception $exception) { |
187
|
|
|
$logger->error(sprintf('Could not process received Response, error: "%s"', $exception->getMessage())); |
188
|
|
|
|
189
|
|
|
$response = $this->createResponseFailureResponse($provider); |
190
|
|
|
|
191
|
|
|
return $this->renderSamlResponse('unprocessableResponse', $stateHandler, $response); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$adaptedAssertion = new AssertionAdapter($assertion); |
195
|
|
|
$expectedResponse = $stateHandler->getGatewayRequestId(); |
196
|
|
View Code Duplication |
if (!$adaptedAssertion->inResponseToMatches($expectedResponse)) { |
|
|
|
|
197
|
|
|
$logger->critical(sprintf( |
198
|
|
|
'Received Response with unexpected InResponseTo: "%s", %s', |
199
|
|
|
$adaptedAssertion->getInResponseTo(), |
200
|
|
|
($expectedResponse ? 'expected "' . $expectedResponse . '"' : ' no response expected') |
201
|
|
|
)); |
202
|
|
|
|
203
|
|
|
return $this->render('unrecoverableError'); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
$authenticatedNameId = $assertion->getNameId(); |
207
|
|
|
$isSubjectRequested = $stateHandler->hasSubject(); |
208
|
|
|
if ($isSubjectRequested && ($stateHandler->getSubject() !== $authenticatedNameId['Value'])) { |
209
|
|
|
$logger->critical(sprintf( |
210
|
|
|
'Requested Subject NameID "%s" and Response NameID "%s" do not match', |
211
|
|
|
$stateHandler->getSubject(), |
212
|
|
|
$authenticatedNameId['Value'] |
213
|
|
|
)); |
214
|
|
|
|
215
|
|
|
if ($stateHandler->secondFactorVerificationRequested()) { |
216
|
|
|
// the error should go to the original requesting service provider |
217
|
|
|
$targetServiceProvider = $this->get('gateway.proxy.response_context')->getServiceProvider(); |
218
|
|
|
$stateHandler->setRequestServiceProvider($targetServiceProvider->getEntityId()); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
return $this->renderSamlResponse( |
222
|
|
|
'recoverableError', |
223
|
|
|
$stateHandler, |
224
|
|
|
$this->createAuthnFailedResponse($provider) |
225
|
|
|
); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
$logger->notice('Successfully processed SAMLResponse'); |
229
|
|
|
|
230
|
|
|
if ($stateHandler->secondFactorVerificationRequested()) { |
231
|
|
|
$logger->notice( |
232
|
|
|
'Second Factor verification was requested and was successful, forwarding to SecondFactor handling' |
233
|
|
|
); |
234
|
|
|
|
235
|
|
|
return $this->forward('SurfnetStepupGatewayGatewayBundle:SecondFactor:gssfVerified'); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
/** @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Saml\ProxyResponseFactory $proxyResponseFactory */ |
239
|
|
|
$targetServiceProvider = $this->getServiceProvider($stateHandler->getRequestServiceProvider()); |
240
|
|
|
$proxyResponseFactory = $this->get('gssp.provider.' . $provider->getName() . '.response_proxy'); |
241
|
|
|
$response = $proxyResponseFactory->createProxyResponse($assertion, $targetServiceProvider); |
242
|
|
|
|
243
|
|
|
$logger->notice(sprintf( |
244
|
|
|
'Responding to request "%s" with response based on response from the remote IdP with response "%s"', |
245
|
|
|
$stateHandler->getRequestId(), |
246
|
|
|
$response->getId() |
247
|
|
|
)); |
248
|
|
|
|
249
|
|
|
return $this->renderSamlResponse('consumeAssertion', $stateHandler, $response); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* @param string $provider |
254
|
|
|
* @return XMLResponse |
255
|
|
|
*/ |
256
|
|
|
public function metadataAction($provider) |
257
|
|
|
{ |
258
|
|
|
$provider = $this->getProvider($provider); |
259
|
|
|
|
260
|
|
|
/** @var \Surfnet\SamlBundle\Metadata\MetadataFactory $factory */ |
261
|
|
|
$factory = $this->get('gssp.provider.' . $provider->getName() . '.metadata.factory'); |
262
|
|
|
|
263
|
|
|
return new XMLResponse($factory->generate()); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param string $provider |
268
|
|
|
* @return \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\Provider |
269
|
|
|
*/ |
270
|
|
|
private function getProvider($provider) |
271
|
|
|
{ |
272
|
|
|
/** @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ProviderRepository $providerRepository */ |
273
|
|
|
$providerRepository = $this->get('gssp.provider_repository'); |
274
|
|
|
|
275
|
|
|
if (!$providerRepository->has($provider)) { |
276
|
|
|
$this->get('logger')->info(sprintf('Requested GSSP "%s" does not exist or is not registered', $provider)); |
277
|
|
|
|
278
|
|
|
throw new NotFoundHttpException('Requested provider does not exist'); |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
return $providerRepository->get($provider); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* @param string $view |
286
|
|
|
* @param StateHandler $stateHandler |
287
|
|
|
* @param SAML2_Response $response |
288
|
|
|
* @return Response |
289
|
|
|
*/ |
290
|
|
View Code Duplication |
public function renderSamlResponse($view, StateHandler $stateHandler, SAML2_Response $response) |
|
|
|
|
291
|
|
|
{ |
292
|
|
|
$response = $this->render($view, [ |
293
|
|
|
'acu' => $response->getDestination(), |
294
|
|
|
'response' => $this->getResponseAsXML($response), |
295
|
|
|
'relayState' => $stateHandler->getRelayState() |
296
|
|
|
]); |
297
|
|
|
|
298
|
|
|
// clear the state so we can call again :) |
299
|
|
|
$stateHandler->clear(); |
300
|
|
|
|
301
|
|
|
return $response; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param string $view |
306
|
|
|
* @param array $parameters |
307
|
|
|
* @param Response $response |
|
|
|
|
308
|
|
|
* @return Response |
309
|
|
|
*/ |
310
|
|
|
public function render($view, array $parameters = array(), Response $response = null) |
311
|
|
|
{ |
312
|
|
|
return parent::render( |
313
|
|
|
'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:' . $view . '.html.twig', |
314
|
|
|
$parameters, |
315
|
|
|
$response |
316
|
|
|
); |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* @param SAML2_Response $response |
321
|
|
|
* @return string |
322
|
|
|
*/ |
323
|
|
|
private function getResponseAsXML(SAML2_Response $response) |
324
|
|
|
{ |
325
|
|
|
return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML()); |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Response that indicates that an error occurred in the responder (the gateway). Used to indicate that we could |
330
|
|
|
* not process the response we received from the upstream GSSP |
331
|
|
|
* |
332
|
|
|
* @param Provider $provider |
333
|
|
|
* @return SAML2_Response |
334
|
|
|
*/ |
335
|
|
|
private function createResponseFailureResponse(Provider $provider) |
336
|
|
|
{ |
337
|
|
|
$response = $this->createResponse($provider); |
338
|
|
|
$response->setStatus(['Code' => SAML2_Const::STATUS_RESPONDER]); |
339
|
|
|
|
340
|
|
|
return $response; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Response that indicates that the authentication could not be performed correctly. In this context it means |
345
|
|
|
* that the upstream GSSP did not responsd with the same NameID as we request to authenticate in the AuthnRequest |
346
|
|
|
* |
347
|
|
|
* @param Provider $provider |
348
|
|
|
* @return SAML2_Response |
349
|
|
|
*/ |
350
|
|
|
private function createAuthnFailedResponse(Provider $provider) |
351
|
|
|
{ |
352
|
|
|
$response = $this->createResponse($provider); |
353
|
|
|
$response->setStatus([ |
354
|
|
|
'Code' => SAML2_Const::STATUS_RESPONDER, |
355
|
|
|
'SubCode' => SAML2_Const::STATUS_AUTHN_FAILED |
356
|
|
|
]); |
357
|
|
|
|
358
|
|
|
return $response; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* Creates a standard response with default status Code (success) |
363
|
|
|
* |
364
|
|
|
* @param Provider $provider |
365
|
|
|
* @return SAML2_Response |
366
|
|
|
*/ |
367
|
|
|
private function createResponse(Provider $provider) |
368
|
|
|
{ |
369
|
|
|
$serviceProvider = $this->getServiceProvider($provider->getStateHandler()->getRequestServiceProvider()); |
370
|
|
|
|
371
|
|
|
$response = new SAML2_Response(); |
372
|
|
|
$response->setDestination($serviceProvider->getAssertionConsumerUrl()); |
373
|
|
|
$response->setIssuer($provider->getIdentityProvider()->getEntityId()); |
374
|
|
|
$response->setIssueInstant((new DateTime('now'))->getTimestamp()); |
375
|
|
|
$response->setInResponseTo($provider->getStateHandler()->getRequestId()); |
376
|
|
|
|
377
|
|
|
return $response; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* @param string $serviceProvider |
382
|
|
|
* @return \Surfnet\SamlBundle\Entity\ServiceProvider |
383
|
|
|
*/ |
384
|
|
|
private function getServiceProvider($serviceProvider) |
385
|
|
|
{ |
386
|
|
|
/** |
387
|
|
|
* @var \Surfnet\StepupGateway\SamlStepupProviderBundle\Provider\ConnectedServiceProviders $connectedServiceProviders |
|
|
|
|
388
|
|
|
*/ |
389
|
|
|
$connectedServiceProviders = $this->get('gssp.connected_service_providers'); |
390
|
|
|
return $connectedServiceProviders->getConfigurationOf($serviceProvider); |
391
|
|
|
} |
392
|
|
|
} |
393
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.