ADFS::sendPassiveResponse()   B
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 103
Code Lines 69

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 69
c 0
b 0
f 0
nc 6
nop 1
dl 0
loc 103
rs 8.3652

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\adfs\IdP;
6
7
use DateInterval;
8
use DateTimeImmutable;
9
use DateTimeZone;
10
use Exception;
11
use SimpleSAML\Assert\Assert;
12
use SimpleSAML\Configuration;
13
use SimpleSAML\Error;
14
use SimpleSAML\IdP;
15
use SimpleSAML\Logger;
16
use SimpleSAML\Metadata\MetaDataStorageHandler;
17
use SimpleSAML\Module;
18
use SimpleSAML\SAML11\Constants as C;
19
use SimpleSAML\SAML11\XML\saml\Assertion;
20
use SimpleSAML\SAML11\XML\saml\Attribute;
21
use SimpleSAML\SAML11\XML\saml\AttributeStatement;
22
use SimpleSAML\SAML11\XML\saml\AttributeValue;
23
use SimpleSAML\SAML11\XML\saml\Audience;
24
use SimpleSAML\SAML11\XML\saml\AudienceRestrictionCondition;
25
use SimpleSAML\SAML11\XML\saml\AuthenticationStatement;
26
use SimpleSAML\SAML11\XML\saml\Conditions;
27
use SimpleSAML\SAML11\XML\saml\ConfirmationMethod;
28
use SimpleSAML\SAML11\XML\saml\NameIdentifier;
29
use SimpleSAML\SAML11\XML\saml\Subject;
30
use SimpleSAML\SAML11\XML\saml\SubjectConfirmation;
31
use SimpleSAML\SAML2\Constants as SAML2_C;
32
use SimpleSAML\SOAP\Constants as SOAP_C;
33
use SimpleSAML\SOAP\XML\env_200305\Body;
34
use SimpleSAML\SOAP\XML\env_200305\Envelope;
35
use SimpleSAML\SOAP\XML\env_200305\Header;
36
use SimpleSAML\Utils;
37
use SimpleSAML\WSSecurity\XML\wsa_200508\Action;
38
use SimpleSAML\WSSecurity\XML\wsa_200508\Address;
39
use SimpleSAML\WSSecurity\XML\wsa_200508\EndpointReference;
40
use SimpleSAML\WSSecurity\XML\wsa_200508\MessageID;
41
use SimpleSAML\WSSecurity\XML\wsa_200508\RelatesTo;
42
use SimpleSAML\WSSecurity\XML\wsa_200508\To;
43
use SimpleSAML\WSSecurity\XML\wsp\AppliesTo;
44
use SimpleSAML\WSSecurity\XML\wsse\KeyIdentifier;
45
use SimpleSAML\WSSecurity\XML\wsse\Password;
46
use SimpleSAML\WSSecurity\XML\wsse\Security;
47
use SimpleSAML\WSSecurity\XML\wsse\SecurityTokenReference;
48
use SimpleSAML\WSSecurity\XML\wsse\UsernameToken;
49
use SimpleSAML\WSSecurity\XML\wst_200502\KeyType;
50
use SimpleSAML\WSSecurity\XML\wst_200502\Lifetime;
51
use SimpleSAML\WSSecurity\XML\wst_200502\RequestedSecurityToken;
52
use SimpleSAML\WSSecurity\XML\wst_200502\RequestedUnattachedReference;
53
use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityToken;
54
use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityTokenResponse;
55
use SimpleSAML\WSSecurity\XML\wst_200502\RequestType;
56
use SimpleSAML\WSSecurity\XML\wst_200502\RequestTypeEnum;
57
use SimpleSAML\WSSecurity\XML\wst_200502\TokenType;
58
use SimpleSAML\WSSecurity\XML\wsu\Created;
59
use SimpleSAML\WSSecurity\XML\wsu\Expires;
60
use SimpleSAML\WSSecurity\XML\wsu\Timestamp;
61
use SimpleSAML\XHTML\Template;
62
use SimpleSAML\XML\Attribute as XMLAttribute;
63
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
64
use SimpleSAML\XMLSecurity\Key\PrivateKey;
65
use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey;
66
use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
67
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
68
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
69
use Symfony\Component\HttpFoundation\Request;
70
use Symfony\Component\HttpFoundation\StreamedResponse;
71
72
use function array_pop;
73
use function base64_encode;
74
use function chunk_split;
75
use function str_replace;
76
use function trim;
77
78
class ADFS
79
{
80
    /**
81
     * @param \Symfony\Component\HttpFoundation\Request $request
82
     * @param \SimpleSAML\SOAP\XML\env_200305\Envelope $soapEnvelope
83
     * @param \SimpleSAML\Module\adfs\IdP\PassiveIdP $idp
84
     * @throws \SimpleSAML\Error\MetadataNotFound
85
     */
86
    public static function receivePassiveAuthnRequest(
87
        Request $request,
88
        Envelope $soapEnvelope,
89
        PassiveIdP $idp,
90
    ): StreamedResponse {
91
        // Parse the SOAP-header
92
        $header = $soapEnvelope->getHeader();
93
94
        $to = To::getChildrenOfClass($header->toXML());
95
        Assert::count($to, 1, 'Missing To in SOAP Header.');
96
        $to = array_pop($to);
97
98
        $action = Action::getChildrenOfClass($header->toXML());
99
        Assert::count($action, 1, 'Missing Action in SOAP Header.');
100
        $action = array_pop($action);
0 ignored issues
show
Unused Code introduced by
The assignment to $action is dead and can be removed.
Loading history...
101
102
        $messageid = MessageID::getChildrenOfClass($header->toXML());
103
        Assert::count($messageid, 1, 'Missing MessageID in SOAP Header.');
104
        $messageid = array_pop($messageid);
105
106
        $security = Security::getChildrenOfClass($header->toXML());
107
        Assert::count($security, 1, 'Missing Security in SOAP Header.');
108
        $security = array_pop($security);
109
110
        // Parse the SOAP-body
111
        $body = $soapEnvelope->getBody();
112
113
        $requestSecurityToken = RequestSecurityToken::getChildrenOfClass($body->toXML());
114
        Assert::count($requestSecurityToken, 1, 'Missing RequestSecurityToken in SOAP Body.');
115
        $requestSecurityToken = array_pop($requestSecurityToken);
116
117
        $appliesTo = AppliesTo::getChildrenOfClass($requestSecurityToken->toXML());
118
        Assert::count($appliesTo, 1, 'Missing AppliesTo in RequestSecurityToken.');
119
        $appliesTo = array_pop($appliesTo);
120
121
        $endpointReference = EndpointReference::getChildrenOfClass($appliesTo->toXML());
122
        Assert::count($endpointReference, 1, 'Missing EndpointReference in AppliesTo.');
123
        $endpointReference = array_pop($endpointReference);
124
125
        // Make sure the message was addressed to us.
126
        if ($to === null || $request->server->get('SCRIPT_URI') !== $to->getContent()) {
127
            throw new Error\BadRequest('This server is not the audience for the message received.');
128
        }
129
130
        // Ensure we know the issuer
131
        $issuer = $endpointReference->getAddress()->getContent();
132
133
        $metadata = MetaDataStorageHandler::getMetadataHandler(Configuration::getInstance());
0 ignored issues
show
Unused Code introduced by
The call to SimpleSAML\Metadata\Meta...r::getMetadataHandler() has too many arguments starting with SimpleSAML\Configuration::getInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

133
        /** @scrutinizer ignore-call */ 
134
        $metadata = MetaDataStorageHandler::getMetadataHandler(Configuration::getInstance());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
134
        $spMetadata = $metadata->getMetaDataConfig($issuer, 'adfs-sp-remote');
135
136
        $usernameToken = UsernameToken::getChildrenOfClass($security->toXML());
137
        Assert::count($usernameToken, 1, 'Missing UsernameToken in Security.');
138
        $usernameToken = array_pop($usernameToken);
139
140
        $username = $usernameToken->getUsername();
141
        $password = Password::getChildrenOfClass($usernameToken->toXML());
142
        $password = array_pop($password);
143
144
        if ($password === null) {
145
            throw new Error\BadRequest('Missing username or password in SOAP header.');
146
        } else {
147
            $_SERVER['PHP_AUTH_USER'] = $username->getContent();
148
            $_SERVER['PHP_AUTH_PW'] = $password->getContent();
149
        }
150
151
        $requestSecurityTokenStr = $requestSecurityToken->toXML()->ownerDocument->saveXML();
152
        $requestSecurityTokenStr = str_replace($password->getContent(), '*****', $requestSecurityTokenStr);
153
        Logger::debug($requestSecurityTokenStr);
154
155
        $state = [
156
            'Responder' => [ADFS::class, 'sendPassiveResponse'],
157
            'SPMetadata' => $spMetadata->toArray(),
158
            'MessageID' => $messageid->getContent(),
159
            // Dirty hack to leverage the SAML ECP logics
160
            'saml:Binding' => SAML2_C::BINDING_PAOS,
161
        ];
162
163
        return new StreamedResponse(
164
            function () use ($idp, &$state) {
165
                $idp->handleAuthenticationRequest($state);
166
            },
167
        );
168
    }
169
170
171
    /**
172
     * @param \Symfony\Component\HttpFoundation\Request $request
173
     * @param \SimpleSAML\IdP $idp
174
     * @throws \SimpleSAML\Error\MetadataNotFound
175
     */
176
    public static function receiveAuthnRequest(Request $request, IdP $idp): StreamedResponse
177
    {
178
        parse_str($request->server->get('QUERY_STRING'), $query);
179
180
        $requestid = $query['wctx'] ?? null;
181
        $issuer = $query['wtrealm'];
182
183
        $metadata = MetaDataStorageHandler::getMetadataHandler(Configuration::getInstance());
0 ignored issues
show
Unused Code introduced by
The call to SimpleSAML\Metadata\Meta...r::getMetadataHandler() has too many arguments starting with SimpleSAML\Configuration::getInstance(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
        /** @scrutinizer ignore-call */ 
184
        $metadata = MetaDataStorageHandler::getMetadataHandler(Configuration::getInstance());

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
184
        $spMetadata = $metadata->getMetaDataConfig($issuer, 'adfs-sp-remote');
185
186
        Logger::info('ADFS - IdP.prp: Incoming Authentication request: ' . $issuer . ' id ' . $requestid);
187
188
        $username = null;
189
        if ($request->query->has('username')) {
190
            $username = (string) $request->query->get('username');
191
        }
192
193
        $wauth = null;
194
        if ($request->query->has('wauth')) {
195
            $wauth = (string) $request->query->get('wauth');
196
        }
197
198
        $state = [
199
            'Responder' => [ADFS::class, 'sendResponse'],
200
            'SPMetadata' => $spMetadata->toArray(),
201
            'ForceAuthn' => false,
202
            'isPassive' => false,
203
            'adfs:wctx' => $requestid,
204
            'adfs:wreply' => false,
205
        ];
206
207
        if ($username !== null) {
208
            $state['core:username'] = $username;
209
        }
210
211
        if ($wauth !== null) {
212
            $state['saml:RequestedAuthnContext'] = ['AuthnContextClassRef' => [$wauth]];
213
        }
214
215
        if (isset($query['wreply']) && !empty($query['wreply'])) {
216
            $httpUtils = new Utils\HTTP();
217
            $state['adfs:wreply'] = $httpUtils->checkURLAllowed($query['wreply']);
218
        }
219
220
        return new StreamedResponse(
221
            function () use ($idp, &$state) {
222
                $idp->handleAuthenticationRequest($state);
223
            },
224
        );
225
    }
226
227
228
    /**
229
     * @param string $issuer
230
     * @param string $target
231
     * @param string $nameid
232
     * @param array<mixed> $attributes
233
     * @param int $assertionLifetime
234
     * @param string $method
235
     * @return \SimpleSAML\SAML11\XML\saml\Assertion
236
     */
237
    private static function generateActiveAssertion(
238
        string $issuer,
239
        string $target,
240
        string $nameid,
241
        array $attributes,
242
        int $assertionLifetime,
243
        string $method,
244
    ): Assertion {
245
        $httpUtils = new Utils\HTTP();
0 ignored issues
show
Unused Code introduced by
The assignment to $httpUtils is dead and can be removed.
Loading history...
246
        $randomUtils = new Utils\Random();
247
        $timeUtils = new Utils\Time();
248
249
        $issueInstant = $timeUtils->generateTimestamp();
0 ignored issues
show
Unused Code introduced by
The assignment to $issueInstant is dead and can be removed.
Loading history...
250
        $notBefore = DateInterval::createFromDateString('30 seconds');
251
        $notOnOrAfter = DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime));
252
        $assertionID = $randomUtils->generateID();
253
        $nameidFormat = 'http://schemas.xmlsoap.org/claims/UPN';
254
        $nameid = htmlspecialchars($nameid);
255
        $now = new DateTimeImmutable('now', new DateTimeZone('Z'));
256
257
        $audience = new Audience($target);
258
        $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]);
259
        $conditions = new Conditions(
260
            [$audienceRestrictionCondition],
261
            [],
262
            [],
263
            $now->sub($notBefore),
264
            $now->add($notOnOrAfter),
265
        );
266
267
        $nameIdentifier = new NameIdentifier($nameid, null, $nameidFormat);
268
        $subject = new Subject(null, $nameIdentifier);
269
270
        $authenticationStatement = new AuthenticationStatement($subject, $method, $now);
271
272
        $attrs = [];
273
        $attrUtils = new Utils\Attributes();
274
        foreach ($attributes as $name => $values) {
275
            if ((!is_array($values)) || (count($values) == 0)) {
276
                continue;
277
            }
278
279
            list($namespace, $name) = $attrUtils->getAttributeNamespace(
280
                $name,
281
                'http://schemas.xmlsoap.org/claims',
282
            );
283
284
            $namespace = htmlspecialchars($namespace);
285
            $name = htmlspecialchars($name);
286
            $attrValue = [];
287
            foreach ($values as $value) {
288
                if ((!isset($value)) || ($value === '')) {
289
                    continue;
290
                }
291
                $attrValue[] = new AttributeValue($value);
292
            }
293
            $attrs[] = new Attribute($name, $namespace, $attrValue);
294
        }
295
        $attributeStatement = new AttributeStatement($subject, $attrs);
296
297
        return new Assertion(
298
            $assertionID,
299
            $issuer,
300
            $now,
301
            $conditions,
302
            null, // Advice
303
            [$authenticationStatement, $attributeStatement],
304
        );
305
    }
306
307
308
    /**
309
     * @param string $issuer
310
     * @param string $target
311
     * @param string $nameid
312
     * @param array<mixed> $attributes
313
     * @param int $assertionLifetime
314
     * @return \SimpleSAML\SAML11\XML\saml\Assertion
315
     */
316
    private static function generatePassiveAssertion(
317
        string $issuer,
318
        string $target,
319
        string $nameid,
320
        array $attributes,
321
        int $assertionLifetime,
322
    ): Assertion {
323
        $httpUtils = new Utils\HTTP();
324
        $randomUtils = new Utils\Random();
325
        $timeUtils = new Utils\Time();
326
327
        $issueInstant = $timeUtils->generateTimestamp();
0 ignored issues
show
Unused Code introduced by
The assignment to $issueInstant is dead and can be removed.
Loading history...
328
        $notBefore = DateInterval::createFromDateString('30 seconds');
329
        $notOnOrAfter = DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime));
330
        $assertionID = $randomUtils->generateID();
331
        $now = new DateTimeImmutable('now', new DateTimeZone('Z'));
332
333
        if ($httpUtils->isHTTPS()) {
334
            $method = SAML2_C::AC_PASSWORD_PROTECTED_TRANSPORT;
335
        } else {
336
            $method = C::AC_PASSWORD;
337
        }
338
339
        $audience = new Audience($target);
340
        $audienceRestrictionCondition = new AudienceRestrictionCondition([$audience]);
341
        $conditions = new Conditions(
342
            [$audienceRestrictionCondition],
343
            [],
344
            [],
345
            $now->sub($notBefore),
346
            $now->add($notOnOrAfter),
347
        );
348
349
        $nameIdentifier = new NameIdentifier($nameid, null, C::NAMEID_UNSPECIFIED);
350
        $subject = new Subject(new SubjectConfirmation([new ConfirmationMethod(C::CM_BEARER)]), $nameIdentifier);
351
352
        $authenticationStatement = new AuthenticationStatement($subject, $method, $now);
353
354
        $attrs = [];
355
        $attrs[] = new Attribute(
356
            'UPN',
357
            'http://schemas.xmlsoap.org/claims',
358
            [new AttributeValue($attributes['http://schemas.xmlsoap.org/claims/UPN'][0])],
359
        );
360
        $attrs[] = new Attribute(
361
            'ImmutableID',
362
            'http://schemas.microsoft.com/LiveID/Federation/2008/05',
363
            [new AttributeValue($attributes['http://schemas.microsoft.com/LiveID/Federation/2008/05/ImmutableID'][0])],
364
        );
365
366
        $attributeStatement = new AttributeStatement($subject, $attrs);
367
368
        return new Assertion(
369
            $assertionID,
370
            $issuer,
371
            $now,
372
            $conditions,
373
            null, // Advice
374
            [$attributeStatement, $authenticationStatement],
375
        );
376
    }
377
378
379
    /**
380
     * @param \SimpleSAML\SAML11\XML\saml\Assertion $assertion
381
     * @param string $key
382
     * @param string $cert
383
     * @param string $algo
384
     * @param string|null $passphrase
385
     * @return \SimpleSAML\SAML11\XML\saml\Assertion
386
     */
387
    private static function signAssertion(
388
        Assertion $assertion,
389
        string $key,
390
        string $cert,
391
        string $algo,
392
        #[\SensitiveParameter]
393
        ?string $passphrase = null,
394
    ): Assertion {
395
        $key = PrivateKey::fromFile($key, $passphrase);
0 ignored issues
show
Bug introduced by
It seems like $passphrase can also be of type null; however, parameter $passphrase of SimpleSAML\XMLSecurity\Key\PrivateKey::fromFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

395
        $key = PrivateKey::fromFile($key, /** @scrutinizer ignore-type */ $passphrase);
Loading history...
396
        $pubkey = PublicKey::fromFile($cert);
397
        $keyInfo = new KeyInfo([
398
            new X509Data(
399
                [new X509Certificate(
400
                    trim(chunk_split(base64_encode($pubkey->getPEM()->data()))),
401
                )],
402
            ),
403
        ]);
404
405
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
406
            $algo,
407
            $key,
408
        );
409
410
        $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
411
        return $assertion;
412
    }
413
414
415
    /**
416
     * @param string $wreply
417
     * @param string $wresult
418
     * @param ?string $wctx
419
     */
420
    private static function postResponse(string $wreply, string $wresult, ?string $wctx): void
421
    {
422
        $config = Configuration::getInstance();
423
        $t = new Template($config, 'adfs:postResponse.twig');
424
        $t->data['wreply'] = $wreply;
425
        $t->data['wresult'] = $wresult;
426
        $t->data['wctx'] = $wctx;
427
        $t->send();
428
        // Idp->postAuthProc expects this function to exit
429
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
430
    }
431
432
433
    /**
434
     * @param array<mixed> $state
435
     * @throws \Exception
436
     */
437
    public static function sendPassiveResponse(array $state): void
438
    {
439
        $idp = IdP::getByState($state);
440
        $idpMetadata = $idp->getConfig();
441
        $idpEntityId = $state['IdPMetadata']['entityid'];
442
443
        $spMetadata = $state['SPMetadata'];
444
        $spEntityId = $spMetadata['entityid'];
445
        $spMetadata = Configuration::loadFromArray(
446
            $spMetadata,
447
            '$metadata[' . var_export($spEntityId, true) . ']',
448
        );
449
450
        $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null);
451
        if ($assertionLifetime === null) {
452
            $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300);
453
        }
454
455
        $now = new DateTimeImmutable('now', new DateTimeZone('Z'));
456
        $created = $now->sub(DateInterval::createFromDateString(sprintf('30 seconds')));
457
        $expires = $now->add(DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime)));
458
459
        $attributes = $state['Attributes'];
460
        $nameid = $state['saml:NameID'][SAML2_C::NAMEID_UNSPECIFIED];
461
462
        $assertion = ADFS::generatePassiveAssertion($idpEntityId, $spEntityId, $nameid->getValue(), $attributes, $assertionLifetime);
0 ignored issues
show
Bug introduced by
It seems like $assertionLifetime can also be of type null; however, parameter $assertionLifetime of SimpleSAML\Module\adfs\I...eratePassiveAssertion() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

462
        $assertion = ADFS::generatePassiveAssertion($idpEntityId, $spEntityId, $nameid->getValue(), $attributes, /** @scrutinizer ignore-type */ $assertionLifetime);
Loading history...
463
464
        $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null);
465
        $certificateCfg = $idpMetadata->getOptionalString('certificate', null);
466
467
        if ($privateKeyCfg !== null && $certificateCfg !== null) {
468
            $configUtils = new Utils\Config();
469
            $privateKeyFile = $configUtils->getCertPath($privateKeyCfg);
470
            $certificateFile = $configUtils->getCertPath($certificateCfg);
471
            $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null);
472
473
            $algo = $spMetadata->getOptionalString('signature.algorithm', null);
474
            if ($algo === null) {
475
                $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
476
            }
477
478
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase);
0 ignored issues
show
Bug introduced by
It seems like $algo can also be of type null; however, parameter $algo of SimpleSAML\Module\adfs\IdP\ADFS::signAssertion() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

478
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, /** @scrutinizer ignore-type */ $algo, $passphrase);
Loading history...
479
            $assertion = Assertion::fromXML($assertion->toXML());
480
        }
481
482
        $requestedSecurityToken = new RequestedSecurityToken($assertion);
483
        $lifetime = new LifeTime(new Created($created), new Expires($expires));
484
        $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]);
485
486
        $requestedAttachedReference = new RequestedAttachedReference(
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\adfs\I...uestedAttachedReference was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
487
            new SecurityTokenReference(null, null, [
488
                new KeyIdentifier(
489
                    $assertion->getId(),
490
                    'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID',
491
                ),
492
            ]),
493
        );
494
        $requestedUnattachedReference = new RequestedUnattachedReference(
495
            new SecurityTokenReference(null, null, [
496
                new KeyIdentifier(
497
                    $assertion->getId(),
498
                    'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID',
499
                ),
500
            ]),
501
        );
502
        $tokenType = new TokenType(C::NS_SAML);
503
        $requestType = new RequestType([RequestTypeEnum::Issue]);
504
        $keyType = new KeyType(['http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey']);
505
506
        $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [
507
            $lifetime,
508
            $appliesTo,
509
            $requestedSecurityToken,
510
            $requestedAttachedReference,
511
            $requestedUnattachedReference,
512
            $tokenType,
513
            $requestType,
514
            $keyType,
515
        ]);
516
517
        // Build envelope
518
        $mustUnderstand = new XMLAttribute(SOAP_C::NS_SOAP_ENV_12, 'env', 'mustUnderstand', '1');
519
        $header = new Header([
520
            new Action('http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue', [$mustUnderstand]),
521
            new RelatesTo($state['MessageID'], null),
522
            new Security(
523
                [
524
                    new Timestamp(
525
                        new Created($created),
526
                        new Expires($expires),
527
                    ),
528
                ],
529
                [$mustUnderstand],
530
            ),
531
        ]);
532
        $body = new Body(null, [$requestSecurityTokenResponse]);
533
        $envelope = new Envelope($body, $header);
534
535
        $xmlResponse = $envelope->toXML();
536
        \SimpleSAML\Logger::debug($xmlResponse->ownerDocument->saveXML($xmlResponse));
0 ignored issues
show
Bug introduced by
The method saveXML() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

536
        \SimpleSAML\Logger::debug($xmlResponse->ownerDocument->/** @scrutinizer ignore-call */ saveXML($xmlResponse));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
537
538
        echo $xmlResponse->ownerDocument->saveXML($xmlResponse);
539
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
540
    }
541
542
543
    /**
544
     * @param array<mixed> $state
545
     * @throws \Exception
546
     */
547
    public static function sendResponse(array $state): void
548
    {
549
        $spMetadata = $state['SPMetadata'];
550
        $spEntityId = $spMetadata['entityid'];
551
        $spMetadata = Configuration::loadFromArray(
552
            $spMetadata,
553
            '$metadata[' . var_export($spEntityId, true) . ']',
554
        );
555
556
        $attributes = $state['Attributes'];
557
558
        $nameidattribute = $spMetadata->getValue('simplesaml.nameidattribute');
559
        if (!empty($nameidattribute)) {
560
            if (!array_key_exists($nameidattribute, $attributes)) {
561
                throw new Exception('simplesaml.nameidattribute does not exist in resulting attribute set');
562
            }
563
            $nameid = $attributes[$nameidattribute][0];
564
        } else {
565
            $randomUtils = new Utils\Random();
566
            $nameid = $randomUtils->generateID();
567
        }
568
569
        $idp = IdP::getByState($state);
570
        $idpMetadata = $idp->getConfig();
571
        $idpEntityId = $state['IdPMetadata']['entityid'];
572
573
        $idp->addAssociation([
574
            'id' => 'adfs:' . $spEntityId,
575
            'Handler' => ADFS::class,
576
            'adfs:entityID' => $spEntityId,
577
        ]);
578
579
        $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null);
580
        if ($assertionLifetime === null) {
581
            $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300);
582
        }
583
584
        if (isset($state['saml:AuthnContextClassRef'])) {
585
            $method = $state['saml:AuthnContextClassRef'];
586
        } elseif ((new Utils\HTTP())->isHTTPS()) {
587
            $method = SAML2_C::AC_PASSWORD_PROTECTED_TRANSPORT;
588
        } else {
589
            $method = C::AC_PASSWORD;
590
        }
591
592
        $assertion = ADFS::generateActiveAssertion($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime, $method);
0 ignored issues
show
Bug introduced by
It seems like $assertionLifetime can also be of type null; however, parameter $assertionLifetime of SimpleSAML\Module\adfs\I...nerateActiveAssertion() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

592
        $assertion = ADFS::generateActiveAssertion($idpEntityId, $spEntityId, $nameid, $attributes, /** @scrutinizer ignore-type */ $assertionLifetime, $method);
Loading history...
593
594
        $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null);
595
        $certificateCfg = $idpMetadata->getOptionalString('certificate', null);
596
597
        if ($privateKeyCfg !== null && $certificateCfg !== null) {
598
            $configUtils = new Utils\Config();
599
            $privateKeyFile = $configUtils->getCertPath($privateKeyCfg);
600
            $certificateFile = $configUtils->getCertPath($certificateCfg);
601
            $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null);
602
603
            $algo = $spMetadata->getOptionalString('signature.algorithm', null);
604
            if ($algo === null) {
605
                $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
606
            }
607
608
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase);
0 ignored issues
show
Bug introduced by
It seems like $algo can also be of type null; however, parameter $algo of SimpleSAML\Module\adfs\IdP\ADFS::signAssertion() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

608
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, /** @scrutinizer ignore-type */ $algo, $passphrase);
Loading history...
609
            $assertion = Assertion::fromXML($assertion->toXML());
610
        }
611
612
        $requestSecurityToken = new RequestSecurityToken(null, [$assertion]);
613
        $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]);
614
        $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]);
615
616
        $xmlResponse = $requestSecurityTokenResponse->toXML();
617
        $wresult = $xmlResponse->ownerDocument->saveXML($xmlResponse);
618
        Logger::debug($wresult);
619
620
        $wctx = $state['adfs:wctx'];
621
        $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp');
622
        ADFS::postResponse($wreply, $wresult, $wctx);
623
    }
624
625
626
    /**
627
     * @param \SimpleSAML\IdP $idp
628
     * @param array<mixed> $state
629
     */
630
    public static function sendLogoutResponse(IdP $idp, array $state): void
0 ignored issues
show
Unused Code introduced by
The parameter $state is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

630
    public static function sendLogoutResponse(IdP $idp, /** @scrutinizer ignore-unused */ array $state): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
631
    {
632
        // NB:: we don't know from which SP the logout request came from
633
        $idpMetadata = $idp->getConfig();
634
        $httpUtils = new Utils\HTTP();
635
        $httpUtils->redirectTrustedURL(
636
            $idpMetadata->getOptionalString('redirect-after-logout', $httpUtils->getBaseURL()),
0 ignored issues
show
Bug introduced by
It seems like $idpMetadata->getOptiona...ttpUtils->getBaseURL()) can also be of type null; however, parameter $url of SimpleSAML\Utils\HTTP::redirectTrustedURL() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

636
            /** @scrutinizer ignore-type */ $idpMetadata->getOptionalString('redirect-after-logout', $httpUtils->getBaseURL()),
Loading history...
637
        );
638
    }
639
640
641
    /**
642
     * @param \SimpleSAML\IdP $idp
643
     * @throws \Exception
644
     */
645
    public static function receiveLogoutMessage(IdP $idp): void
646
    {
647
        // if a redirect is to occur based on wreply, we will redirect to url as
648
        // this implies an override to normal sp notification
649
        if (isset($_GET['wreply']) && !empty($_GET['wreply'])) {
650
            $httpUtils = new Utils\HTTP();
651
            $idp->doLogoutRedirect($httpUtils->checkURLAllowed($_GET['wreply']));
652
            throw new Exception("Code should never be reached");
653
        }
654
655
        $state = [
656
            'Responder' => [ADFS::class, 'sendLogoutResponse'],
657
        ];
658
        $assocId = null;
659
        // TODO: verify that this is really no problem for:
660
        //       a) SSP, because there's no caller SP.
661
        //       b) ADFS SP because caller will be called back..
662
        $idp->handleLogoutRequest($state, $assocId);
663
    }
664
665
666
    /**
667
     * accepts an association array, and returns a URL that can be accessed to terminate the association
668
     *
669
     * @param \SimpleSAML\IdP $idp
670
     * @param array<mixed> $association
671
     * @param string $relayState
672
     * @return string
673
     */
674
    public static function getLogoutURL(IdP $idp, array $association, string $relayState): string
675
    {
676
        $metadata = MetaDataStorageHandler::getMetadataHandler();
677
        $spMetadata = $metadata->getMetaDataConfig($association['adfs:entityID'], 'adfs-sp-remote');
678
        $returnTo = Module::getModuleURL(
679
            'adfs/idp/prp.php?assocId=' . urlencode($association["id"]) . '&relayState=' . urlencode($relayState),
680
        );
681
        return $spMetadata->getValue('prp') . '?wa=wsignoutcleanup1.0&wreply=' . urlencode($returnTo);
682
    }
683
}
684