Completed
Pull Request — feature/acceptance-tests (#192)
by Michiel
02:05
created

IdentityProviderController::createResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 1
nc 1
nop 3
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(
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (render() instead of renderSamlResponse()). Are you sure this is correct? If so, you might want to change this to $this->render().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
96
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:consumeAssertion.html.twig',
97
            $parameters
98
        );
99
100
        return $response;
101
    }
102
103
    /**
104
     * @param string $destination
105
     * @param string $nameId
0 ignored issues
show
Documentation introduced by
Should the type for parameter $nameId not be array?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
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)]]);
0 ignored issues
show
Deprecated Code introduced by
The method SAML2\XML\saml\NameIDType::fromArray() has been deprecated.

This method has been deprecated.

Loading history...
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
0 ignored issues
show
Documentation introduced by
Should the return type not be ReceivedAuthnRequest|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
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