ADFS::postResponse()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 3
dl 0
loc 10
rs 10
c 0
b 0
f 0
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\SAML2\Constants as SAML2_C;
19
use SimpleSAML\SAML11\Constants as C;
20
use SimpleSAML\SAML11\XML\saml\Assertion;
21
use SimpleSAML\SAML11\XML\saml\Attribute;
22
use SimpleSAML\SAML11\XML\saml\AttributeStatement;
23
use SimpleSAML\SAML11\XML\saml\AttributeValue;
24
use SimpleSAML\SAML11\XML\saml\Audience;
25
use SimpleSAML\SAML11\XML\saml\AudienceRestrictionCondition;
26
use SimpleSAML\SAML11\XML\saml\AuthenticationStatement;
27
use SimpleSAML\SAML11\XML\saml\Conditions;
28
use SimpleSAML\SAML11\XML\saml\NameIdentifier;
29
use SimpleSAML\SAML11\XML\saml\{ConfirmationMethod, Subject, SubjectConfirmation};
30
use SimpleSAML\SOAP\Constants as SOAP_C;
31
use SimpleSAML\SOAP\XML\env_200305\{Body, Envelope, Header};
32
use SimpleSAML\Utils;
33
use SimpleSAML\WSSecurity\XML\wsa_200508\{Action, Address, EndpointReference, MessageID, RelatesTo, To};
34
use SimpleSAML\WSSecurity\XML\wsp\AppliesTo;
35
use SimpleSAML\WSSecurity\XML\wsse\{KeyIdentifier, Password, Security, SecurityTokenReference, Username, UsernameToken};
36
use SimpleSAML\WSSecurity\XML\wst_200502\{KeyType, KeyTypeEnum, Lifetime, RequestType, RequestTypeEnum, TokenType};
37
use SimpleSAML\WSSecurity\XML\wst_200502\{RequestedSecurityToken, RequestSecurityToken, RequestSecurityTokenResponse};
38
use SimpleSAML\WSSecurity\XML\wst_200502\{RequestedAttachedReference, RequestedUnattachedReference};
39
use SimpleSAML\WSSecurity\XML\wsu\{Created, Expires, Timestamp};
40
use SimpleSAML\XHTML\Template;
41
use SimpleSAML\XML\Attribute as XMLAttribute;
42
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
43
use SimpleSAML\XMLSecurity\Key\PrivateKey;
44
use SimpleSAML\XMLSecurity\Key\X509Certificate as PublicKey;
45
use SimpleSAML\XMLSecurity\XML\ds\KeyInfo;
46
use SimpleSAML\XMLSecurity\XML\ds\X509Certificate;
47
use SimpleSAML\XMLSecurity\XML\ds\X509Data;
48
use Symfony\Component\HttpFoundation\{Request, StreamedResponse};
49
50
use function array_pop;
51
use function base64_encode;
52
use function chunk_split;
53
use function str_replace;
54
use function trim;
55
56
class ADFS
57
{
58
    /**
59
     * @param \Symfony\Component\HttpFoundation\Request $request
60
     * @param \SimpleSAML\SOAP\XML\env_200305\Envelope $soapEnvelope
61
     * @param \SimpleSAML\Module\adfs\IdP\PassiveIdP $idp
62
     * @throws \SimpleSAML\Error\MetadataNotFound
63
     */
64
    public static function receivePassiveAuthnRequest(
65
        Request $request,
66
        Envelope $soapEnvelope,
67
        PassiveIdP $idp,
68
    ): StreamedResponse {
69
        // Parse the SOAP-header
70
        $header = $soapEnvelope->getHeader();
71
72
        $to = To::getChildrenOfClass($header->toXML());
73
        Assert::count($to, 1, 'Missing To in SOAP Header.');
74
        $to = array_pop($to);
75
76
        $action = Action::getChildrenOfClass($header->toXML());
77
        Assert::count($action, 1, 'Missing Action in SOAP Header.');
78
        $action = array_pop($action);
0 ignored issues
show
Unused Code introduced by
The assignment to $action is dead and can be removed.
Loading history...
79
80
        $messageid = MessageID::getChildrenOfClass($header->toXML());
81
        Assert::count($messageid, 1, 'Missing MessageID in SOAP Header.');
82
        $messageid = array_pop($messageid);
83
84
        $security = Security::getChildrenOfClass($header->toXML());
85
        Assert::count($security, 1, 'Missing Security in SOAP Header.');
86
        $security = array_pop($security);
87
88
        // Parse the SOAP-body
89
        $body = $soapEnvelope->getBody();
90
91
        $requestSecurityToken = RequestSecurityToken::getChildrenOfClass($body->toXML());
92
        Assert::count($requestSecurityToken, 1, 'Missing RequestSecurityToken in SOAP Body.');
93
        $requestSecurityToken = array_pop($requestSecurityToken);
94
95
        $appliesTo = AppliesTo::getChildrenOfClass($requestSecurityToken->toXML());
96
        Assert::count($appliesTo, 1, 'Missing AppliesTo in RequestSecurityToken.');
97
        $appliesTo = array_pop($appliesTo);
98
99
        $endpointReference = EndpointReference::getChildrenOfClass($appliesTo->toXML());
100
        Assert::count($endpointReference, 1, 'Missing EndpointReference in AppliesTo.');
101
        $endpointReference = array_pop($endpointReference);
102
103
        // Make sure the message was addressed to us.
104
        if ($to === null || $request->server->get('SCRIPT_URI') !== $to->getContent()) {
105
            throw new Error\BadRequest('This server is not the audience for the message received.');
106
        }
107
108
        // Ensure we know the issuer
109
        $issuer = $endpointReference->getAddress()->getContent();
110
111
        $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

111
        /** @scrutinizer ignore-call */ 
112
        $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...
112
        $spMetadata = $metadata->getMetaDataConfig($issuer, 'adfs-sp-remote');
113
114
        $usernameToken = UsernameToken::getChildrenOfClass($security->toXML());
115
        Assert::count($usernameToken, 1, 'Missing UsernameToken in Security.');
116
        $usernameToken = array_pop($usernameToken);
117
118
        $username = $usernameToken->getUsername();
119
        $password = Password::getChildrenOfClass($usernameToken->toXML());
120
        $password = array_pop($password);
121
122
        if ($username === null || $password === null) {
123
            throw new Error\BadRequest('Missing username or password in SOAP header.');
124
        } else {
125
            $_SERVER['PHP_AUTH_USER'] = $username->getContent();
126
            $_SERVER['PHP_AUTH_PW'] = $password->getContent();
127
        }
128
129
        $requestSecurityTokenStr = $requestSecurityToken->toXML()->ownerDocument->saveXML();
130
        $requestSecurityTokenStr = str_replace($password->getContent(), '*****', $requestSecurityTokenStr);
131
        Logger::debug($requestSecurityTokenStr);
132
133
        $state = [
134
            'Responder' => [ADFS::class, 'sendPassiveResponse'],
135
            'SPMetadata' => $spMetadata->toArray(),
136
            'MessageID' => $messageid->getContent(),
137
            // Dirty hack to leverage the SAML ECP logics
138
            'saml:Binding' => SAML2_C::BINDING_PAOS,
139
        ];
140
141
        return new StreamedResponse(
142
            function () use ($idp, &$state) {
143
                $idp->handleAuthenticationRequest($state);
144
            },
145
        );
146
    }
147
148
149
    /**
150
     * @param \Symfony\Component\HttpFoundation\Request $request
151
     * @param \SimpleSAML\IdP $idp
152
     * @throws \SimpleSAML\Error\MetadataNotFound
153
     */
154
    public static function receiveAuthnRequest(Request $request, IdP $idp): StreamedResponse
155
    {
156
        parse_str($request->server->get('QUERY_STRING'), $query);
157
158
        $requestid = $query['wctx'] ?? null;
159
        $issuer = $query['wtrealm'];
160
161
        $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

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

373
        $key = PrivateKey::fromFile($key, /** @scrutinizer ignore-type */ $passphrase);
Loading history...
374
        $pubkey = PublicKey::fromFile($cert);
375
        $keyInfo = new KeyInfo([
376
            new X509Data(
377
                [new X509Certificate(
378
                    trim(chunk_split(base64_encode($pubkey->getPEM()->data()))),
379
                )],
380
            ),
381
        ]);
382
383
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
384
            $algo,
385
            $key,
386
        );
387
388
        $assertion->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
389
        return $assertion;
390
    }
391
392
393
    /**
394
     * @param string $wreply
395
     * @param string $wresult
396
     * @param ?string $wctx
397
     */
398
    private static function postResponse(string $wreply, string $wresult, ?string $wctx): void
399
    {
400
        $config = Configuration::getInstance();
401
        $t = new Template($config, 'adfs:postResponse.twig');
402
        $t->data['wreply'] = $wreply;
403
        $t->data['wresult'] = $wresult;
404
        $t->data['wctx'] = $wctx;
405
        $t->send();
406
        // Idp->postAuthProc expects this function to exit
407
        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...
408
    }
409
410
411
    /**
412
     * @param array $state
413
     * @throws \Exception
414
     */
415
    public static function sendPassiveResponse(array $state): void
416
    {
417
        $idp = IdP::getByState($state);
418
        $idpMetadata = $idp->getConfig();
419
        $idpEntityId = $idpMetadata->getString('entityid');
420
421
        $spMetadata = $state['SPMetadata'];
422
        $spEntityId = $spMetadata['entityid'];
423
        $spMetadata = Configuration::loadFromArray(
424
            $spMetadata,
425
            '$metadata[' . var_export($spEntityId, true) . ']',
426
        );
427
428
        $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null);
429
        if ($assertionLifetime === null) {
430
            $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300);
431
        }
432
433
        $now = new DateTimeImmutable('now', new DateTimeZone('Z'));
434
        $created = $now->sub(DateInterval::createFromDateString(sprintf('30 seconds')));
435
        $expires = $now->add(DateInterval::createFromDateString(sprintf('%d seconds', $assertionLifetime)));
436
437
        $attributes = $state['Attributes'];
438
        $nameid = $state['saml:NameID'][SAML2_C::NAMEID_UNSPECIFIED];
439
440
        $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

440
        $assertion = ADFS::generatePassiveAssertion($idpEntityId, $spEntityId, $nameid->getValue(), $attributes, /** @scrutinizer ignore-type */ $assertionLifetime);
Loading history...
441
442
        $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null);
443
        $certificateCfg = $idpMetadata->getOptionalString('certificate', null);
444
445
        if ($privateKeyCfg !== null && $certificateCfg !== null) {
446
            $configUtils = new Utils\Config();
447
            $privateKeyFile = $configUtils->getCertPath($privateKeyCfg);
448
            $certificateFile = $configUtils->getCertPath($certificateCfg);
449
            $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null);
450
451
            $algo = $spMetadata->getOptionalString('signature.algorithm', null);
452
            if ($algo === null) {
453
                $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
454
            }
455
456
            $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

456
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, /** @scrutinizer ignore-type */ $algo, $passphrase);
Loading history...
457
            $assertion = Assertion::fromXML($assertion->toXML());
458
        }
459
460
        $requestedSecurityToken = new RequestedSecurityToken($assertion);
461
        $lifetime = new LifeTime(new Created($created), new Expires($expires));
462
        $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]);
463
464
        $requestedAttachedReference = new RequestedAttachedReference(
465
            new SecurityTokenReference(null, null, [
466
                new KeyIdentifier(
467
                    $assertion->getId(),
468
                    'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID',
469
                ),
470
            ]),
471
        );
472
        $requestedUnattachedReference = new RequestedUnattachedReference(
473
            new SecurityTokenReference(null, null, [
474
                new KeyIdentifier(
475
                    $assertion->getId(),
476
                    'http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID',
477
                ),
478
            ]),
479
        );
480
        $tokenType = new TokenType(C::NS_SAML);
481
        $requestType = new RequestType([RequestTypeEnum::Issue]);
482
        $keyType = new KeyType(['http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey']);
483
484
        $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [
485
            $lifetime,
486
            $appliesTo,
487
            $requestedSecurityToken,
488
            $requestedAttachedReference,
489
            $requestedUnattachedReference,
490
            $tokenType,
491
            $requestType,
492
            $keyType,
493
        ]);
494
495
        // Build envelope
496
        $mustUnderstand = new XMLAttribute(SOAP_C::NS_SOAP_ENV_12, 'env', 'mustUnderstand', '1');
497
        $header = new Header([
498
            new Action('http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue', [$mustUnderstand]),
499
            new RelatesTo($state['MessageID'], null),
500
            new Security(
501
                [
502
                    new Timestamp(
503
                        new Created($created),
504
                        new Expires($expires),
505
                    ),
506
                ],
507
                [$mustUnderstand],
508
            ),
509
        ]);
510
        $body = new Body(null, [$requestSecurityTokenResponse]);
511
        $envelope = new Envelope($body, $header);
512
513
        $xmlResponse = $envelope->toXML();
514
        \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

514
        \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...
515
516
        echo $xmlResponse->ownerDocument->saveXML($xmlResponse);
517
        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...
518
    }
519
520
521
    /**
522
     * @param array $state
523
     * @throws \Exception
524
     */
525
    public static function sendResponse(array $state): void
526
    {
527
        $spMetadata = $state['SPMetadata'];
528
        $spEntityId = $spMetadata['entityid'];
529
        $spMetadata = Configuration::loadFromArray(
530
            $spMetadata,
531
            '$metadata[' . var_export($spEntityId, true) . ']',
532
        );
533
534
        $attributes = $state['Attributes'];
535
536
        $nameidattribute = $spMetadata->getValue('simplesaml.nameidattribute');
537
        if (!empty($nameidattribute)) {
538
            if (!array_key_exists($nameidattribute, $attributes)) {
539
                throw new Exception('simplesaml.nameidattribute does not exist in resulting attribute set');
540
            }
541
            $nameid = $attributes[$nameidattribute][0];
542
        } else {
543
            $randomUtils = new Utils\Random();
544
            $nameid = $randomUtils->generateID();
545
        }
546
547
        $idp = IdP::getByState($state);
548
        $idpMetadata = $idp->getConfig();
549
        $idpEntityId = $idpMetadata->getString('entityid');
550
551
        $idp->addAssociation([
552
            'id' => 'adfs:' . $spEntityId,
553
            'Handler' => ADFS::class,
554
            'adfs:entityID' => $spEntityId,
555
        ]);
556
557
        $assertionLifetime = $spMetadata->getOptionalInteger('assertion.lifetime', null);
558
        if ($assertionLifetime === null) {
559
            $assertionLifetime = $idpMetadata->getOptionalInteger('assertion.lifetime', 300);
560
        }
561
562
        if (isset($state['saml:AuthnContextClassRef'])) {
563
            $method = $state['saml:AuthnContextClassRef'];
564
        } elseif ((new Utils\HTTP())->isHTTPS()) {
565
            $method = SAML2_C::AC_PASSWORD_PROTECTED_TRANSPORT;
566
        } else {
567
            $method = C::AC_PASSWORD;
568
        }
569
570
        $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

570
        $assertion = ADFS::generateActiveAssertion($idpEntityId, $spEntityId, $nameid, $attributes, /** @scrutinizer ignore-type */ $assertionLifetime, $method);
Loading history...
571
572
        $privateKeyCfg = $idpMetadata->getOptionalString('privatekey', null);
573
        $certificateCfg = $idpMetadata->getOptionalString('certificate', null);
574
575
        if ($privateKeyCfg !== null && $certificateCfg !== null) {
576
            $configUtils = new Utils\Config();
577
            $privateKeyFile = $configUtils->getCertPath($privateKeyCfg);
578
            $certificateFile = $configUtils->getCertPath($certificateCfg);
579
            $passphrase = $idpMetadata->getOptionalString('privatekey_pass', null);
580
581
            $algo = $spMetadata->getOptionalString('signature.algorithm', null);
582
            if ($algo === null) {
583
                $algo = $idpMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
584
            }
585
586
            $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

586
            $assertion = ADFS::signAssertion($assertion, $privateKeyFile, $certificateFile, /** @scrutinizer ignore-type */ $algo, $passphrase);
Loading history...
587
            $assertion = Assertion::fromXML($assertion->toXML());
588
        }
589
590
        $requestSecurityToken = new RequestSecurityToken(null, [$assertion]);
591
        $appliesTo = new AppliesTo([new EndpointReference(new Address($spEntityId))]);
592
        $requestSecurityTokenResponse = new RequestSecurityTokenResponse(null, [$requestSecurityToken, $appliesTo]);
593
594
        $xmlResponse = $requestSecurityTokenResponse->toXML();
595
        $wresult = $xmlResponse->ownerDocument->saveXML($xmlResponse);
596
        Logger::debug($wresult);
597
598
        $wctx = $state['adfs:wctx'];
599
        $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp');
600
        ADFS::postResponse($wreply, $wresult, $wctx);
601
    }
602
603
604
    /**
605
     * @param \SimpleSAML\IdP $idp
606
     * @param array $state
607
     */
608
    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

608
    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...
609
    {
610
        // NB:: we don't know from which SP the logout request came from
611
        $idpMetadata = $idp->getConfig();
612
        $httpUtils = new Utils\HTTP();
613
        $httpUtils->redirectTrustedURL(
614
            $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

614
            /** @scrutinizer ignore-type */ $idpMetadata->getOptionalString('redirect-after-logout', $httpUtils->getBaseURL()),
Loading history...
615
        );
616
    }
617
618
619
    /**
620
     * @param \SimpleSAML\IdP $idp
621
     * @throws \Exception
622
     */
623
    public static function receiveLogoutMessage(IdP $idp): void
624
    {
625
        // if a redirect is to occur based on wreply, we will redirect to url as
626
        // this implies an override to normal sp notification
627
        if (isset($_GET['wreply']) && !empty($_GET['wreply'])) {
628
            $httpUtils = new Utils\HTTP();
629
            $idp->doLogoutRedirect($httpUtils->checkURLAllowed($_GET['wreply']));
630
            throw new Exception("Code should never be reached");
631
        }
632
633
        $state = [
634
            'Responder' => [ADFS::class, 'sendLogoutResponse'],
635
        ];
636
        $assocId = null;
637
        // TODO: verify that this is really no problem for:
638
        //       a) SSP, because there's no caller SP.
639
        //       b) ADFS SP because caller will be called back..
640
        $idp->handleLogoutRequest($state, $assocId);
641
    }
642
643
644
    /**
645
     * accepts an association array, and returns a URL that can be accessed to terminate the association
646
     *
647
     * @param \SimpleSAML\IdP $idp
648
     * @param array $association
649
     * @param string $relayState
650
     * @return string
651
     */
652
    public static function getLogoutURL(IdP $idp, array $association, string $relayState): string
653
    {
654
        $metadata = MetaDataStorageHandler::getMetadataHandler();
655
        $spMetadata = $metadata->getMetaDataConfig($association['adfs:entityID'], 'adfs-sp-remote');
656
        $returnTo = Module::getModuleURL(
657
            'adfs/idp/prp.php?assocId=' . urlencode($association["id"]) . '&relayState=' . urlencode($relayState),
658
        );
659
        return $spMetadata->getValue('prp') . '?wa=wsignoutcleanup1.0&wreply=' . urlencode($returnTo);
660
    }
661
}
662