AssertionConsumer::validateAssertionSubjectTime()   B
last analyzed

Complexity

Conditions 8
Paths 6

Size

Total Lines 16
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 16
rs 7.7778
cc 8
eloc 9
nc 6
nop 1
1
<?php
2
3
namespace AerialShip\SamlSPBundle\Bridge;
4
5
use AerialShip\LightSaml\Bindings;
6
use AerialShip\LightSaml\Model\Assertion\Assertion;
7
use AerialShip\LightSaml\Model\Protocol\Response;
8
use AerialShip\LightSaml\Model\XmlDSig\SignatureXmlValidator;
9
use AerialShip\LightSaml\Security\KeyHelper;
10
use AerialShip\SamlSPBundle\Config\ServiceInfo;
11
use AerialShip\SamlSPBundle\Config\ServiceInfoCollection;
12
use AerialShip\SamlSPBundle\RelyingParty\RelyingPartyInterface;
13
use AerialShip\SamlSPBundle\State\Request\RequestStateStoreInterface;
14
use AerialShip\SamlSPBundle\State\SSO\SSOStateStoreInterface;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\Security\Core\Exception\AuthenticationException;
17
18
class AssertionConsumer implements RelyingPartyInterface
19
{
20
    /** @var BindingManager  */
21
    protected $bindingManager;
22
23
    /** @var  ServiceInfoCollection */
24
    protected $serviceInfoCollection;
25
26
    /** @var  RequestStateStoreInterface */
27
    protected $requestStore;
28
29
    /** @var SSOStateStoreInterface  */
30
    protected $ssoStore;
31
32
33
34
35
    public function __construct(
36
        BindingManager $bindingManager,
37
        ServiceInfoCollection $serviceInfoCollection,
38
        RequestStateStoreInterface $requestStore,
39
        SSOStateStoreInterface $ssoStore
40
    ) {
41
        $this->bindingManager = $bindingManager;
42
        $this->serviceInfoCollection = $serviceInfoCollection;
43
        $this->requestStore = $requestStore;
44
        $this->ssoStore = $ssoStore;
45
    }
46
47
48
49
    /**
50
     * @param \Symfony\Component\HttpFoundation\Request $request
51
     * @return bool
52
     */
53
    public function supports(Request $request)
54
    {
55
        $result = $request->attributes->get('check_path') == $request->getPathInfo();
56
        return $result;
57
    }
58
59
    /**
60
     * @param \Symfony\Component\HttpFoundation\Request $request
61
     * @throws \RuntimeException
62
     * @throws \Symfony\Component\Security\Core\Exception\AuthenticationException
63
     * @throws \InvalidArgumentException if cannot manage the Request
64
     * @return \Symfony\Component\HttpFoundation\RedirectResponse|SamlSpInfo
65
     */
66
    public function manage(Request $request)
67
    {
68
        if (!$this->supports($request)) {
69
            throw new \InvalidArgumentException();
70
        }
71
72
        $response = $this->getSamlResponse($request);
73
        $serviceInfo = $this->serviceInfoCollection->findByIDPEntityID($response->getIssuer());
74
        
75
        if (!$serviceInfo) {
76
            throw new \RuntimeException('Could not find ServiceProvider with entity id: '.$response->getIssuer());
77
        }
78
79
        $serviceInfo->getSpProvider()->setRequest($request);
80
        $this->validateResponse($serviceInfo, $response);
81
82
        $assertion = $this->getSingleAssertion($response);
83
84
        $this->createSSOState($serviceInfo, $assertion);
0 ignored issues
show
Bug introduced by
It seems like $assertion defined by $this->getSingleAssertion($response) on line 82 can be null; however, AerialShip\SamlSPBundle\...sumer::createSSOState() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
85
86
        return new SamlSpInfo(
87
            $serviceInfo->getAuthenticationService(),
88
            $assertion->getSubject()->getNameID(),
89
            $assertion->getAllAttributes(),
90
            $assertion->getAuthnStatement()
91
        );
92
    }
93
94
95
    protected function getSamlResponse(Request $request)
96
    {
97
        $bindingType = null;
98
        /** @var Response $response */
99
        $response = $this->bindingManager->receive($request, $bindingType);
100
        if ($bindingType == Bindings::SAML2_HTTP_REDIRECT) {
101
            throw new \RuntimeException('SAML protocol response cannot be sent via binding HTTP REDIRECT');
102
        }
103
        if (!$response instanceof Response) {
104
            throw new \RuntimeException('Expected Protocol/Response type but got '.($response ? get_class($response) : 'nothing'));
105
        }
106
107
        return $response;
108
    }
109
110
    /**
111
     * @param Response $response
112
     * @return Assertion
113
     * @throws \RuntimeException
114
     */
115
    protected function getSingleAssertion(Response $response)
116
    {
117
        $arr = $response->getAllAssertions();
118
        if (empty($arr)) {
119
            throw new \RuntimeException('No assertion received');
120
        }
121
        $assertion = array_pop($arr);
122
123
        return $assertion;
124
    }
125
126
127
    protected function createSSOState(ServiceInfo $serviceInfo, Assertion $assertion)
128
    {
129
        $ssoState = $this->ssoStore->create();
130
        $ssoState->setNameID($assertion->getSubject()->getNameID()->getValue());
131
        $ssoState->setNameIDFormat($assertion->getSubject()->getNameID()->getFormat() ?: '');
132
        $ssoState->setAuthenticationServiceName($serviceInfo->getAuthenticationService());
133
        $ssoState->setProviderID($serviceInfo->getProviderID());
134
        $ssoState->setSessionIndex($assertion->getAuthnStatement()->getSessionIndex());
135
        $this->ssoStore->set($ssoState);
136
137
        return $ssoState;
138
    }
139
140
141
    protected function validateResponse(ServiceInfo $metaProvider, Response $response)
142
    {
143
        if (!$metaProvider) {
144
            throw new \RuntimeException('Unknown issuer '.$response->getIssuer());
145
        }
146
        $this->validateState($response);
147
        $this->validateStatus($response);
148
        $this->validateResponseSignature($metaProvider, $response);
149
        foreach ($response->getAllAssertions() as $assertion) {
150
            $this->validateAssertion($metaProvider, $assertion);
151
        }
152
    }
153
154
    protected function validateState(Response $response)
155
    {
156
        if ($response->getInResponseTo()) {
157
            $requestState = $this->requestStore->get($response->getInResponseTo());
158
            if (!$requestState) {
159
                throw new \RuntimeException('Got response to a request that was not made');
160
            }
161
            if ($requestState->getDestination() != $response->getIssuer()) {
162
                throw new \RuntimeException('Got response from different issuer');
163
            }
164
            $this->requestStore->remove($requestState);
165
        }
166
    }
167
168
    protected function validateStatus(Response $response)
169
    {
170
        if (!$response->getStatus()->isSuccess()) {
171
            $status = $response->getStatus()->getStatusCode()->getValue();
172
            $status .= "\n".$response->getStatus()->getMessage();
173
            if ($response->getStatus()->getStatusCode()->getChild()) {
174
                $status .= "\n".$response->getStatus()->getStatusCode()->getChild()->getValue();
175
            }
176
            throw new AuthenticationException('Unsuccessful SAML response: '.$status);
177
        }
178
    }
179
180
    protected function validateResponseSignature(ServiceInfo $serviceInfo, Response $response)
181
    {
182
        /** @var  $signature SignatureXmlValidator */
183
        if ($signature = $response->getSignature()) {
184
            $keys = $this->getAllKeys($serviceInfo);
185
            $signature->validateMulti($keys);
186
        }
187
    }
188
189
    protected function validateAssertion(ServiceInfo $serviceInfo, Assertion $assertion)
190
    {
191
        $this->validateAssertionSignature($assertion, $serviceInfo);
192
        $this->validateAssertionTime($assertion);
193
        $this->validateAssertionSubjectTime($assertion);
194
        $this->validateSubjectConfirmationRecipient($assertion, $serviceInfo);
195
    }
196
197
    protected function validateAssertionSignature(Assertion $assertion, ServiceInfo $serviceInfo)
198
    {
199
        /** @var  $signature SignatureXmlValidator */
200
        if ($signature = $assertion->getSignature()) {
201
            $keys = $this->getAllKeys($serviceInfo);
202
            $signature->validateMulti($keys);
203
        } else {
204
            throw new AuthenticationException('Assertion must be signed');
205
        }
206
    }
207
208
209
    /**
210
     * @param Assertion $assertion
211
     * @throws \Symfony\Component\Security\Core\Exception\AuthenticationException
212
     */
213
    protected function validateAssertionTime(Assertion $assertion)
214
    {
215
        if ($assertion->getNotBefore() && $assertion->getNotBefore() > time() + 60) {
216
            throw new AuthenticationException('Received an assertion that is valid in the future. Check clock synchronization on IdP and SP');
217
        }
218
        if ($assertion->getNotOnOrAfter() && $assertion->getNotOnOrAfter() <= time() - 60) {
219
            throw new AuthenticationException('Received an assertion that has expired. Check clock synchronization on IdP and SP');
220
        }
221
    }
222
223
    /**
224
     * @param Assertion $assertion
225
     * @throws \Symfony\Component\Security\Core\Exception\AuthenticationException
226
     */
227
    protected function validateAssertionSubjectTime(Assertion $assertion)
228
    {
229
        $arrSubjectConfirmations = $assertion->getSubject()->getSubjectConfirmations();
230
        if ($arrSubjectConfirmations) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arrSubjectConfirmations of type AerialShip\LightSaml\Mod...n\SubjectConfirmation[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
231
            foreach ($arrSubjectConfirmations as $subjectConfirmation) {
232
                if ($data = $subjectConfirmation->getData()) {
233
                    if ($data->getNotBefore() && $data->getNotBefore() > time() + 60) {
234
                        throw new AuthenticationException('Received an assertion with a session valid in future. Check clock synchronization on IdP and SP');
235
                    }
236
                    if ($data->getNotOnOrAfter() && $data->getNotOnOrAfter() <= time() - 60) {
237
                        throw new AuthenticationException('Received an assertion with a session that has expired. Check clock synchronization on IdP and SP');
238
                    }
239
                }
240
            }
241
        }
242
    }
243
244
245
    protected function validateSubjectConfirmationRecipient(Assertion $assertion, ServiceInfo $serviceInfo)
246
    {
247
        $arrACS = $serviceInfo->getSpProvider()
248
                ->getEntityDescriptor()
249
                ->getFirstSpSsoDescriptor()
250
                ->findAssertionConsumerServices();
251
        foreach ($assertion->getSubject()->getSubjectConfirmations() as $subjectConfirmation) {
252
            $ok = false;
253
            foreach ($arrACS as $acs) {
254
                if ($acs->getLocation() == $subjectConfirmation->getData()->getRecipient()) {
255
                    $ok = true;
256
                    break;
257
                }
258
            }
259
            if (!$ok) {
260
                throw new AuthenticationException(
261
                    sprintf(
262
                        'Invalid Assertion SubjectConfirmation Recipient %s',
263
                        $subjectConfirmation->getData()->getRecipient()
264
                    )
265
                );
266
            }
267
        }
268
    }
269
270
    /**
271
     * @param ServiceInfo $metaProvider
272
     * @return \XMLSecurityKey[]
273
     */
274
    protected function getAllKeys(ServiceInfo $metaProvider)
275
    {
276
        $result = array();
277
        $edIDP = $metaProvider->getIdpProvider()->getEntityDescriptor();
278
        if ($edIDP) {
279
            $arr = $edIDP->getAllIdpSsoDescriptors();
280
            if ($arr) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arr of type array<AerialShip\LightSa...a\LoadFromXmlInterface> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
281
                $idp = $arr[0];
282
                $keyDescriptors = $idp->getKeyDescriptors();
283
                foreach ($keyDescriptors as $keyDescriptor) {
284
                    $certificate = $keyDescriptor->getCertificate();
285
                    $result[] = KeyHelper::createPublicKey($certificate);
286
                }
287
            }
288
        }
289
290
        return $result;
291
    }
292
293
    /**
294
     * @param \AerialShip\SamlSPBundle\Config\ServiceInfo $metaProvider
295
     * @return null|\XMLSecurityKey
296
     */
297
    protected function getSigningKey(ServiceInfo $metaProvider)
298
    {
299
        $result = null;
300
        $edIDP = $metaProvider->getIdpProvider()->getEntityDescriptor();
301
        if ($edIDP) {
302
            $arr = $edIDP->getAllIdpSsoDescriptors();
303
            if ($arr) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $arr of type array<AerialShip\LightSa...a\LoadFromXmlInterface> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
304
                $idp = $arr[0];
305
                $arr = $idp->findKeyDescriptors('signing');
306
                if ($arr) {
307
                    $keyDescriptor = $arr[0];
308
                    $certificate = $keyDescriptor->getCertificate();
309
                    $result = KeyHelper::createPublicKey($certificate);
310
                }
311
            }
312
        }
313
        
314
        return $result;
315
    }
316
}
317