Completed
Push — develop ( ca2d71...98becb )
by Daan van
37:32 queued 31:53
created

ProxyResponseService::parseEptiNameId()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
rs 9.4285
cc 3
eloc 9
nc 2
nop 1
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\StepupGateway\GatewayBundle\Service;
20
21
use Exception;
22
use SAML2_Assertion;
23
use Surfnet\SamlBundle\Entity\IdentityProvider;
24
use Surfnet\SamlBundle\Entity\ServiceProvider;
25
use Surfnet\SamlBundle\SAML2\Attribute\AttributeDefinition;
26
use Surfnet\SamlBundle\SAML2\Attribute\AttributeDictionary;
27
use Surfnet\SamlBundle\SAML2\Response\AssertionAdapter;
28
use Surfnet\StepupBundle\Value\Loa;
29
use Surfnet\StepupGateway\GatewayBundle\Saml\AssertionSigningService;
30
use Surfnet\StepupGateway\GatewayBundle\Saml\Exception\RuntimeException;
31
use Surfnet\StepupGateway\GatewayBundle\Saml\Proxy\ProxyStateHandler;
32
33
/**
34
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
35
 */
36
class ProxyResponseService
37
{
38
    /**
39
     * @var \Surfnet\SamlBundle\Entity\IdentityProvider
40
     */
41
    private $hostedIdentityProvider;
42
43
    /**
44
     * @var \Surfnet\StepupGateway\GatewayBundle\Saml\Proxy\ProxyStateHandler
45
     */
46
    private $proxyStateHandler;
47
48
    /**
49
     * @var \Surfnet\SamlBundle\SAML2\Attribute\AttributeDictionary
50
     */
51
    private $attributeDictionary;
52
53
    /**
54
     * @var \Surfnet\SamlBundle\SAML2\Attribute\AttributeDefinition
55
     */
56
    private $eptiAttribute;
57
58
    /**
59
     * @var \DateTime
60
     */
61
    private $currentTime;
62
63
    /**
64
     * @var \Surfnet\StepupGateway\GatewayBundle\Saml\AssertionSigningService
65
     */
66
    private $assertionSigningService;
67
68
    /**
69
     * @var \Surfnet\StepupBundle\Value\Loa
70
     */
71
    private $intrinsicLoa;
72
73
    public function __construct(
74
        IdentityProvider $hostedIdentityProvider,
75
        ProxyStateHandler $proxyStateHandler,
76
        AssertionSigningService $assertionSigningService,
77
        AttributeDictionary $attributeDictionary,
78
        AttributeDefinition $eptiAttribute,
79
        Loa $intrinsicLoa
80
    ) {
81
        $this->hostedIdentityProvider    = $hostedIdentityProvider;
82
        $this->proxyStateHandler         = $proxyStateHandler;
83
        $this->assertionSigningService   = $assertionSigningService;
84
        $this->attributeDictionary       = $attributeDictionary;
85
        $this->eptiAttribute             = $eptiAttribute;
86
        $this->intrinsicLoa              = $intrinsicLoa;
87
        $this->currentTime = new \DateTime('now', new \DateTimeZone('UTC'));
88
    }
89
90
    /**
91
     * @param SAML2_Assertion $assertion
92
     * @param ServiceProvider $targetServiceProvider
93
     * @param string|null $loa
94
     * @return \SAML2_Response
95
     */
96
    public function createProxyResponse(SAML2_Assertion $assertion, ServiceProvider $targetServiceProvider, $loa = null)
97
    {
98
99
        $newAssertion = new SAML2_Assertion();
100
        $newAssertion->setNotBefore($this->currentTime->getTimestamp());
101
        $newAssertion->setNotOnOrAfter($this->getTimestamp('PT5M'));
102
        $newAssertion->setAttributes($assertion->getAttributes());
103
        $newAssertion->setIssuer($this->hostedIdentityProvider->getEntityId());
104
        $newAssertion->setIssueInstant($this->getTimestamp());
105
106
        $this->assertionSigningService->signAssertion($newAssertion);
107
        $this->addSubjectConfirmationFor($newAssertion, $targetServiceProvider);
108
109
        $translatedAssertion = $this->attributeDictionary->translate($assertion);
110
        $eptiNameId = $this->parseEptiNameId($translatedAssertion);
111
        $newAssertion->setNameId($eptiNameId);
112
113
        $newAssertion->setValidAudiences([$this->proxyStateHandler->getRequestServiceProvider()]);
114
115
        $this->addAuthenticationStatementTo($newAssertion, $assertion);
116
117
        if ($loa) {
118
            $newAssertion->setAuthnContextClassRef($loa);
119
        }
120
121
        return $this->createNewAuthnResponse($newAssertion, $targetServiceProvider);
122
    }
123
124
    /**
125
     * @param SAML2_Assertion $newAssertion
126
     * @param ServiceProvider $targetServiceProvider
127
     */
128 View Code Duplication
    private function addSubjectConfirmationFor(SAML2_Assertion $newAssertion, ServiceProvider $targetServiceProvider)
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...
129
    {
130
        $confirmation         = new \SAML2_XML_saml_SubjectConfirmation();
131
        $confirmation->Method = \SAML2_Const::CM_BEARER;
132
133
        $confirmationData                      = new \SAML2_XML_saml_SubjectConfirmationData();
134
        $confirmationData->InResponseTo        = $this->proxyStateHandler->getRequestId();
135
        $confirmationData->Recipient           = $targetServiceProvider->getAssertionConsumerUrl();
136
        $confirmationData->NotOnOrAfter        = $this->getTimestamp('PT8H');
137
138
        $confirmation->SubjectConfirmationData = $confirmationData;
139
140
        $newAssertion->setSubjectConfirmation([$confirmation]);
141
    }
142
143
    /**
144
     * @param SAML2_Assertion $newAssertion
145
     * @param SAML2_Assertion $assertion
146
     */
147
    private function addAuthenticationStatementTo(SAML2_Assertion $newAssertion, SAML2_Assertion $assertion)
148
    {
149
        $newAssertion->setAuthnInstant($assertion->getAuthnInstant());
150
        $newAssertion->setAuthnContextClassRef((string) $this->intrinsicLoa);
151
152
        $authority = $assertion->getAuthenticatingAuthority();
153
        $newAssertion->setAuthenticatingAuthority(
154
            array_merge(
155
                (empty($authority) ? [] : $authority),
156
                [$this->hostedIdentityProvider->getEntityId()]
157
            )
158
        );
159
    }
160
161
    /**
162
     * @param SAML2_Assertion $newAssertion
163
     * @param ServiceProvider $targetServiceProvider
164
     * @return \SAML2_Response
165
     */
166 View Code Duplication
    private function createNewAuthnResponse(SAML2_Assertion $newAssertion, ServiceProvider $targetServiceProvider)
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...
167
    {
168
        $response = new \SAML2_Response();
169
        $response->setAssertions([$newAssertion]);
170
        $response->setIssuer($this->hostedIdentityProvider->getEntityId());
171
        $response->setIssueInstant($this->getTimestamp());
172
        $response->setDestination($targetServiceProvider->getAssertionConsumerUrl());
173
        $response->setInResponseTo($this->proxyStateHandler->getRequestId());
174
175
        return $response;
176
    }
177
178
    /**
179
     * @param string $interval a \DateInterval compatible interval to skew the time with
0 ignored issues
show
Documentation introduced by
Should the type for parameter $interval not be string|null?

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...
180
     * @return int
181
     */
182 View Code Duplication
    private function getTimestamp($interval = null)
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...
183
    {
184
        $time = clone $this->currentTime;
185
186
        if ($interval) {
187
            $time->add(new \DateInterval($interval));
188
        }
189
190
        return $time->getTimestamp();
191
    }
192
193
    /**
194
     * @param AssertionAdapter $translatedAssertion
195
     * @return array
196
     */
197
    private function parseEptiNameId(AssertionAdapter $translatedAssertion)
198
    {
199
        /** @var \DOMNodeList[] $eptiValues */
200
        $eptiValues      = $translatedAssertion->getAttributeValue('eduPersonTargetedID');
201
        $eptiDomNodeList = $eptiValues[0];
202
203
        if (!$eptiDomNodeList instanceof \DOMNodeList || $eptiDomNodeList->length !== 1) {
204
            throw new RuntimeException(
205
                'EPTI attribute must contain exactly one NameID element as value:::: ' . print_r($eptiValues, true)
206
            );
207
        }
208
209
        $eptiValue  = $eptiDomNodeList->item(0);
210
        $eptiNameId = \SAML2_Utils::parseNameId($eptiValue);
0 ignored issues
show
Compatibility introduced by
$eptiValue of type object<DOMNode> is not a sub-type of object<DOMElement>. It seems like you assume a child class of the class DOMNode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
211
212
        return $eptiNameId;
213
    }
214
}
215