IdP Proxy for two(mandatory) or more factor authentication

100 views
Skip to first unread message

Anshuman Mor

unread,
Apr 13, 2016, 6:08:50 AM4/13/16
to SimpleSAMLphp
I have a scenario where I want to authenticate against 2 or more IdPs through a proxy, has someone implemented this earlier? Each IdP will handle one type of authentication including (username/password, X509 cert based, and second factor done by third party IdP). It would be great if someone can direct me something on implementing this?

Patrick Radtke

unread,
Apr 14, 2016, 5:02:07 PM4/14/16
to SimpleSAMLphp
The general approach of doing a proxy in SSP is to define an SP as an authsource, and an IdP that uses the SP for authentication.
Your SP would send the user to the proxy IDP, the proxy IDP would try to authenticate the user using the SP authsource. The authsource would then present a discovery page and the user would pick the IdP they wanted.

You requirements sound more complex. Are you wanting a user to authenticate against multi IdPs as part of their login flow? We did something similar with a custom Auth processing filter. Basically we use the SSP api https://simplesamlphp.org/docs/1.14/simplesamlphp-sp-api to require the user to authenticate to certain IdPs. Forcing a user to log into multiple IdPs will re-set their SSP session for each auth so you'll need to set your own cookie and store any desired attributes from each IdP. I also believe the initial requesting SP context info may get lost once you do this, so you'd need to store that as well.
It is a bit complex and semi-difficult to write unit tests.

-Patrick

Pieter van der Meulen

unread,
Apr 15, 2016, 4:30:16 AM4/15/16
to simple...@googlegroups.com
We implemented a gateway based on the SSP library that can authenticate to two IdPs to perform stepup authentication with a second factor. The source is on GitHub: https://github.com/SURFnet/Stepup-Gateway

This gateway is part of the “SURFconext Strong Authentication” service we operate to offer second factor authentication as a service to the existing IdPs in our federation. Note that the Gateway requires other stepup-* components to function. It’s all open source and on github. Information about the service: https://wiki.surfnet.nl/display/surfconextdev/SURFconext+Strong+Authentication

> On 13 Apr 2016, at 12:08, Anshuman Mor <mor.an...@gmail.com> wrote:
>
> I have a scenario where I want to authenticate against 2 or more IdPs through a proxy, has someone implemented this earlier? Each IdP will handle one type of authentication including (username/password, X509 cert based, and second factor done by third party IdP). It would be great if someone can direct me something on implementing this?
>
> --
> You received this message because you are subscribed to the Google Groups "SimpleSAMLphp" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to simplesamlph...@googlegroups.com.
> To post to this group, send email to simple...@googlegroups.com.
> Visit this group at https://groups.google.com/group/simplesamlphp.
> For more options, visit https://groups.google.com/d/optout.

Anshuman Mor

unread,
Apr 18, 2016, 2:06:31 AM4/18/16
to SimpleSAMLphp
Thanks Patrick for the answer.

Just after posting this question I found a post where similar kind of questions was asked and someone suggested a solution, the link for that post is - 'https://groups.google.com/forum/#!searchin/simplesamlphp/Authenticating$20against$20two$20IdPs|sort:relevance/simplesamlphp/5V4HUA4eeQY/GApxIPX-YiIJ'.

I followed this approach and it worked for me but I am still evaluating whether this kind of implementation will leave any grey area on overall authentication. 

I basically created a new auth source similar to 'multiauth', and configured multiple IDP based sub authentication sources and then I called each of them from loginCompleted except the first one which I called from authentication of my new module.

How does it sound to you, Is there anything I should be worrying about?

My authsources.php looks like - 

<?php
$config = array(
        'admin' => array(
                'core:AdminPassword',
        ),
        'multifactor-idp-auth' => array(
                'multifactoridpauth:MultiFactorIdP',
                'sources' => array('cert', 'userpw'),
        ),
        'thirdparty' => array(
                'saml:SP',
                'entityID' => 'https://anshuman-proxy.example.net',
                'idp' => 'thirdparty.example.net',
                'certificate'   => '/var/simplesamlphp/cert/anshuman-proxy.example.net.crt',
                'privatekey'    => '/var/simplesamlphp/cert/anshuman-proxy.example.net.pem',
                'discoURL' => null,
                'redirect.sign' => false,
                'sign.authnrequest' => true,
                'NameIDPolicy' => null,
                'AuthnContextClassRef' => 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified',
        ),
        'cert' => array(
                'saml:SP',
                'entityID' => 'https://anshuman-proxy.example.net',
                'idp' => 'https://shibboleth-idp.example.net/idp/shibboleth',
                'idp' => 'https://shibboleth-idp.example.net/idp/shibboleth',
                'certificate'   => '/var/simplesamlphp/cert/anshuman-proxy.example.net.crt',
                'privatekey'    => '/var/simplesamlphp/cert/anshuman-proxy.example.net.pem',
                'discoURL' => null,
                'redirect.sign' => false,
                'sign.authnrequest' => false,
        ),
        'userpw' => array(
                'saml:SP',
                'entityID' => 'https://anshuman-proxy.example.net',
                'certificate'   => '/var/simplesamlphp/cert/anshuman-proxy.example.net.crt',
                'privatekey'    => '/var/simplesamlphp/cert/anshuman-proxy.example.net.pem',
                'discoURL' => null,
                'redirect.sign' => false,
                'sign.authnrequest' => false,
        ),
);


My module class file looks like -

/var/simplesamlphp/modules/multifactoridpauth/lib/Auth/Source/MultiFactorIdP.php

<?php
class sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP extends SimpleSAML_Auth_Source {
        private $sources;

        public function __construct($info, $config) {
                assert('is_array($info)');
                assert('is_array($config)');

                parent::__construct($info, $config);

                if (!array_key_exists('sources', $config)) {
                        throw new Exception('The required "sources" config option was not found');
                }
                $this->sources = $config['sources'];
        }

        public function authenticate(&$state) {
                $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.sources'] = $this->sources;
                $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.completed'] = array();
                assert('is_array($state)');
                foreach ($this->sources as $authId) {
                        $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.current'] = $authId;
                        $as = SimpleSAML_Auth_Source::getById($authId);
                        if ($as === NULL) {
                                throw new Exception('Invalid authentication source: ' . $authId);
                        }
                        $as->authenticate($state);
                }
        }

        public function initLogin($return, $errorURL = null, array $params = array()) {
                SimpleSAML_Logger::debug('Anshuman - MultiFactorIdP->initLogin is called for ');
                assert('is_string($return) || is_array($return)');
                assert('is_string($errorURL) || is_null($errorURL)');

                $state = array_merge($params, array(
                        'SimpleSAML_Auth_Default.id' => $this->authId, // TODO: remove in 2.0
                        'SimpleSAML_Auth_Source.id' => $this->authId,
                        'SimpleSAML_Auth_Default.Return' => $return, // TODO: remove in 2.0
                        'SimpleSAML_Auth_Source.Return' => $return,
                        'SimpleSAML_Auth_Default.ErrorURL' => $errorURL, // TODO: remove in 2.0
                        'SimpleSAML_Auth_Source.ErrorURL' => $errorURL,
                        'LoginCompletedHandler' => array(get_class(), 'loginCompleted'),
                        'LogoutCallback' => array(get_class(), 'logoutCallback'),
                        'LogoutCallbackState' => array(
                        'SimpleSAML_Auth_Default.logoutSource' => $this->authId, // TODO: remove in 2.0
                        'SimpleSAML_Auth_Source.logoutSource' => $this->authId,
                        ),
                ));
                if (is_string($return)) {
                        $state['SimpleSAML_Auth_Default.ReturnURL'] = $return; // TODO: remove in 2.0
                        $state['SimpleSAML_Auth_Source.ReturnURL'] = $return;
                }

                if ($errorURL !== null) {
                        $state[SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL] = $errorURL;
                }

                try {
                        $this->authenticate($state);
                } catch (SimpleSAML_Error_Exception $e) {
                        SimpleSAML_Auth_State::throwException($state, $e);
                } catch (Exception $e) {
                        $e = new SimpleSAML_Error_UnserializableException($e);
                        SimpleSAML_Auth_State::throwException($state, $e);
                }
                self::loginCompleted($state);
        }
        public static function loginCompleted($state) {
                assert('is_array($state)');
                assert('array_key_exists("SimpleSAML_Auth_Source.Return", $state)');
                assert('array_key_exists("SimpleSAML_Auth_Source.id", $state)');
                assert('array_key_exists("Attributes", $state)');
                assert('!array_key_exists("LogoutState", $state) || is_array($state["LogoutState"])');

                $return = $state['SimpleSAML_Auth_Source.Return'];
                $session = SimpleSAML_Session::getSessionFromRequest();
                $authId = $state['SimpleSAML_Auth_Source.id'];

                $sources = $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.sources'];
                $completed = $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.completed'];
                $current = $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.current'];

                SimpleSAML_Logger::debug('Last completed source' . $current);
                $completed = array_merge($completed, array($current));
                $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.completed'] = $completed;
                SimpleSAML_Logger::debug('All available sources ' . var_export($sources, true));
                SimpleSAML_Logger::debug('Completed sources ' . var_export($completed, true));

                foreach(array_diff_key($sources, $completed) as $authId) {
                        $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.current'] = $authId;
                        $as = SimpleSAML_Auth_Source::getById($authId);
                        $as->authenticate($state);
                        assert('false');
                }

                $state['sspmod_multifactoridpauth_Auth_Source_MultiFactorIdP.completed'] = $completed;
                $session->doLogin($authId, SimpleSAML_Auth_State::getPersistentAuthData($state));
                SimpleSAML_Logger::debug('Final state object ' . var_export($state, true));
                if (is_string($return)) { // redirect...
                        \SimpleSAML\Utils\HTTP::redirectTrustedURL($return);
                } else {
                        call_user_func($return, $state);
                }
                assert('false');
        }
}
?>


This is half completed yet though.

Thanks,
Anshuman

Patrick Radtke

unread,
Apr 19, 2016, 12:59:24 PM4/19/16
to SimpleSAMLphp
Hi Anshuman,

You are into part of the code that I'm unfamiliar with, so I can't comment on the approach :)
Thanks for sharing your code - it is interesting reading through other approaches to a problem.

-Patrick
Reply all
Reply to author
Forward
0 new messages