Passed
Push — master ( ec35f9...211094 )
by Tim
04:25 queued 01:46
created

ADFS::receiveAuthnRequest()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 19
c 2
b 0
f 0
dl 0
loc 30
rs 9.6333
cc 4
nc 8
nop 1
1
<?php
2
3
namespace SimpleSAML\Module\adfs\IdP;
4
5
use RobRichards\XMLSecLibs\XMLSecurityDSig;
6
use RobRichards\XMLSecLibs\XMLSecurityKey;
7
use SAML2\Constants;
8
use SAML2\DOMDocumentFactory;
9
use SimpleSAML\Configuration;
10
use SimpleSAML\Error;
11
use SimpleSAML\Logger;
12
use SimpleSAML\Metadata\MetaDataStorageHandler;
13
use SimpleSAML\Module;
14
use SimpleSAML\Utils;
15
use SimpleSAML\XHTML\Template;
16
17
class ADFS
18
{
19
    /**
20
     * @param \SimpleSAML\IdP $idp
21
     * @return void
22
     * @throws \SimpleSAML\Error\Error
23
     */
24
    public static function receiveAuthnRequest(IdP $idp): void
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\adfs\IdP\IdP 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...
25
    {
26
        try {
27
            parse_str($_SERVER['QUERY_STRING'], $query);
28
29
            $requestid = $query['wctx'];
30
            $issuer = $query['wtrealm'];
31
32
            $metadata = MetaDataStorageHandler::getMetadataHandler();
33
            $spMetadata = $metadata->getMetaDataConfig($issuer, 'adfs-sp-remote');
34
35
            Logger::info('ADFS - IdP.prp: Incoming Authentication request: ' . $issuer . ' id ' . $requestid);
36
        } catch (\Exception $exception) {
37
            throw new Error\Error('PROCESSAUTHNREQUEST', $exception);
38
        }
39
40
        $state = [
41
            'Responder' => [ADFS::class, 'sendResponse'],
42
            'SPMetadata' => $spMetadata->toArray(),
43
            'ForceAuthn' => false,
44
            'isPassive' => false,
45
            'adfs:wctx' => $requestid,
46
            'adfs:wreply' => false
47
        ];
48
49
        if (isset($query['wreply']) && !empty($query['wreply'])) {
50
            $state['adfs:wreply'] = Utils\HTTP::checkURLAllowed($query['wreply']);
51
        }
52
53
        $idp->handleAuthenticationRequest($state);
54
    }
55
56
57
    /**
58
     * @param string $issuer
59
     * @param string $target
60
     * @param string $nameid
61
     * @param array $attributes
62
     * @param int $assertionLifetime
63
     * @return string
64
     */
65
    private static function generateResponse(
66
        string $issuer,
67
        string $target,
68
        string $nameid,
69
        array $attributes,
70
        int $assertionLifetime
71
    ): string {
72
        $issueInstant = Utils\Time::generateTimestamp();
73
        $notBefore = Utils\Time::generateTimestamp(time() - 30);
74
        $assertionExpire = Utils\Time::generateTimestamp(time() + $assertionLifetime);
75
        $assertionID = Utils\Random::generateID();
76
        $nameidFormat = 'http://schemas.xmlsoap.org/claims/UPN';
77
        $nameid = htmlspecialchars($nameid);
78
79
        if (Utils\HTTP::isHTTPS()) {
80
            $method = Constants::AC_PASSWORD_PROTECTED_TRANSPORT;
81
        } else {
82
            $method = Constants::AC_PASSWORD;
83
        }
84
85
        $result = <<<MSG
86
<wst:RequestSecurityTokenResponse xmlns:wst="http://schemas.xmlsoap.org/ws/2005/02/trust">
87
    <wst:RequestedSecurityToken>
88
        <saml:Assertion Issuer="$issuer" IssueInstant="$issueInstant" AssertionID="$assertionID" MinorVersion="1" MajorVersion="1" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
89
            <saml:Conditions NotOnOrAfter="$assertionExpire" NotBefore="$notBefore">
90
                <saml:AudienceRestrictionCondition>
91
                    <saml:Audience>$target</saml:Audience>
92
                </saml:AudienceRestrictionCondition>
93
            </saml:Conditions>
94
            <saml:AuthenticationStatement AuthenticationMethod="$method" AuthenticationInstant="$issueInstant">
95
                <saml:Subject>
96
                    <saml:NameIdentifier Format="$nameidFormat">$nameid</saml:NameIdentifier>
97
                </saml:Subject>
98
            </saml:AuthenticationStatement>
99
            <saml:AttributeStatement>
100
                <saml:Subject>
101
                    <saml:NameIdentifier Format="$nameidFormat">$nameid</saml:NameIdentifier>
102
                </saml:Subject>
103
MSG;
104
105
        foreach ($attributes as $name => $values) {
106
            if ((!is_array($values)) || (count($values) == 0)) {
107
                continue;
108
            }
109
110
            list($namespace, $name) = Utils\Attributes::getAttributeNamespace(
111
                $name,
112
                'http://schemas.xmlsoap.org/claims'
113
            );
114
            foreach ($values as $value) {
115
                if ((!isset($value)) || ($value === '')) {
116
                    continue;
117
                }
118
                $value = htmlspecialchars($value);
119
120
                $result .= <<<MSG
121
                <saml:Attribute AttributeNamespace="$namespace" AttributeName="$name">
122
                    <saml:AttributeValue>$value</saml:AttributeValue>
123
                </saml:Attribute>
124
MSG;
125
            }
126
        }
127
128
        $result .= <<<MSG
129
            </saml:AttributeStatement>
130
        </saml:Assertion>
131
   </wst:RequestedSecurityToken>
132
   <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
133
       <wsa:EndpointReference xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing">
134
           <wsa:Address>$target</wsa:Address>
135
       </wsa:EndpointReference>
136
   </wsp:AppliesTo>
137
</wst:RequestSecurityTokenResponse>
138
MSG;
139
140
        return $result;
141
    }
142
143
144
    /**
145
     * @param string $response
146
     * @param string $key
147
     * @param string $cert
148
     * @param string $algo
149
     * @param string|null $passphrase
150
     * @return string
151
     */
152
    private static function signResponse(
153
        string $response,
154
        string $key,
155
        string $cert,
156
        string $algo,
157
        string $passphrase = null
158
    ): string {
159
        $objXMLSecDSig = new XMLSecurityDSig();
160
        $objXMLSecDSig->idKeys = ['AssertionID'];
161
        $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
162
        $responsedom = DOMDocumentFactory::fromString(str_replace("\r", "", $response));
163
        $firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0);
164
165
        if (is_null($firstassertionroot)) {
166
            throw new \Exception("No assertion found in response.");
167
        }
168
169
        $objXMLSecDSig->addReferenceList(
170
            [$firstassertionroot],
171
            XMLSecurityDSig::SHA256,
172
            ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N],
173
            ['id_name' => 'AssertionID']
174
        );
175
176
        $objKey = new XMLSecurityKey($algo, ['type' => 'private']);
177
        if (is_string($passphrase)) {
178
            $objKey->passphrase = $passphrase;
179
        }
180
        $objKey->loadKey($key, true);
181
        $objXMLSecDSig->sign($objKey);
182
        if ($cert) {
183
            $public_cert = file_get_contents($cert);
184
            $objXMLSecDSig->add509Cert($public_cert, true);
185
        }
186
187
        /** @var \DOMElement $objXMLSecDSig->sigNode */
188
        $newSig = $responsedom->importNode($objXMLSecDSig->sigNode, true);
0 ignored issues
show
Bug introduced by
It seems like $objXMLSecDSig->sigNode can also be of type null; however, parameter $importedNode of DOMDocument::importNode() does only seem to accept DOMNode, 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

188
        $newSig = $responsedom->importNode(/** @scrutinizer ignore-type */ $objXMLSecDSig->sigNode, true);
Loading history...
189
        $firstassertionroot->appendChild($newSig);
190
        return $responsedom->saveXML();
191
    }
192
193
194
    /**
195
     * @param string $url
196
     * @param string $wresult
197
     * @param string $wctx
198
     * @return void
199
     */
200
    private static function postResponse(string $url, string $wresult, string $wctx): void
201
    {
202
        $config = Configuration::getInstance();
203
        $t = new Template($config, 'adfs:postResponse.twig');
204
        $t->data['baseurlpath'] = Module::getModuleURL('adfs');
205
        $t->data['url'] = $url;
206
        $t->data['wresult'] = $wresult;
207
        $t->data['wctx'] = $wctx;
208
        $t->show();
0 ignored issues
show
Bug introduced by
The method show() does not exist on SimpleSAML\XHTML\Template. ( Ignorable by Annotation )

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

208
        $t->/** @scrutinizer ignore-call */ 
209
            show();

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...
209
    }
210
211
212
    /**
213
     * Get the metadata of a given hosted ADFS IdP.
214
     *
215
     * @param string $entityid The entity ID of the hosted ADFS IdP whose metadata we want to fetch.
216
     *
217
     * @return array
218
     * @throws \SimpleSAML\Error\Exception
219
     * @throws \SimpleSAML\Error\MetadataNotFound
220
     */
221
    public static function getHostedMetadata(string $entityid): array
222
    {
223
        $handler = MetaDataStorageHandler::getMetadataHandler();
224
        $config = $handler->getMetaDataConfig($entityid, 'adfs-idp-hosted');
225
226
        $endpoint = Module::getModuleURL('adfs/idp/prp.php');
227
        $metadata = [
228
            'metadata-set' => 'adfs-idp-hosted',
229
            'entityid' => $entityid,
230
            'SingleSignOnService' => [
231
                [
232
                    'Binding' => Constants::BINDING_HTTP_REDIRECT,
233
                    'Location' => $endpoint,
234
                ]
235
            ],
236
            'SingleLogoutService' => [
237
                'Binding' => Constants::BINDING_HTTP_REDIRECT,
238
                'Location' => $endpoint,
239
            ],
240
            'NameIDFormat' => $config->getString('NameIDFormat', Constants::NAMEID_TRANSIENT),
241
            'contacts' => [],
242
        ];
243
244
        // add certificates
245
        $keys = [];
246
        $certInfo = Utils\Crypto::loadPublicKey($config, false, 'new_');
247
        $hasNewCert = false;
248
        if ($certInfo !== null) {
249
            $keys[] = [
250
                'type' => 'X509Certificate',
251
                'signing' => true,
252
                'encryption' => true,
253
                'X509Certificate' => $certInfo['certData'],
254
                'prefix' => 'new_',
255
            ];
256
            $hasNewCert = true;
257
        }
258
259
        /** @var array $certInfo */
260
        $certInfo = Utils\Crypto::loadPublicKey($config, true);
261
        $keys[] = [
262
            'type' => 'X509Certificate',
263
            'signing' => true,
264
            'encryption' => $hasNewCert === false,
265
            'X509Certificate' => $certInfo['certData'],
266
            'prefix' => '',
267
        ];
268
269
        if ($config->hasValue('https.certificate')) {
270
            /** @var array $httpsCert */
271
            $httpsCert = Utils\Crypto::loadPublicKey($config, true, 'https.');
272
            $keys[] = [
273
                'type' => 'X509Certificate',
274
                'signing' => true,
275
                'encryption' => false,
276
                'X509Certificate' => $httpsCert['certData'],
277
                'prefix' => 'https.'
278
            ];
279
        }
280
        $metadata['keys'] = $keys;
281
282
        // add organization information
283
        if ($config->hasValue('OrganizationName')) {
284
            $metadata['OrganizationName'] = $config->getLocalizedString('OrganizationName');
285
            $metadata['OrganizationDisplayName'] = $config->getLocalizedString(
286
                'OrganizationDisplayName',
287
                $metadata['OrganizationName']
288
            );
289
290
            if (!$config->hasValue('OrganizationURL')) {
291
                throw new Error\Exception('If OrganizationName is set, OrganizationURL must also be set.');
292
            }
293
            $metadata['OrganizationURL'] = $config->getLocalizedString('OrganizationURL');
294
        }
295
296
        // add scope
297
        if ($config->hasValue('scope')) {
298
            $metadata['scope'] = $config->getArray('scope');
299
        }
300
301
        // add extensions
302
        if ($config->hasValue('EntityAttributes')) {
303
            $metadata['EntityAttributes'] = $config->getArray('EntityAttributes');
304
305
            // check for entity categories
306
            if (Utils\Config\Metadata::isHiddenFromDiscovery($metadata)) {
307
                $metadata['hide.from.discovery'] = true;
308
            }
309
        }
310
311
        if ($config->hasValue('UIInfo')) {
312
            $metadata['UIInfo'] = $config->getArray('UIInfo');
313
        }
314
315
        if ($config->hasValue('DiscoHints')) {
316
            $metadata['DiscoHints'] = $config->getArray('DiscoHints');
317
        }
318
319
        if ($config->hasValue('RegistrationInfo')) {
320
            $metadata['RegistrationInfo'] = $config->getArray('RegistrationInfo');
321
        }
322
323
        // add contact information
324
        $globalConfig = Configuration::getInstance();
325
        $email = $globalConfig->getString('technicalcontact_email', false);
326
        if ($email && $email !== '[email protected]') {
327
            $contact = [
328
                'emailAddress' => $email,
329
                'name' => $globalConfig->getString('technicalcontact_name', null),
330
                'contactType' => 'technical',
331
            ];
332
            $metadata['contacts'][] = Utils\Config\Metadata::getContact($contact);
333
        }
334
335
        return $metadata;
336
    }
337
338
339
    /**
340
     * @param array $state
341
     * @throws \Exception
342
     * @return void
343
     */
344
    public static function sendResponse(array $state): void
345
    {
346
        $spMetadata = $state["SPMetadata"];
347
        $spEntityId = $spMetadata['entityid'];
348
        $spMetadata = Configuration::loadFromArray(
349
            $spMetadata,
350
            '$metadata[' . var_export($spEntityId, true) . ']'
351
        );
352
353
        $attributes = $state['Attributes'];
354
355
        $nameidattribute = $spMetadata->getValue('simplesaml.nameidattribute');
356
        if (!empty($nameidattribute)) {
357
            if (!array_key_exists($nameidattribute, $attributes)) {
358
                throw new \Exception('simplesaml.nameidattribute does not exist in resulting attribute set');
359
            }
360
            $nameid = $attributes[$nameidattribute][0];
361
        } else {
362
            $nameid = Utils\Random::generateID();
363
        }
364
365
        $idp = IdP::getByState($state);
366
        $idpMetadata = $idp->getConfig();
367
        $idpEntityId = $idpMetadata->getString('entityid');
368
369
        $idp->addAssociation([
370
            'id' => 'adfs:' . $spEntityId,
371
            'Handler' => ADFS::class,
372
            'adfs:entityID' => $spEntityId,
373
        ]);
374
375
        $assertionLifetime = $spMetadata->getInteger('assertion.lifetime', null);
376
        if ($assertionLifetime === null) {
377
            $assertionLifetime = $idpMetadata->getInteger('assertion.lifetime', 300);
378
        }
379
380
        $response = ADFS::generateResponse($idpEntityId, $spEntityId, $nameid, $attributes, $assertionLifetime);
381
382
        $privateKeyFile = Utils\Config::getCertPath($idpMetadata->getString('privatekey'));
383
        $certificateFile = Utils\Config::getCertPath($idpMetadata->getString('certificate'));
384
        $passphrase = $idpMetadata->getString('privatekey_pass', null);
385
386
        $algo = $spMetadata->getString('signature.algorithm', null);
387
        if ($algo === null) {
388
            $algo = $idpMetadata->getString('signature.algorithm', XMLSecurityKey::RSA_SHA256);
389
        }
390
        $wresult = ADFS::signResponse($response, $privateKeyFile, $certificateFile, $algo, $passphrase);
391
392
        $wctx = $state['adfs:wctx'];
393
        $wreply = $state['adfs:wreply'] ? : $spMetadata->getValue('prp');
394
        ADFS::postResponse($wreply, $wresult, $wctx);
0 ignored issues
show
Bug introduced by
It seems like $wreply can also be of type null; however, parameter $url of SimpleSAML\Module\adfs\IdP\ADFS::postResponse() 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

394
        ADFS::postResponse(/** @scrutinizer ignore-type */ $wreply, $wresult, $wctx);
Loading history...
395
    }
396
397
398
    /**
399
     * @param \SimpleSAML\IdP $idp
400
     * @param array $state
401
     * @return void
402
     */
403
    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

403
    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...
404
    {
405
        // NB:: we don't know from which SP the logout request came from
406
        $idpMetadata = $idp->getConfig();
407
        HTTP::redirectTrustedURL(
0 ignored issues
show
Bug introduced by
The type SimpleSAML\Module\adfs\IdP\HTTP 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...
408
            $idpMetadata->getValue('redirect-after-logout', Utils\HTTP::getBaseURL())
409
        );
410
    }
411
412
413
    /**
414
     * @param \SimpleSAML\IdP $idp
415
     * @throws \Exception
416
     * @return void
417
     */
418
    public static function receiveLogoutMessage(IdP $idp): void
419
    {
420
        // if a redirect is to occur based on wreply, we will redirect to url as
421
        // this implies an override to normal sp notification
422
        if (isset($_GET['wreply']) && !empty($_GET['wreply'])) {
423
            $idp->doLogoutRedirect(Utils\HTTP::checkURLAllowed($_GET['wreply']));
424
            throw new \Exception("Code should never be reached");
425
        }
426
427
        $state = [
428
            'Responder' => [ADFS::class, 'sendLogoutResponse'],
429
        ];
430
        $assocId = null;
431
        // TODO: verify that this is really no problem for:
432
        //       a) SSP, because there's no caller SP.
433
        //       b) ADFS SP because caller will be called back..
434
        $idp->handleLogoutRequest($state, $assocId);
435
    }
436
437
438
    /**
439
     * accepts an association array, and returns a URL that can be accessed to terminate the association
440
     *
441
     * @param \SimpleSAML\IdP $idp
442
     * @param array $association
443
     * @param string $relayState
444
     * @return string
445
     */
446
    public static function getLogoutURL(IdP $idp, array $association, string $relayState): string
447
    {
448
        $metadata = MetaDataStorageHandler::getMetadataHandler();
449
        $spMetadata = $metadata->getMetaDataConfig($association['adfs:entityID'], 'adfs-sp-remote');
450
        $returnTo = Module::getModuleURL(
451
            'adfs/idp/prp.php?assocId=' . urlencode($association["id"]) . '&relayState=' . urlencode($relayState)
452
        );
453
        return $spMetadata->getValue('prp') . '?wa=wsignoutcleanup1.0&wreply=' . urlencode($returnTo);
454
    }
455
}
456