Issues (34)

src/IdP/ADFS.php (18 issues)

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\RequestedAttachedReference;
52
use SimpleSAML\WSSecurity\XML\wst_200502\RequestedSecurityToken;
53
use SimpleSAML\WSSecurity\XML\wst_200502\RequestedUnattachedReference;
54
use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityToken;
55
use SimpleSAML\WSSecurity\XML\wst_200502\RequestSecurityTokenResponse;
56
use SimpleSAML\WSSecurity\XML\wst_200502\RequestType;
57
use SimpleSAML\WSSecurity\XML\wst_200502\RequestTypeEnum;
58
use SimpleSAML\WSSecurity\XML\wst_200502\TokenType;
59
use SimpleSAML\WSSecurity\XML\wsu\Created;
60
use SimpleSAML\WSSecurity\XML\wsu\Expires;
61
use SimpleSAML\WSSecurity\XML\wsu\Timestamp;
62
use SimpleSAML\XHTML\Template;
63
use SimpleSAML\XML\Attribute as XMLAttribute;
64
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
65
use SimpleSAML\XMLSecurity\Key\PrivateKey;
66
use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey;
67
use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
68
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
69
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
70
use Symfony\Component\HttpFoundation\Request;
71
use Symfony\Component\HttpFoundation\StreamedResponse;
72
73
use function array_pop;
74
use function base64_encode;
75
use function chunk_split;
76
use function str_replace;
77
use function trim;
78
79
class ADFS
80
{
81
    /**
82
     * @param \Symfony\Component\HttpFoundation\Request $request
83
     * @param \SimpleSAML\SOAP\XML\env_200305\Envelope $soapEnvelope
84
     * @param \SimpleSAML\Module\adfs\IdP\PassiveIdP $idp
85
     * @throws \SimpleSAML\Error\MetadataNotFound
86
     */
87
    public static function receivePassiveAuthnRequest(
88
        Request $request,
89
        Envelope $soapEnvelope,
90
        PassiveIdP $idp,
91
    ): StreamedResponse {
92
        // Parse the SOAP-header
93
        $header = $soapEnvelope->getHeader();
94
95
        $to = To::getChildrenOfClass($header->toXML());
96
        Assert::count($to, 1, 'Missing To in SOAP Header.');
97
        $to = array_pop($to);
98
99
        $action = Action::getChildrenOfClass($header->toXML());
100
        Assert::count($action, 1, 'Missing Action in SOAP Header.');
101
        $action = array_pop($action);
0 ignored issues
show
The assignment to $action is dead and can be removed.
Loading history...
102
103
        $messageid = MessageID::getChildrenOfClass($header->toXML());
104
        Assert::count($messageid, 1, 'Missing MessageID in SOAP Header.');
105
        $messageid = array_pop($messageid);
106
107
        $security = Security::getChildrenOfClass($header->toXML());
108
        Assert::count($security, 1, 'Missing Security in SOAP Header.');
109
        $security = array_pop($security);
110
111
        // Parse the SOAP-body
112
        $body = $soapEnvelope->getBody();
113
114
        $requestSecurityToken = RequestSecurityToken::getChildrenOfClass($body->toXML());
115
        Assert::count($requestSecurityToken, 1, 'Missing RequestSecurityToken in SOAP Body.');
116
        $requestSecurityToken = array_pop($requestSecurityToken);
117
118
        $appliesTo = AppliesTo::getChildrenOfClass($requestSecurityToken->toXML());
119
        Assert::count($appliesTo, 1, 'Missing AppliesTo in RequestSecurityToken.');
120
        $appliesTo = array_pop($appliesTo);
121
122
        $endpointReference = EndpointReference::getChildrenOfClass($appliesTo->toXML());
123
        Assert::count($endpointReference, 1, 'Missing EndpointReference in AppliesTo.');
124
        $endpointReference = array_pop($endpointReference);
125
126
        // Make sure the message was addressed to us.
127
        if ($to === null || $request->server->get('SCRIPT_URI') !== $to->getContent()) {
128
            throw new Error\BadRequest('This server is not the audience for the message received.');
129
        }
130
131
        // Ensure we know the issuer
132
        $issuer = $endpointReference->getAddress()->getContent();
133
134
        $metadata = MetaDataStorageHandler::getMetadataHandler(Configuration::getInstance());
0 ignored issues
show
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

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

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

396
        $key = PrivateKey::fromFile($key, /** @scrutinizer ignore-type */ $passphrase);
Loading history...
397
        $pubkey = PublicKey::fromFile($cert);
398
        $keyInfo = new KeyInfo([
399
            new X509Data(
400
                [new X509Certificate(
401
                    trim(chunk_split(base64_encode($pubkey->getPEM()->data()))),
402
                )],
403
            ),
404
        ]);
405
406
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
407
            $algo,
408
            $key,
409
        );
410
411
        $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
412
        return $assertion;
413
    }
414
415
416
    /**
417
     * @param string $wreply
418
     * @param string $wresult
419
     * @param ?string $wctx
420
     */
421
    private static function postResponse(string $wreply, string $wresult, ?string $wctx): void
422
    {
423
        $config = Configuration::getInstance();
424
        $t = new Template($config, 'adfs:postResponse.twig');
425
        $t->data['wreply'] = $wreply;
426
        $t->data['wresult'] = $wresult;
427
        $t->data['wctx'] = $wctx;
428
        $t->send();
429
        // Idp->postAuthProc expects this function to exit
430
        exit();
0 ignored issues
show
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...
431
    }
432
433
434
    /**
435
     * Get the metadata of a given hosted ADFS IdP.
436
     *
437
     * @param string $entityid The entity ID of the hosted ADFS IdP whose metadata we want to fetch.
438
     * @param \SimpleSAML\Metadata\MetaDataStorageHandler $handler Optionally the metadata storage to use,
439
     *        if omitted the configured handler will be used.
440
     * @return array
441
     *
442
     * @throws \SimpleSAML\Error\Exception
443
     * @throws \SimpleSAML\Error\MetadataNotFound
444
     */
445
    public static function getHostedMetadata(string $entityid, ?MetaDataStorageHandler $handler = null): array
446
    {
447
        $cryptoUtils = new Utils\Crypto();
448
449
        $globalConfig = Configuration::getInstance();
450
        if ($handler === null) {
451
            $handler = MetaDataStorageHandler::getMetadataHandler($globalConfig);
0 ignored issues
show
The call to SimpleSAML\Metadata\Meta...r::getMetadataHandler() has too many arguments starting with $globalConfig. ( Ignorable by Annotation )

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

451
            /** @scrutinizer ignore-call */ 
452
            $handler = MetaDataStorageHandler::getMetadataHandler($globalConfig);

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...
452
        }
453
        $config = $handler->getMetaDataConfig($entityid, 'adfs-idp-hosted');
454
455
        $host = Module::getModuleURL('adfs/idp/prp.php');
456
457
        // configure endpoints
458
        $ssob = $handler->getGenerated('SingleSignOnServiceBinding', 'adfs-idp-hosted', $host);
459
        $slob = $handler->getGenerated('SingleLogoutServiceBinding', 'adfs-idp-hosted', $host);
460
        $ssol = $handler->getGenerated('SingleSignOnService', 'adfs-idp-hosted', $host);
461
        $slol = $handler->getGenerated('SingleLogoutService', 'adfs-idp-hosted', $host);
462
463
        $sso = [];
464
        if (is_array($ssob)) {
465
            foreach ($ssob as $binding) {
466
                $sso[] = [
467
                    'Binding'  => $binding,
468
                    'Location' => $ssol,
469
                ];
470
            }
471
        } else {
472
            $sso[] = [
473
                'Binding'  => $ssob,
474
                'Location' => $ssol,
475
            ];
476
        }
477
478
        $slo = [];
479
        if (is_array($slob)) {
480
            foreach ($slob as $binding) {
481
                $slo[] = [
482
                    'Binding'  => $binding,
483
                    'Location' => $slol,
484
                ];
485
            }
486
        } else {
487
            $slo[] = [
488
                'Binding'  => $slob,
489
                'Location' => $slol,
490
            ];
491
        }
492
493
494
        $metadata = [
495
            'metadata-set' => 'adfs-idp-hosted',
496
            'entityid' => $entityid,
497
            'SingleSignOnService' => $sso,
498
            'SingleLogoutService' => $slo,
499
            'NameIDFormat' => $config->getOptionalArrayizeString('NameIDFormat', [C::NAMEID_TRANSIENT]),
0 ignored issues
show
The constant SimpleSAML\SAML11\Constants::NAMEID_TRANSIENT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
500
            'contacts' => [],
501
        ];
502
503
        // add certificates
504
        $keys = [];
505
        $certInfo = $cryptoUtils->loadPublicKey($config, false, 'new_');
506
        $hasNewCert = false;
507
        if ($certInfo !== null) {
508
            $keys[] = [
509
                'type' => 'X509Certificate',
510
                'signing' => true,
511
                'encryption' => true,
512
                'X509Certificate' => $certInfo['certData'],
513
                'prefix' => 'new_',
514
            ];
515
            $hasNewCert = true;
516
        }
517
518
        /** @var array $certInfo */
519
        $certInfo = $cryptoUtils->loadPublicKey($config, true);
520
        $keys[] = [
521
            'type' => 'X509Certificate',
522
            'signing' => true,
523
            'encryption' => $hasNewCert === false,
524
            'X509Certificate' => $certInfo['certData'],
525
            'prefix' => '',
526
        ];
527
528
        if ($config->hasValue('https.certificate')) {
529
            /** @var array $httpsCert */
530
            $httpsCert = $cryptoUtils->loadPublicKey($config, true, 'https.');
531
            $keys[] = [
532
                'type' => 'X509Certificate',
533
                'signing' => true,
534
                'encryption' => false,
535
                'X509Certificate' => $httpsCert['certData'],
536
                'prefix' => 'https.',
537
            ];
538
        }
539
        $metadata['keys'] = $keys;
540
541
        // add organization information
542
        if ($config->hasValue('OrganizationName')) {
543
            $metadata['OrganizationName'] = $config->getLocalizedString('OrganizationName');
544
            $metadata['OrganizationDisplayName'] = $config->getOptionalLocalizedString(
545
                'OrganizationDisplayName',
546
                $metadata['OrganizationName'],
547
            );
548
549
            if (!$config->hasValue('OrganizationURL')) {
550
                throw new Error\Exception('If OrganizationName is set, OrganizationURL must also be set.');
551
            }
552
            $metadata['OrganizationURL'] = $config->getLocalizedString('OrganizationURL');
553
        }
554
555
        // add scope
556
        if ($config->hasValue('scope')) {
557
            $metadata['scope'] = $config->getArray('scope');
558
        }
559
560
        // add extensions
561
        if ($config->hasValue('EntityAttributes')) {
562
            $metadata['EntityAttributes'] = $config->getArray('EntityAttributes');
563
564
            // check for entity categories
565
            if (Utils\Config\Metadata::isHiddenFromDiscovery($metadata)) {
566
                $metadata['hide.from.discovery'] = true;
567
            }
568
        }
569
570
        if ($config->hasValue('UIInfo')) {
571
            $metadata['UIInfo'] = $config->getArray('UIInfo');
572
        }
573
574
        if ($config->hasValue('DiscoHints')) {
575
            $metadata['DiscoHints'] = $config->getArray('DiscoHints');
576
        }
577
578
        if ($config->hasValue('RegistrationInfo')) {
579
            $metadata['RegistrationInfo'] = $config->getArray('RegistrationInfo');
580
        }
581
582
        // add contact information
583
        $globalConfig = Configuration::getInstance();
584
        $email = $globalConfig->getOptionalString('technicalcontact_email', null);
585
        if ($email !== null && $email !== '[email protected]') {
586
            $contact = [
587
                'emailAddress' => $email,
588
                'givenName' => $globalConfig->getOptionalString('technicalcontact_name', null),
589
                'contactType' => 'technical',
590
            ];
591
            $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact);
592
        }
593
594
        return $metadata;
595
    }
596
597
598
    /**
599
     * @param array<mixed> $state
600
     * @throws \Exception
601
     */
602
    public static function sendPassiveResponse(array $state): void
603
    {
604
        $idp = IdP::getByState($state);
605
        $idpMetadata = $idp->getConfig();
606
        $idpEntityId = $state['IdPMetadata']['entityid'];
607
608
        $spMetadata = $state['SPMetadata'];
609
        $spEntityId = $spMetadata['entityid'];
610
        $spMetadata = Configuration::loadFromArray(
611
            $spMetadata,
612
            '$metadata[' . var_export($spEntityId, true) . ']',
613
        );
614
615
        $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null);
616
        if ($assertionLifetime === null) {
617
            $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300);
618
        }
619
620
        $now = new DateTimeImmutable('now', new DateTimeZone('Z'));
621
        $created = $now->sub(DateInterval::createFromDateString(sprintf('30 seconds')));
622
        $expires = $now->add(DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime)));
623
624
        $attributes = $state['Attributes'];
625
        $nameid = $state['saml:NameID'][SAML2_C::NAMEID_UNSPECIFIED];
626
627
        $assertion = ADFS::generatePassiveAssertion(
628
            $idpEntityId,
629
            $spEntityId,
630
            $nameid->getValue(),
631
            $attributes,
632
            $assertionLifetime,
0 ignored issues
show
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

632
            /** @scrutinizer ignore-type */ $assertionLifetime,
Loading history...
633
        );
634
635
        $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null);
636
        $certificateCfg = $idpMetadata->getOptionalString('certificate', null);
637
638
        if ($privateKeyCfg !== null && $certificateCfg !== null) {
639
            $configUtils = new Utils\Config();
640
            $privateKeyFile = $configUtils->getCertPath($privateKeyCfg);
641
            $certificateFile = $configUtils->getCertPath($certificateCfg);
642
            $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null);
643
644
            $algo = $spMetadata->getOptionalString('signature.algorithm', null);
645
            if ($algo === null) {
646
                $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
647
            }
648
649
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase);
0 ignored issues
show
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

649
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, /** @scrutinizer ignore-type */ $algo, $passphrase);
Loading history...
650
            $assertion = Assertion::fromXML($assertion->toXML());
651
        }
652
653
        $requestedSecurityToken = new RequestedSecurityToken($assertion);
654
        $lifetime = new LifeTime(new Created($created), new Expires($expires));
655
        $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]);
656
657
        $requestedAttachedReference = new RequestedAttachedReference(
658
            new SecurityTokenReference(null, null, [
659
                new KeyIdentifier(
660
                    $assertion->getId(),
661
                    'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID',
662
                ),
663
            ]),
664
        );
665
        $requestedUnattachedReference = new RequestedUnattachedReference(
666
            new SecurityTokenReference(null, null, [
667
                new KeyIdentifier(
668
                    $assertion->getId(),
669
                    'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID',
670
                ),
671
            ]),
672
        );
673
        $tokenType = new TokenType(C::NS_SAML);
674
        $requestType = new RequestType([RequestTypeEnum::Issue]);
675
        $keyType = new KeyType(['http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey']);
676
677
        $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [
678
            $lifetime,
679
            $appliesTo,
680
            $requestedSecurityToken,
681
            $requestedAttachedReference,
682
            $requestedUnattachedReference,
683
            $tokenType,
684
            $requestType,
685
            $keyType,
686
        ]);
687
688
        // Build envelope
689
        $mustUnderstand = new XMLAttribute(SOAP_C::NS_SOAP_ENV_12, 'env', 'mustUnderstand', '1');
690
        $header = new Header([
691
            new Action('http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue', [$mustUnderstand]),
692
            new RelatesTo($state['MessageID'], null),
693
            new Security(
694
                [
695
                    new Timestamp(
696
                        new Created($created),
697
                        new Expires($expires),
698
                    ),
699
                ],
700
                [$mustUnderstand],
701
            ),
702
        ]);
703
        $body = new Body(null, [$requestSecurityTokenResponse]);
704
        $envelope = new Envelope($body, $header);
705
706
        $xmlResponse = $envelope->toXML();
707
        \SimpleSAML\Logger::debug($xmlResponse->ownerDocument->saveXML($xmlResponse));
0 ignored issues
show
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

707
        \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...
708
709
        echo $xmlResponse->ownerDocument->saveXML($xmlResponse);
710
        exit();
0 ignored issues
show
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...
711
    }
712
713
714
    /**
715
     * @param array<mixed> $state
716
     * @throws \Exception
717
     */
718
    public static function sendResponse(array $state): void
719
    {
720
        $spMetadata = $state['SPMetadata'];
721
        $spEntityId = $spMetadata['entityid'];
722
        $spMetadata = Configuration::loadFromArray(
723
            $spMetadata,
724
            '$metadata[' . var_export($spEntityId, true) . ']',
725
        );
726
727
        $attributes = $state['Attributes'];
728
729
        $nameidattribute = $spMetadata->getValue('simplesaml.nameidattribute');
730
        if (!empty($nameidattribute)) {
731
            if (!array_key_exists($nameidattribute, $attributes)) {
732
                throw new Exception('simplesaml.nameidattribute does not exist in resulting attribute set');
733
            }
734
            $nameid = $attributes[$nameidattribute][0];
735
        } else {
736
            $randomUtils = new Utils\Random();
737
            $nameid = $randomUtils->generateID();
738
        }
739
740
        $idp = IdP::getByState($state);
741
        $idpMetadata = $idp->getConfig();
742
        $idpEntityId = $state['IdPMetadata']['entityid'];
743
744
        $idp->addAssociation([
745
            'id' => 'adfs:' . $spEntityId,
746
            'Handler' => ADFS::class,
747
            'adfs:entityID' => $spEntityId,
748
        ]);
749
750
        $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null);
751
        if ($assertionLifetime === null) {
752
            $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300);
753
        }
754
755
        if (isset($state['saml:AuthnContextClassRef'])) {
756
            $method = $state['saml:AuthnContextClassRef'];
757
        } elseif ((new Utils\HTTP())->isHTTPS()) {
758
            $method = SAML2_C::AC_PASSWORD_PROTECTED_TRANSPORT;
759
        } else {
760
            $method = C::AC_PASSWORD;
761
        }
762
763
        $assertion = ADFS::generateActiveAssertion(
764
            $idpEntityId,
765
            $spEntityId,
766
            $nameid,
767
            $attributes,
768
            $assertionLifetime,
0 ignored issues
show
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

768
            /** @scrutinizer ignore-type */ $assertionLifetime,
Loading history...
769
            $method,
770
        );
771
772
        $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null);
773
        $certificateCfg = $idpMetadata->getOptionalString('certificate', null);
774
775
        if ($privateKeyCfg !== null && $certificateCfg !== null) {
776
            $configUtils = new Utils\Config();
777
            $privateKeyFile = $configUtils->getCertPath($privateKeyCfg);
778
            $certificateFile = $configUtils->getCertPath($certificateCfg);
779
            $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null);
780
781
            $algo = $spMetadata->getOptionalString('signature.algorithm', null);
782
            if ($algo === null) {
783
                $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
784
            }
785
786
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, $algo, $passphrase);
0 ignored issues
show
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

786
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, /** @scrutinizer ignore-type */ $algo, $passphrase);
Loading history...
787
            $assertion = Assertion::fromXML($assertion->toXML());
788
        }
789
790
        $requestedSecurityToken = new RequestedSecurityToken($assertion);
791
        $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]);
792
        $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [$requestedSecurityToken, $appliesTo]);
793
794
        $xmlResponse = $requestSecurityTokenResponse->toXML();
795
        $wresult = $xmlResponse->ownerDocument->saveXML($xmlResponse);
796
        Logger::debug($wresult);
797
798
        $wctx = $state['adfs:wctx'];
799
        $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp');
800
        ADFS::postResponse($wreply, $wresult, $wctx);
801
    }
802
803
804
    /**
805
     * @param \SimpleSAML\IdP $idp
806
     * @param array<mixed> $state
807
     */
808
    public static function sendLogoutResponse(IdP $idp, array $state): void
0 ignored issues
show
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

808
    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...
809
    {
810
        // NB:: we don't know from which SP the logout request came from
811
        $idpMetadata = $idp->getConfig();
812
        $httpUtils = new Utils\HTTP();
813
        $httpUtils->redirectTrustedURL(
814
            $idpMetadata->getOptionalString('redirect-after-logout', $httpUtils->getBaseURL()),
0 ignored issues
show
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

814
            /** @scrutinizer ignore-type */ $idpMetadata->getOptionalString('redirect-after-logout', $httpUtils->getBaseURL()),
Loading history...
815
        );
816
    }
817
818
819
    /**
820
     * @param \SimpleSAML\IdP $idp
821
     * @throws \Exception
822
     */
823
    public static function receiveLogoutMessage(IdP $idp): void
824
    {
825
        // if a redirect is to occur based on wreply, we will redirect to url as
826
        // this implies an override to normal sp notification
827
        if (isset($_GET['wreply']) && !empty($_GET['wreply'])) {
828
            $httpUtils = new Utils\HTTP();
829
            $idp->doLogoutRedirect($httpUtils->checkURLAllowed($_GET['wreply']));
830
            throw new Exception("Code should never be reached");
831
        }
832
833
        $state = [
834
            'Responder' => [ADFS::class, 'sendLogoutResponse'],
835
        ];
836
        $assocId = null;
837
        // TODO: verify that this is really no problem for:
838
        //       a) SSP, because there's no caller SP.
839
        //       b) ADFS SP because caller will be called back..
840
        $idp->handleLogoutRequest($state, $assocId);
841
    }
842
843
844
    /**
845
     * accepts an association array, and returns a URL that can be accessed to terminate the association
846
     *
847
     * @param \SimpleSAML\IdP $idp
848
     * @param array<mixed> $association
849
     * @param string|null $relayState
850
     * @return string
851
     */
852
    public static function getLogoutURL(IdP $idp, array $association, ?string $relayState = null): string
853
    {
854
        $metadata = MetaDataStorageHandler::getMetadataHandler();
855
        $spMetadata = $metadata->getMetaDataConfig($association['adfs:entityID'], 'adfs-sp-remote');
856
        $params = ['assocId' => urlencode($association['id'])];
857
        if ($relayState !== null) {
858
            $params['relayState'] = urlencode($relayState);
859
        }
860
        $returnTo = Module::getModuleURL(
861
            'adfs/idp/prp.php',
862
            $params,
863
        );
864
        return $spMetadata->getValue('prp') . '?wa=wsignoutcleanup1.0&wreply=' . urlencode($returnTo);
865
    }
866
}
867