simplesamlphp / saml2

SimpleSAMLphp low-level SAML2 PHP library
https://www.simplesamlphp.org
GNU Lesser General Public License v2.1
284 stars 136 forks source link

A "urn:oid:1.3.6.1.4.1.5923.1.1.1.10" (EPTI) attribute value must be a NameID, none found for value no. "0" #105

Closed ghost closed 7 years ago

ghost commented 7 years ago

Running SimpleSAMLphp 1.15 (RC1/2) as SP, against older version SimpleSAMLphp 1.8 as IdP. Gets the following "Received message" which fails at "SAML2\Assertion::parseAttributeValue".

12:03:57 DEBUG <> 12:03:57 DEBUG ---identical---</saml:NameID> 12:03:57 DEBUG 12:03:57 DEBUG 12:03:57 DEBUG </saml:SubjectConfirmation> 12:03:57 DEBUG </saml:Subject>

12:03:57 DEBUG <> 12:03:57 DEBUG 12:03:57 DEBUG <saml:AttributeValue xsi:type="xs:string”>snip@snip.snip</saml:AttributeValue> 12:03:57 DEBUG </saml:Attribute> 12:03:57 DEBUG 12:03:57 DEBUG <saml:AttributeValue xsi:type="xs:string”>g s</saml:AttributeValue> 12:03:57 DEBUG </saml:Attribute> 12:03:57 DEBUG 12:03:57 DEBUG ---identical---</saml:AttributeValue> 12:03:57 DEBUG </saml:Attribute> 12:03:58 DEBUG </saml:AttributeStatement> 12:03:58 DEBUG </saml:Assertion> 12:03:58 DEBUG </samlp:Response> 12:03:58 ERROR SimpleSAML_Error_Error: UNHANDLEDEXCEPTION 12:03:58 ERROR Backtrace: 12:03:58 ERROR 1 simplesamlphp\www_include.php:45 (SimpleSAML_exception_handler) 12:03:58 ERROR 0 [builtin] (N/A) 12:03:58 ERROR Caused by: SAML2\Exception\RuntimeException: A "urn:oid:1.3.6.1.4.1.5923.1.1.1.10" (EPTI) attribute value must be a NameID, none found for value no. "0" 12:03:58 ERROR Backtrace: 12:03:58 ERROR 7 simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\Assertion.php:539 (SAML2\Assertion::parseAttributeValue) 12:03:58 ERROR 6 simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\Assertion.php:521 (SAML2\Assertion::parseAttributes) 12:03:58 ERROR 5 simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\Assertion.php:280 (SAML2\Assertion::construct) 12:03:58 ERROR 4 simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\Response.php:38 (SAML2\Response::construct) 12:03:58 ERROR 3 simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\Message.php:578 (SAML2\Message::fromXML) 12:03:58 ERROR 2 simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\HTTPPost.php:76 (SAML2\HTTPPost::receive) 12:03:58 ERROR 1 simplesamlphp\modules\saml\www\sp\saml2-acs.php:31 (require) 12:03:58 ERROR 0 simplesamlphp\www\module.php:135 (N/A)

    private function parseSubject(\DOMElement $xml)
    {
        $subject = Utils::xpQuery($xml, './saml_assertion:Subject');
        if (empty($subject)) {
            /* No Subject node. */
            return;
        } elseif (count($subject) > 1) {
            throw new \Exception('More than one <saml:Subject> in <saml:Assertion>.');
        }
        $subject = $subject[0];
        $nameId = Utils::xpQuery(
            $subject,
            './saml_assertion:NameID | ./saml_assertion:EncryptedID/xenc:EncryptedData'
        );
        if (count($nameId) > 1) {
            throw new \Exception('More than one <saml:NameID> or <saml:EncryptedID> in <saml:Subject>.');
        } elseif (!empty($nameId)) {
            $nameId = $nameId[0];
            if ($nameId->localName === 'EncryptedData') {
                /* The NameID element is encrypted. */
                $this->encryptedNameId = $nameId;
            } else {
                $this->nameId = new XML\saml\NameID($nameId);
            }
        }
        $subjectConfirmation = Utils::xpQuery($subject, './saml_assertion:SubjectConfirmation');
        if (empty($subjectConfirmation) && empty($nameId)) {
            throw new \Exception('Missing <saml:SubjectConfirmation> in <saml:Subject>.');
        }
        foreach ($subjectConfirmation as $sc) {
            $this->SubjectConfirmation[] = new SubjectConfirmation($sc);
        }
    }

https://github.com/simplesamlphp/saml2/blob/v3.0.2/src/SAML2/Assertion.php#L291

    private function parseAttributeValue($attribute, $attributeName)
    {
        /** @var \DOMElement[] $values */
        $values = Utils::xpQuery($attribute, './saml_assertion:AttributeValue');
        if ($attributeName === Constants::EPTI_URN_MACE || $attributeName === Constants::EPTI_URN_OID) {
            foreach ($values as $index => $eptiAttributeValue) {
                $eptiNameId = Utils::xpQuery($eptiAttributeValue, './saml_assertion:NameID');
                if (count($eptiNameId) !== 1) {
                    throw new RuntimeException(sprintf(
                        'A "%s" (EPTI) attribute value must be a NameID, none found for value no. "%d"',
                        $attributeName,
                        $index
                    ));
                }
                $this->attributes[$attributeName][] = new XML\saml\NameID($eptiNameId[0]);
            }
            return;
        }
        foreach ($values as $value) {
            $hasNonTextChildElements = false;
            foreach ($value->childNodes as $childNode) {
                /** @var \DOMNode $childNode */
                if ($childNode->nodeType !== XML_TEXT_NODE) {
                    $hasNonTextChildElements = true;
                    break;
                }
            }
            if ($hasNonTextChildElements) {
                $this->attributes[$attributeName][] = $value->childNodes;
                continue;
            }
            $type = $value->getAttribute('xsi:type');
            if ($type === 'xs:integer') {
                $this->attributes[$attributeName][] = (int)$value->textContent;
            } else {
                $this->attributes[$attributeName][] = trim($value->textContent);
            }
        }
    }

https://github.com/simplesamlphp/saml2/blob/v3.0.2/src/SAML2/Assertion.php#L529

thijskh commented 7 years ago

You are sending a string in the eduPersonTargetedID (urn:oid:1.3.6.1.4.1.5923.1.1.1.10) attribute. That is not allowed; per the specification eduPersonTargetedID is an XML construct, being a NameID. Your IdP needs to change what it sends for the eduPersonTargetedID.

ghost commented 7 years ago

@thijskh: Is there anything we can do on our SP side? And for the IdP, how do they change this setting (and specifically for our SP)? Thanks.

ghost commented 7 years ago

We can login when we tested disable NameID lookup in "private function parseAttributeValue($attribute, $attributeName)" (tested with this particular IdP). Is it purely a string vs XML issue involved?

ghost commented 7 years ago

Should I expect the IdP to send the attribute in this way instead?

<saml:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10"
                NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
    <saml:AttributeValue>
        <saml:NameID NameQualifier="IdP-Entity-ID"
                     SPNameQualifier="SP-Entity-ID">
            xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        </saml:NameID>
    </saml:AttributeValue>
</saml:Attribute>
thijskh commented 7 years ago

Yes, that is how it should look. Not sure what your IdP config looks like, but if you're running 1.8 that might be a good idea to upgrade anyway.

ghost commented 7 years ago

I have no control over the IdP which belongs to an institution. It is one of the IdPs we get from a federation metadata stream. We spent 1.5 hours on the phone this friday afternoon, trying to solve this. If the guy has a boss (which I think he has), I would not expect any futher help. However, we would like to know how how to prevent it from happening again in the future.

Is below what the IdP need to send?

'attributeencodings' => array('urn:oid:1.3.6.1.4.1.5923.1.1.1.10' => 'raw',),

Is below a better approach? Will this also move the NameID from Subject to AttributeValue as RAW XML?

'authproc' => array(
    // Generate the persistent NameID.
    2 => array(
        'class' => 'saml:PersistentNameID',
        'attribute' => 'eduPersonPrincipalName',
    ),
    // Add the persistent to the eduPersonTargetedID attribute
    60 => array(
        'class' => 'saml:PersistentNameID2TargetedID',
        'attribute' => 'eduPersonTargetedID', // The default
        'nameId' => TRUE, // The default
    ),
    // Use OID attribute names.
    90 => array(
        'class' => 'core:AttributeMap',
        'name2oid',
    ),
),
// The URN attribute NameFormat for OID attributes.
'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
'attributeencodings' => array(
    'urn:oid:1.3.6.1.4.1.5923.1.1.1.10' => 'raw', /* eduPersonTargetedID with oid NameFormat is a raw XML value */
),

Is it possible for the IdP choose to send the RAW XML specifically to our SP? Or, can our SP to convert the "received message" to RAW XML instead? Should we have any doubt that the IdP is the only place to configure sending this RAW XML attribute? Also I can't see 'NameQualifier' from this specific IdP, could it cause a problem for NameID generation, for instance a persistent one. And at last, is version 1.8 capable and compatible with the current version and protocols?

@thijskh: Many thanks for your guidance.

thijskh commented 7 years ago

I believe the approach with PersistentNameID2TargetedID sketched above should work. However, that authproc filter has only been added in SSP 1.11. Not sure how they generate the attribute now. Core of the problem is indeed that according to specification the attribute value must be an XML nameID.

It might be difficult to fix this in the SP because the error already happens at parsing the assertion.

Some possible alternative approaches:

ghost commented 7 years ago

@thijskh: Many thanks for your help. Will follow your advice.

However, I don't know if I am still mixing up our problem with this one: eduPersonTargetedId with SSP 1.14.6

thijskh commented 7 years ago

That was fixed in 1.14.7 (2016) already.

ghost commented 6 years ago

Confirming "IdP stops releasing eduPersonTargetedID at all (to your SP or in general)" is a working solution.

Many thanks @thijskh