Completed
Push — feature/run-selenium-tests-poc ( 832342...a806da )
by Michiel
02:26
created

addSubjectConfirmationFor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 14
Ratio 100 %

Importance

Changes 0
Metric Value
dl 14
loc 14
rs 9.7998
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\SubjectConfirmation;
14
use SAML2\XML\saml\SubjectConfirmationData;
15
use Surfnet\SamlBundle\Http\Exception\UnsignedRequestException;
16
use Surfnet\SamlBundle\Http\ReceivedAuthnRequestQueryString;
17
use Surfnet\SamlBundle\SAML2\ReceivedAuthnRequest;
18
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
21
22
class IdentityProviderController extends Controller
23
{
24
    /**
25
     * Handles a SSO request
26
     * @param Request $request
27
     */
28
    public function ssoAction(Request $request)
29
    {
30
        // receives the AuthnRequest and sends a SAML response
31
        $authnRequest = $this->receiveSignedAuthnRequestFrom($request);
32
        $response = $this->createResponse(
33
            'https://gateway.stepup.example.com/authentication/consume-assertion',
34
            $authnRequest->getNameId(),
35
            $authnRequest->getRequestId()
36
        );
37
        return $this->renderSamlResponse($response);
38
    }
39
40
    /**
41
     * @param SAMLResponse $response
42
     * @return \Symfony\Component\HttpFoundation\Response
43
     */
44
    public function renderSamlResponse(SAMLResponse $response)
45
    {
46
        $parameters = [
47
            'acu' => $response->getDestination(),
48
            'response' => $this->getResponseAsXML($response),
49
            'relayState' => ''
50
        ];
51
52
        $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...
53
            'SurfnetStepupGatewaySamlStepupProviderBundle:SamlProxy:consumeAssertion.html.twig',
54
            $parameters
55
        );
56
57
        return $response;
58
    }
59
60
    /**
61
     * @param string $destination
62
     * @param string $nameId
63
     * @return Response
64
     */
65
    public function createResponse($destination, $nameId, $requestId)
66
    {
67
        $newAssertion = new Assertion();
68
        $newAssertion->setNotBefore(time());
69
        $newAssertion->setNotOnOrAfter(time() + (60 * 5));
70
        $newAssertion->setAttributes(['urn:mace:dir:attribute-def:eduPersonTargetedID' => [$nameId]]);
71
        $newAssertion->setIssuer('https://idp.stepup.example.com/');
72
        $newAssertion->setIssueInstant(time());
73
74
        $this->signAssertion($newAssertion);
75
        $this->addSubjectConfirmationFor($newAssertion, $destination, $requestId);
76
77
        $newAssertion->setNameId($nameId);
0 ignored issues
show
Documentation introduced by
$nameId is of type string, but the function expects a object<SAML2\XML\saml\NameID>|array|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
78
        $response = new SAMLResponse();
79
        $response->setAssertions([$newAssertion]);
80
        $response->setIssuer('https://idp.stepup.example.com/');
81
        $response->setIssueInstant(time());
82
        $response->setDestination($destination);
83
        $response->setInResponseTo($requestId);
84
        return $response;
85
    }
86
87
    /**
88
     * @param SAMLResponse $response
89
     * @return string
90
     */
91
    private function getResponseAsXML(SAMLResponse $response)
92
    {
93
        return base64_encode($response->toUnsignedXML()->ownerDocument->saveXML());
94
    }
95
96
    /**
97
     * @param Request $request
98
     * @return string
99
     */
100
    private function getFullRequestUri(Request $request)
101
    {
102
        return $request->getSchemeAndHttpHost() . $request->getBasePath() . $request->getRequestUri();
103
    }
104
105
    /**
106
     * @param Request $request
107
     * @return ReceivedAuthnRequest
108
     */
109
    public function receiveSignedAuthnRequestFrom(Request $request)
110
    {
111
        if (!$request->isMethod(Request::METHOD_GET)) {
112
            throw new BadRequestHttpException(sprintf(
113
                'Could not receive AuthnRequest from HTTP Request: expected a GET method, got %s',
114
                $request->getMethod()
115
            ));
116
        }
117
118
        $requestUri = $request->getRequestUri();
119
        if (strpos($requestUri, '?') === false) {
120
            throw new BadRequestHttpException(
121
                'Could not receive AuthnRequest from HTTP Request: expected query parameters, none found'
122
            );
123
        }
124
125
        list(, $rawQueryString) = explode('?', $requestUri);
126
        $query = ReceivedAuthnRequestQueryString::parse($rawQueryString);
127
128
        if (!$query->isSigned()) {
129
            throw new UnsignedRequestException('The SAMLRequest is expected to be signed but it was not');
130
        }
131
132
        $authnRequest = ReceivedAuthnRequest::from($query->getDecodedSamlRequest());
133
134
        $currentUri = $this->getFullRequestUri($request);
135
        if (!$authnRequest->getDestination() === $currentUri) {
136
            throw new BadRequestHttpException(sprintf(
137
                'Actual Destination "%s" does not match the AuthnRequest Destination "%s"',
138
                $currentUri,
139
                $authnRequest->getDestination()
140
            ));
141
        }
142
143
        return $authnRequest;
144
    }
145
146
    /**
147
     * @param Assertion $assertion
148
     * @return Assertion
149
     */
150
    public function signAssertion(Assertion $assertion)
151
    {
152
        $assertion->setSignatureKey($this->loadPrivateKey());
153
        $assertion->setCertificates([$this->getPublicCertificate()]);
154
155
        return $assertion;
156
    }
157
158 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...
159
    {
160
        $confirmation = new SubjectConfirmation();
161
        $confirmation->Method = Constants::CM_BEARER;
162
163
        $confirmationData                      = new SubjectConfirmationData();
164
        $confirmationData->InResponseTo        = $requestId;
165
        $confirmationData->Recipient           = $destination;
166
        $confirmationData->NotOnOrAfter        = $newAssertion->getNotOnOrAfter();
167
168
        $confirmation->SubjectConfirmationData = $confirmationData;
169
170
        $newAssertion->setSubjectConfirmation([$confirmation]);
171
    }
172
173
    /**
174
     * @return XMLSecurityKey
175
     */
176
    private function loadPrivateKey()
177
    {
178
        $key        = new PrivateKey('/var/www/ci/certificates/sp.pem', 'default');
179
        $keyLoader  = new PrivateKeyLoader();
180
        $privateKey = $keyLoader->loadPrivateKey($key);
181
182
        $xmlSecurityKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
183
        $xmlSecurityKey->loadKey($privateKey->getKeyAsString());
184
185
        return $xmlSecurityKey;
186
    }
187
188
    /**
189
     * @return string
190
     */
191
    private function getPublicCertificate()
192
    {
193
        $keyLoader = new KeyLoader();
194
        $keyLoader->loadCertificateFile('/var/www/ci/certificates/sp.crt');
195
        /** @var \SAML2\Certificate\X509 $publicKey */
196
        $publicKey = $keyLoader->getKeys()->getOnlyElement();
197
198
        return $publicKey->getCertificate();
199
    }
200
}
201