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\StepupRa\RaBundle\Controller\Vetting; |
20
|
|
|
|
21
|
|
|
use Exception; |
22
|
|
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; |
23
|
|
|
use Surfnet\SamlBundle\Http\XMLResponse; |
24
|
|
|
use Surfnet\SamlBundle\SAML2\AuthnRequestFactory; |
25
|
|
|
use Surfnet\SamlBundle\SAML2\Response\Assertion\InResponseTo; |
26
|
|
|
use Surfnet\StepupRa\RaBundle\Exception\RuntimeException; |
27
|
|
|
use Surfnet\StepupRa\RaBundle\Form\Type\InitiateGssfType; |
28
|
|
|
use Surfnet\StepupRa\RaBundle\Service\VettingService; |
29
|
|
|
use Symfony\Component\HttpFoundation\Request; |
30
|
|
|
use Symfony\Component\HttpFoundation\Response; |
31
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
32
|
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* Orchestrates verification of GSSFs (Generic SAML Second Factors) through GSSPs (Generic SAML Stepup Providers). |
36
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) |
37
|
|
|
*/ |
38
|
|
|
final class GssfController extends SecondFactorController |
39
|
|
|
{ |
40
|
|
|
/** |
41
|
|
|
* Initiates verification of a GSSF. |
42
|
|
|
* |
43
|
|
|
* @Template |
44
|
|
|
* @param string $procedureId |
45
|
|
|
* @param string $provider |
46
|
|
|
* @return array|Response |
47
|
|
|
*/ |
48
|
|
|
public function initiateAction($procedureId, $provider) |
49
|
|
|
{ |
50
|
|
|
$this->assertSecondFactorEnabled($provider); |
51
|
|
|
|
52
|
|
|
$this->denyAccessUnlessGranted(['ROLE_RA']); |
53
|
|
|
|
54
|
|
|
$logger = $this->get('ra.procedure_logger')->forProcedure($procedureId); |
55
|
|
|
$logger->notice('Showing Initiate GSSF Verification Screen', ['provider' => $provider]); |
56
|
|
|
|
57
|
|
View Code Duplication |
if (!$this->getVettingService()->hasProcedure($procedureId)) { |
|
|
|
|
58
|
|
|
$logger->notice(sprintf('Vetting procedure "%s" not found', $procedureId)); |
59
|
|
|
throw new NotFoundHttpException(sprintf('Vetting procedure "%s" not found', $procedureId)); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
return $this->renderInitiateForm($procedureId, $this->getProvider($provider)->getName()); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @param string $procedureId |
67
|
|
|
* @param string $provider |
68
|
|
|
* @return array|Response |
69
|
|
|
*/ |
70
|
|
|
public function authenticateAction($procedureId, $provider) |
71
|
|
|
{ |
72
|
|
|
$this->assertSecondFactorEnabled($provider); |
73
|
|
|
|
74
|
|
|
$this->denyAccessUnlessGranted(['ROLE_RA']); |
75
|
|
|
|
76
|
|
|
$logger = $this->get('ra.procedure_logger')->forProcedure($procedureId); |
77
|
|
|
$logger->notice('Generating GSSF verification request', ['provider' => $provider]); |
78
|
|
|
|
79
|
|
View Code Duplication |
if (!$this->getVettingService()->hasProcedure($procedureId)) { |
|
|
|
|
80
|
|
|
$logger->notice(sprintf('Vetting procedure "%s" not found', $procedureId)); |
81
|
|
|
throw new NotFoundHttpException(sprintf('Vetting procedure "%s" not found', $procedureId)); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
$provider = $this->getProvider($provider); |
85
|
|
|
|
86
|
|
|
$authnRequest = AuthnRequestFactory::createNewRequest( |
87
|
|
|
$provider->getServiceProvider(), |
88
|
|
|
$provider->getRemoteIdentityProvider() |
89
|
|
|
); |
90
|
|
|
|
91
|
|
|
/** @var \Surfnet\StepupRa\RaBundle\Service\VettingService $vettingService */ |
92
|
|
|
$vettingService = $this->get('ra.service.vetting'); |
93
|
|
|
$authnRequest->setSubject($vettingService->getSecondFactorIdentifier($procedureId)); |
94
|
|
|
|
95
|
|
|
$stateHandler = $provider->getStateHandler(); |
96
|
|
|
$stateHandler->setRequestId($authnRequest->getRequestId()); |
97
|
|
|
|
98
|
|
|
/** @var \Surfnet\SamlBundle\Http\RedirectBinding $redirectBinding */ |
99
|
|
|
$redirectBinding = $this->get('surfnet_saml.http.redirect_binding'); |
100
|
|
|
|
101
|
|
|
$logger->notice( |
102
|
|
|
sprintf( |
103
|
|
|
'Sending AuthnRequest with request ID: "%s" to GSSP "%s" at "%s"', |
104
|
|
|
$authnRequest->getRequestId(), |
105
|
|
|
$provider->getName(), |
106
|
|
|
$provider->getRemoteIdentityProvider()->getSsoUrl() |
107
|
|
|
), |
108
|
|
|
['provider' => $provider] |
109
|
|
|
); |
110
|
|
|
|
111
|
|
|
$vettingService->startGssfVerification($procedureId); |
112
|
|
|
|
113
|
|
|
return $redirectBinding->createRedirectResponseFor($authnRequest); |
|
|
|
|
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* @param Request $httpRequest |
118
|
|
|
* @param string $provider |
119
|
|
|
* @return array|Response |
120
|
|
|
*/ |
121
|
|
|
public function verifyAction(Request $httpRequest, $provider) |
122
|
|
|
{ |
123
|
|
|
$this->assertSecondFactorEnabled($provider); |
124
|
|
|
|
125
|
|
|
$provider = $this->getProvider($provider); |
126
|
|
|
|
127
|
|
|
$this->get('logger')->notice( |
128
|
|
|
sprintf('Received GSSP "%s" SAMLResponse through Gateway, attempting to process', $provider->getName()) |
129
|
|
|
); |
130
|
|
|
|
131
|
|
|
try { |
132
|
|
|
/** @var \Surfnet\SamlBundle\Http\PostBinding $postBinding */ |
133
|
|
|
$postBinding = $this->get('surfnet_saml.http.post_binding'); |
134
|
|
|
$assertion = $postBinding->processResponse( |
135
|
|
|
$httpRequest, |
136
|
|
|
$provider->getRemoteIdentityProvider(), |
137
|
|
|
$provider->getServiceProvider() |
138
|
|
|
); |
139
|
|
|
} catch (Exception $exception) { |
140
|
|
|
$provider->getStateHandler()->clear(); |
141
|
|
|
$this->getLogger()->error( |
142
|
|
|
sprintf('Could not process received Response, error: "%s"', $exception->getMessage()) |
143
|
|
|
); |
144
|
|
|
|
145
|
|
|
throw new BadRequestHttpException( |
146
|
|
|
'Could not process received SAML response, cannot return to vetting procedure' |
147
|
|
|
); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$expectedResponseTo = $provider->getStateHandler()->getRequestId(); |
151
|
|
|
$provider->getStateHandler()->clear(); |
152
|
|
|
|
153
|
|
|
if (!InResponseTo::assertEquals($assertion, $expectedResponseTo)) { |
154
|
|
|
$this->getLogger()->critical(sprintf( |
155
|
|
|
'Received Response with unexpected InResponseTo: %s', |
156
|
|
|
($expectedResponseTo ? 'expected "' . $expectedResponseTo . '"' : ' no response expected') |
157
|
|
|
)); |
158
|
|
|
|
159
|
|
|
throw new BadRequestHttpException('Received unexpected SAML response, cannot return to vetting procedure'); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
$this->get('logger')->notice( |
163
|
|
|
sprintf('Processed GSSP "%s" SAMLResponse received through Gateway successfully', $provider->getName()) |
164
|
|
|
); |
165
|
|
|
|
166
|
|
|
/** @var \Surfnet\SamlBundle\SAML2\Attribute\AttributeDictionary $attributeDictionary */ |
167
|
|
|
$attributeDictionary = $this->get('surfnet_saml.saml.attribute_dictionary'); |
168
|
|
|
$gssfId = $attributeDictionary->translate($assertion)->getNameID(); |
169
|
|
|
|
170
|
|
|
/** @var \Surfnet\StepupRa\RaBundle\Service\VettingService $vettingService */ |
171
|
|
|
$vettingService = $this->get('ra.service.vetting'); |
172
|
|
|
$result = $vettingService->verifyGssfId($gssfId); |
173
|
|
|
|
174
|
|
|
if ($result->isSuccess()) { |
175
|
|
|
$this->getLogger()->notice('GSSP possession proven successfully'); |
176
|
|
|
|
177
|
|
|
return $this->redirectToRoute('ra_vetting_verify_identity', ['procedureId' => $result->getProcedureId()]); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
if (!$result->getProcedureId()) { |
181
|
|
|
// Should be unreachable statement, because the request ID is compared to the response ID a few lines before |
182
|
|
|
// this. |
183
|
|
|
throw new RuntimeException('Procedure ID for GSSF verification procedure could not be recovered.'); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$this->getLogger()->notice( |
187
|
|
|
'Unable to prove possession of correct GSSF: ' . |
188
|
|
|
'GSSF ID registered in Self-Service does not match current GSSF ID' |
189
|
|
|
); |
190
|
|
|
|
191
|
|
|
return $this->renderInitiateForm( |
192
|
|
|
$result->getProcedureId(), |
193
|
|
|
$provider->getName(), |
194
|
|
|
['gssfIdMismatch' => true] |
195
|
|
|
); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* @param string $provider |
200
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
201
|
|
|
*/ |
202
|
|
|
public function metadataAction($provider) |
203
|
|
|
{ |
204
|
|
|
$this->assertSecondFactorEnabled($provider); |
205
|
|
|
|
206
|
|
|
$provider = $this->getProvider($provider); |
207
|
|
|
|
208
|
|
|
/** @var \Surfnet\SamlBundle\Metadata\MetadataFactory $factory */ |
209
|
|
|
$factory = $this->get('gssp.provider.' . $provider->getName() . '.metadata.factory'); |
210
|
|
|
|
211
|
|
|
return new XMLResponse($factory->generate()); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* @param string $provider |
216
|
|
|
* @return \Surfnet\StepupRa\SamlStepupProviderBundle\Provider\Provider |
217
|
|
|
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException |
218
|
|
|
*/ |
219
|
|
|
private function getProvider($provider) |
220
|
|
|
{ |
221
|
|
|
/** @var \Surfnet\StepupRa\SamlStepupProviderBundle\Provider\ProviderRepository $providerRepository */ |
222
|
|
|
$providerRepository = $this->get('gssp.provider_repository'); |
223
|
|
|
|
224
|
|
|
if (!$providerRepository->has($provider)) { |
225
|
|
|
$this->get('logger')->info(sprintf('Requested GSSP "%s" does not exist or is not registered', $provider)); |
226
|
|
|
|
227
|
|
|
throw new NotFoundHttpException('Requested provider does not exist'); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $providerRepository->get($provider); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @return \Psr\Log\LoggerInterface |
235
|
|
|
*/ |
236
|
|
|
private function getLogger() |
237
|
|
|
{ |
238
|
|
|
return $this->get('logger'); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* @return VettingService |
243
|
|
|
*/ |
244
|
|
|
private function getVettingService() |
245
|
|
|
{ |
246
|
|
|
return $this->get('ra.service.vetting'); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* @param string $procedureId |
251
|
|
|
* @param string $provider |
252
|
|
|
* @param array $parameters |
253
|
|
|
* @return Response |
254
|
|
|
*/ |
255
|
|
|
private function renderInitiateForm($procedureId, $provider, array $parameters = []) |
256
|
|
|
{ |
257
|
|
|
$collection = $this->get("surfnet_stepup.provider.collection"); |
258
|
|
|
$secondFactorConfig = $collection->getByIdentifier($provider); |
259
|
|
|
|
260
|
|
|
$form = $this->createForm( |
261
|
|
|
InitiateGssfType::class, |
262
|
|
|
null, |
263
|
|
|
[ |
264
|
|
|
'procedureId' => $procedureId, |
265
|
|
|
'provider' => $provider, |
266
|
|
|
/** @Ignore from translation message extraction */ |
267
|
|
|
'label' => $secondFactorConfig->getInitiate() |
268
|
|
|
] |
269
|
|
|
); |
270
|
|
|
|
271
|
|
|
$templateParameters = array_merge( |
272
|
|
|
$parameters, |
273
|
|
|
[ |
274
|
|
|
'form' => $form->createView(), |
275
|
|
|
'procedureId' => $procedureId, |
276
|
|
|
'provider' => $provider, |
277
|
|
|
'secondFactorConfig' => $secondFactorConfig |
278
|
|
|
] |
279
|
|
|
); |
280
|
|
|
|
281
|
|
|
return $this->render('SurfnetStepupRaRaBundle:vetting/gssf:initiate.html.twig', $templateParameters); |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.