simplesamlphp / simplesamlphp

SimpleSAMLphp is an application written in native PHP that deals with authentication.
https://simplesamlphp.org
GNU Lesser General Public License v2.1
1.07k stars 679 forks source link

Retrieve SAML assertion from SAML Response #220

Open alexsenatore opened 9 years ago

alexsenatore commented 9 years ago

Hi all, considering the following scenario

In this scenario, IdP and Backend Service Platform are trusted and OAuth2 token is generated using SAMLBearer grant type by passing also the assertion included in the response.

The E2E flow works in this way 1) user hits a Web App URL 2) user is redirected to IdP (SP initiated flow) 3) user is authenticated on IdP and redirected to Web Application 4) web application retrieves SAML assertion from the SAML response 5) web application requests an OAuth2 token by using SAMLBearer as grant type and the assertion as value 6) Backend Service Platform checks the SAML Response (signature, encryption etc...) and generates the token 7) web application starts consuming APIs

We were able to succesfully implement the first three steps using simpleSAMLphp v1.13 However, there are no APIs available to get the assertion from the response for step 4 (the getAttributes can only be used to retrieve claims..)

In order to implement the E2E flow, we had to add a method in the following file to retrieve the assertion • simplesamlphp\vendor\simplesamlphp\saml2\src\SAML2\HTTPPost.php

The assertion is then retireved in • simplesamlphp\modulerà\saml\www\sp\saml2-acs.php

With this change we were able to complete the flow

I think it make sense to have this as an enhancement, so that the same scenario can be supported out of the box.

jaimeperez commented 9 years ago

Hi @alexsenatore!

Thanks for your suggestion.

I'm afraid implementing what you are asking for is not that simple. First of all, note that what you modified is not SimpleSAMLphp, but its sister SAML2 library. Then, you modified the HTTPPost class, which is actually one single implementation of the many SAML 2.0 bindings supported. This should therefore be implemented for every binding, given that you may receive responses on any of them if you have configured your SSP adequately. HTTP-post is the most common binding, but the rest of them should also be supported.

The problem with other bindings is that the response may not be immediately available to you, or may require additional steps (as in the artifact binding, where you get an artifact you can use to query the IdP for the response in the back channel). In any case, you would need to implement a transversal way to obtain the response from any binding.

There's also the issue about what you need to do next with the response. The library creates a DOM object as soon as possible with the response, and you can't use that since the DOM interface is not serializable and therefore cannot be saved in the state array for future iterations. So you would need to capture the string very early, and keep it in the state array for a while.

Finally, you have to consider whether you actually need the response or the assertion. SAML responses are targeted to a specific recipient (the service provider), and therefore they are not intended to be carried forward. The meaningful part here is the SAML assertion, which contains information about a certain subject. Assertions can have their audience restricted, but that could be according to your setup, so that the audience intended for the assertion is the final OAuth2 consumer and your SAML service provider ignores it completely (it could even be encrypted, making it impossible for the SP to see it). I'm definitely not an expert on OAuth, but I think you'd want to pass the assertion rather than the response...

In any case, contributions are always welcome, and if you can provide a pull request with a complete implementation we could of course discuss the specifics and see if this is reasonable or not.

Desseres commented 8 years ago

Hey. I recently had the same problem. I received a reply from ADFS and I had to delegate request (ActAs). I needed a Security Token logged-in user (Assertion). I changed the 2 functions. Security Token for user is saved to session variable.

\saml\simplesamlphp-1.13.2\lib\SimpleSAML\Utils\XML.php

    public static function debugSAMLMessage($message, $type) {
        if (!(is_string($type) && (is_string($message) || $message instanceof \DOMElement))) {
            throw new \InvalidArgumentException('Invalid input parameters.');
        }

        $globalConfig = \SimpleSAML_Configuration::getInstance();

        if ($message instanceof \DOMElement) {
            $message = $message->ownerDocument->saveXML($message);
        }

        switch ($type) {
            case 'in':
                \SimpleSAML_Logger::debug('Received message:');
                break;
            case 'out':
                \SimpleSAML_Logger::debug('Sending message:');
                break;
            case 'decrypt':
                \SimpleSAML_Logger::debug('Decrypted message:');
                break;
            case 'encrypt':
                \SimpleSAML_Logger::debug('Encrypted message:');
                break;
            default:
                assert(false);
        }

        $str = self::formatXMLString($message);
        foreach (explode("\n", $str) as $line) {
            if (!$globalConfig->getBoolean('debug', true)) {
                \SimpleSAML_Logger::debug($line);
            }
        }

        return $message;
    }

\saml\simplesamlphp-1.13.2\vendor\simplesamlphp\saml2\src\SAML2\EncryptedAssertion.php

    public function getAssertion(XMLSecurityKey $inputKey, array $blacklist = array()) {
        $assertionXML = SAML2_Utils::decryptElement($this->encryptedData, $inputKey, $blacklist);

        $str = \SimpleSAML\Utils\XML::debugSAMLMessage($assertionXML, 'decrypt');
        $sessionObject = SimpleSAML_Session::getSession();
        $sessionObject->setData('string', 'SAMLAssertionCLEAR', $str);

        return new SAML2_Assertion($assertionXML);
    }
ghost commented 8 years ago

Hi @jaimeperez

You mention that it would be better to obtain the assertion instead of the response. Is this possible in the current version of simplesamlphp without changing any code? I'm trying to obtain this assertion so I can get an OAuth token from an OAuth 2.0 service, but I can't seem to get to the assertion without actually changing the simplesamlphp code?

Kind regards Nils

jaimeperez commented 8 years ago

Hi @nlamot.

Yes, capturing the response doesn't make any sense since it's just the outer wrapper, the protocol message, not the piece of information carrying assertions about the user's identity. That would be the assertion, of course.

In any case, no, I'm afraid it's not possible.

@Desseres, I'm afraid your code is wrong. First, because you don't need to use \SimpleSAML\Utils\XML::debugSAMLMessage() to retrieve the assertion, you already have it. Second, because if you want to save it, you shouldn't save it formatted, you should worry about storage space, not about formatting XML that's not intended to be shown to a user. And third, because you are taking the wrong approach to saving it in the session. The type parameter in SimpleSAML_Session::setData is not a data type (as in a string, or an integer), but a unique string that helps you identify the contents (as in SAMLAssertion). In any case, you shouldn't be storing the assertion like that. The assertion is something you get from a SAML auth source, so it should be the SAML auth source the one who dumps the assertion into its own state and marks it as persistent so that it is kept in the session.

ghost commented 8 years ago

Thanks @jaimeperez for your quick response! But in that case, your webserver cannot authenticate to webservices using the credentials of the logged in user? I need to consume an OData service using the credentials of my logged in user. Since I don't have his password and I cannot access his assertion, I can't find a way to do this.

I'm searching on this issue for months, but can't seem to find a solution. Is there something obvious I'm overlooking? This doesn't seem like a very exotic scenario to me?

jaimeperez commented 8 years ago

Hi again!

Well, it's not an exotic scenario to OAuth, but this is not SimpleOAuthPHP, but SimpleSAMLphp 😄

In SAML, the IdP sends the SP a SAML response containing an assertion. SimpleSAMLphp (when acting as an SP) will process that and set up a session. You can then ask SSP in your application if the user is authenticated and what are his/her attributes. That's the normal use case in SAML, so if you compare to that, what you are trying to do is indeed exotic.

ghost commented 8 years ago

Woops, I didn't give you the complete background, sorry. :)

OAuth is our second try at it. First we also authenticated the webservice with SimpleSAML. The IdP is an AD-server. I read that you can use an "ActAs" from AD to implement this, but again we needed the SAML assertion to do this, since we don't have the user credentials.

Thank you very much for your feedback so far, it's nice to get confirmation that this indeed isn't possible and I'm not overlooking anything obvious.

jaimeperez commented 8 years ago

Hi!

You should indeed never receive the user credentials 😉

I don't know if it is common to use SAML assertions as tokens for OAuth, to be honest. In any case, I don't have anything against making assertions available so that you can use them for that or any other use you might imagine, no matter how perverted that'd be 😆 My only requirement is to do it right, and of course PRs are always welcome!

ghost commented 8 years ago

Ok, thanks!

I certainly want to try and add this, but can you hint me where I could start looking for a place to do it?

jaimeperez commented 8 years ago

Hi @nlamot!

It really depends on what you need. If you are fine with having a SAML2\Assertion object that you can manipulate (and convert to XML if you want), then it's relatively simple, and could probably be done entirely in the AssertionConsumerService endpoint (modules/saml/www/sp/saml2-acs.php). You basically need to capture the assertion (note the array iterating over a list of assertions, though I would say only the last one is used) and keep it in the state array, under a meaningful key, i.e.:

    $state['saml:sp:Assertion'] = $assertion;

Then you would need to add that entry in the state array holding the assertion (remember this is an object, not the raw string containing the XML representation of the assertion) as persistent. Since the size of the assertion can be quite significant and this will end up stored in the session, I think it's mandatory to have a configuration option to enable this (disabled by default). E.g.:

$sp_c = $source->getMetadata();
if ($sp_c->getBoolean('KeepAssertions', false)) {
    $state['PersistentAuthData'][] = 'saml:sp:Assertion';
}

Some additional considerations:

I don't know if I missed anything. As you can see, it's relatively simple, but not that simple anyway... 😄

jrobinss commented 7 years ago

Hello all, I'm just pitching in to say that I have the exact same requirement. It comes from using WSO2, check out this instructions page: https://docs.wso2.com/display/AM200/Exchanging+SAML2+Bearer+Tokens+with+OAuth2+-+SAML+Extension+Grant+Type

@alexsenatore could you by any chance share your SSP patch? I'd like to see this working, even if I have to change my implentation later...

BTW, I asked the question here: http://stackoverflow.com/questions/40723999/how-to-retrieve-saml2-token-in-php-for-getting-an-oauth-token-from-wso2-apim

Linux4ever commented 6 years ago

I see this is still open. I used a different package to do SP but used simplesamlphp as IDP.

Here is sample code (using onelogin/php-saml as SP):

if (isset($_POST['SAMLResponse']))
{
       $samlSettings = new OneLogin_Saml2_Settings();
       $samlResponse = new OneLogin_Saml2_Response($samlSettings, $_POST['SAMLResponse']);

       $dom = $samlResponse->getXMLDocument();
       $xml = simplexml_import_dom($dom);

       $parse = $xml->xpath('saml:Assertion');

       $parsexml = new SimpleXmlElement($parse[0]->asXML());
       $parsexml->addAttribute('xmlns:xmlns:saml', 'urn:oasis:names:tc:SAML:2.0:assertion');

       $samlAssertion = base64_encode($parsexml->asXML());
}

Pass the saml assertion to WSO2 to get Token.