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