1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Surfnet\StepupGateway\Behat\Controller; |
4
|
|
|
|
5
|
|
|
use RobRichards\XMLSecLibs\XMLSecurityKey; |
6
|
|
|
use SAML2\Assertion; |
7
|
|
|
use SAML2\Certificate\KeyLoader; |
8
|
|
|
use SAML2\Certificate\PrivateKeyLoader; |
9
|
|
|
use SAML2\Configuration\PrivateKey; |
10
|
|
|
use SAML2\Constants; |
11
|
|
|
use SAML2\Response; |
12
|
|
|
use SAML2\Response as SAMLResponse; |
13
|
|
|
use SAML2\XML\saml\NameID; |
14
|
|
|
use SAML2\XML\saml\SubjectConfirmation; |
15
|
|
|
use SAML2\XML\saml\SubjectConfirmationData; |
16
|
|
|
use Surfnet\SamlBundle\Http\Exception\UnsignedRequestException; |
17
|
|
|
use Surfnet\SamlBundle\Http\ReceivedAuthnRequestQueryString; |
18
|
|
|
use Surfnet\SamlBundle\SAML2\ReceivedAuthnRequest; |
19
|
|
|
use Surfnet\StepupGateway\Behat\Command\LoginCommand; |
20
|
|
|
use Symfony\Bundle\FrameworkBundle\Controller\Controller; |
21
|
|
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType; |
22
|
|
|
use Symfony\Component\Form\Extension\Core\Type\TextType; |
23
|
|
|
use Symfony\Component\HttpFoundation\Request; |
24
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
25
|
|
|
|
26
|
|
|
class IdentityProviderController extends Controller |
|
|
|
|
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* Handles a SSO request |
30
|
|
|
* @param Request $request |
31
|
|
|
*/ |
32
|
|
|
public function ssoAction(Request $request) |
33
|
|
|
{ |
34
|
|
|
// Receives the AuthnRequest and sends a SAML response |
35
|
|
|
$authnRequest = $this->receiveSignedAuthnRequestFrom($request); |
36
|
|
|
// By default render the username form |
37
|
|
|
$loginData = new LoginCommand(); |
38
|
|
|
if ($authnRequest) { |
39
|
|
|
$loginData->setRequestId($authnRequest->getRequestId()); |
40
|
|
|
} |
41
|
|
|
$form = $this |
42
|
|
|
->createFormBuilder($loginData) |
43
|
|
|
->add('username', TextType::class) |
44
|
|
|
->add('requestId', TextType::class) |
45
|
|
|
->add('submit', SubmitType::class) |
46
|
|
|
->getForm(); |
47
|
|
|
|
48
|
|
|
$form->handleRequest($request); |
49
|
|
|
|
50
|
|
|
if ($form->isSubmitted() && $form->isValid()) { |
51
|
|
|
$loginData = $form->getData(); |
52
|
|
|
$response = $this->createResponse( |
53
|
|
|
'https://gateway.stepup.example.com/authentication/consume-assertion', |
54
|
|
|
['Value' => $loginData->getUsername(), 'Format' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'], |
55
|
|
|
$loginData->getRequestId() |
56
|
|
|
); |
57
|
|
|
return $this->renderSamlResponse($response); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
return $this->render('@test_resources/login.html.twig', [ |
61
|
|
|
'form' => $form->createView(), |
62
|
|
|
]); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Handles a GSSP SSO request |
67
|
|
|
* @param Request $request |
68
|
|
|
*/ |
69
|
|
|
public function gsspSsoAction(Request $request) |
70
|
|
|
{ |
71
|
|
|
// receives the AuthnRequest and sends a SAML response |
72
|
|
|
$authnRequest = $this->receiveSignedAuthnRequestFrom($request); |
73
|
|
|
// Todo: For some reason, the nameId is not transpored even tho it is set on the auhtnrequest.. Figure out whats going on here and fix this. |
74
|
|
|
// now the test will only work with one hard-coded user. |
75
|
|
|
$response = $this->createResponse( |
76
|
|
|
$authnRequest->getAssertionConsumerServiceURL(), |
77
|
|
|
['Value' => 'foobar', 'Format' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'], |
78
|
|
|
$authnRequest->getRequestId() |
79
|
|
|
); |
80
|
|
|
return $this->renderSamlResponse($response); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* @param SAMLResponse $response |
85
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
86
|
|
|
*/ |
87
|
|
|
public function renderSamlResponse(SAMLResponse $response) |
88
|
|
|
{ |
89
|
|
|
$parameters = [ |
90
|
|
|
'acu' => $response->getDestination(), |
91
|
|
|
'response' => $this->getResponseAsXML($response), |
92
|
|
|
'relayState' => '' |
93
|
|
|
]; |
94
|
|
|
|
95
|
|
|
$response = parent::render( |
|
|
|
|
96
|
|
|
'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:consumeAssertion.html.twig', |
97
|
|
|
$parameters |
98
|
|
|
); |
99
|
|
|
|
100
|
|
|
return $response; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* @param string $destination |
105
|
|
|
* @param string $nameId |
|
|
|
|
106
|
|
|
* @return Response |
107
|
|
|
*/ |
108
|
|
|
public function createResponse($destination, array $nameId, $requestId) |
109
|
|
|
{ |
110
|
|
|
$newAssertion = new Assertion(); |
111
|
|
|
$newAssertion->setNotBefore(time()); |
112
|
|
|
$newAssertion->setNotOnOrAfter(time() + (60 * 5));// |
113
|
|
|
$newAssertion->setAttributes(['urn:mace:dir:attribute-def:eduPersonTargetedID' => [NameID::fromArray($nameId)]]); |
|
|
|
|
114
|
|
|
$newAssertion->setIssuer('https://idp.stepup.example.com/'); |
115
|
|
|
$newAssertion->setIssueInstant(time()); |
116
|
|
|
|
117
|
|
|
$this->signAssertion($newAssertion); |
118
|
|
|
$this->addSubjectConfirmationFor($newAssertion, $destination, $requestId); |
119
|
|
|
$newAssertion->setNameId($nameId); |
120
|
|
|
$response = new SAMLResponse(); |
121
|
|
|
$response->setAssertions([$newAssertion]); |
122
|
|
|
$response->setIssuer('https://idp.stepup.example.com/'); |
123
|
|
|
$response->setIssueInstant(time()); |
124
|
|
|
$response->setDestination($destination); |
125
|
|
|
$response->setInResponseTo($requestId); |
126
|
|
|
return $response; |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* @param SAMLResponse $response |
131
|
|
|
* @return string |
132
|
|
|
*/ |
133
|
|
|
private function getResponseAsXML(SAMLResponse $response) |
134
|
|
|
{ |
135
|
|
|
return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML()); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
/** |
139
|
|
|
* @param Request $request |
140
|
|
|
* @return string |
141
|
|
|
*/ |
142
|
|
|
private function getFullRequestUri(Request $request) |
143
|
|
|
{ |
144
|
|
|
return $request->getSchemeAndHttpHost() . $request->getBasePath() . $request->getRequestUri(); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* @param Request $request |
149
|
|
|
* @return ReceivedAuthnRequest |
|
|
|
|
150
|
|
|
*/ |
151
|
|
|
public function receiveSignedAuthnRequestFrom(Request $request) |
152
|
|
|
{ |
153
|
|
|
if (!$request->isMethod(Request::METHOD_GET)) { |
154
|
|
|
return; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
$requestUri = $request->getRequestUri(); |
158
|
|
|
if (strpos($requestUri, '?') === false) { |
159
|
|
|
throw new BadRequestHttpException( |
160
|
|
|
'Could not receive AuthnRequest from HTTP Request: expected query parameters, none found' |
161
|
|
|
); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
list(, $rawQueryString) = explode('?', $requestUri); |
165
|
|
|
$query = ReceivedAuthnRequestQueryString::parse($rawQueryString); |
166
|
|
|
|
167
|
|
|
if (!$query->isSigned()) { |
168
|
|
|
throw new UnsignedRequestException('The SAMLRequest is expected to be signed but it was not'); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$authnRequest = ReceivedAuthnRequest::from($query->getDecodedSamlRequest()); |
172
|
|
|
|
173
|
|
|
$currentUri = $this->getFullRequestUri($request); |
174
|
|
|
if (!$authnRequest->getDestination() === $currentUri) { |
175
|
|
|
throw new BadRequestHttpException(sprintf( |
176
|
|
|
'Actual Destination "%s" does not match the AuthnRequest Destination "%s"', |
177
|
|
|
$currentUri, |
178
|
|
|
$authnRequest->getDestination() |
179
|
|
|
)); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return $authnRequest; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* @param Assertion $assertion |
187
|
|
|
* @return Assertion |
188
|
|
|
*/ |
189
|
|
|
public function signAssertion(Assertion $assertion) |
190
|
|
|
{ |
191
|
|
|
$assertion->setSignatureKey($this->loadPrivateKey()); |
192
|
|
|
$assertion->setCertificates([$this->getPublicCertificate()]); |
193
|
|
|
|
194
|
|
|
return $assertion; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
View Code Duplication |
private function addSubjectConfirmationFor(Assertion $newAssertion, $destination, $requestId) |
|
|
|
|
198
|
|
|
{ |
199
|
|
|
$confirmation = new SubjectConfirmation(); |
200
|
|
|
$confirmation->Method = Constants::CM_BEARER; |
201
|
|
|
|
202
|
|
|
$confirmationData = new SubjectConfirmationData(); |
203
|
|
|
$confirmationData->InResponseTo = $requestId; |
204
|
|
|
$confirmationData->Recipient = $destination; |
205
|
|
|
$confirmationData->NotOnOrAfter = $newAssertion->getNotOnOrAfter(); |
206
|
|
|
|
207
|
|
|
$confirmation->SubjectConfirmationData = $confirmationData; |
208
|
|
|
|
209
|
|
|
$newAssertion->setSubjectConfirmation([$confirmation]); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @return XMLSecurityKey |
214
|
|
|
*/ |
215
|
|
View Code Duplication |
private function loadPrivateKey() |
|
|
|
|
216
|
|
|
{ |
217
|
|
|
$key = new PrivateKey('/var/www/ci/certificates/sp.pem', 'default'); |
218
|
|
|
$keyLoader = new PrivateKeyLoader(); |
219
|
|
|
$privateKey = $keyLoader->loadPrivateKey($key); |
220
|
|
|
|
221
|
|
|
$xmlSecurityKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']); |
222
|
|
|
$xmlSecurityKey->loadKey($privateKey->getKeyAsString()); |
223
|
|
|
|
224
|
|
|
return $xmlSecurityKey; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* @return string |
229
|
|
|
*/ |
230
|
|
|
private function getPublicCertificate() |
231
|
|
|
{ |
232
|
|
|
$keyLoader = new KeyLoader(); |
233
|
|
|
$keyLoader->loadCertificateFile('/var/www/ci/certificates/sp.crt'); |
234
|
|
|
/** @var \SAML2\Certificate\X509 $publicKey */ |
235
|
|
|
$publicKey = $keyLoader->getKeys()->getOnlyElement(); |
236
|
|
|
|
237
|
|
|
return $publicKey->getCertificate(); |
238
|
|
|
} |
239
|
|
|
} |
240
|
|
|
|
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.