AD FS – uzyskiwanie Security Token dla użytkownika

STS (Security Token Service) jest usługą która dostarcza tokeny zabezpieczające zgodne ze standardami WS-Trust i WS-Federation protocols. Ostatnio musiałem uzyskać token zabezpieczający z Active Directory Federation Services (AD FS) 2.0 właśnie w języku jakim jest PHP.  Istnieje kilka ciekawych i dobrze działających rozwiązań. Jednym z nich jest SimpleSAMLphp (https://simplesamlphp.org/). Poza połączeniem z idP jakim jest AD FS oferuje również możliwość połączenia się z innymi systemami SSO, takmi jak Facebook, oAuth, Google+, InfoCards. Używam SimpleSAMLphp do połączenia z ADFSem, jednak w tym przypadku potrzebowałem uzyskać token bezpośrednio od usługi a nie poprzez redirect i delegację.Więcej o scenariuszach autoryzacji możesz przeczytać np. tutaj: https://msdn.microsoft.com/en-us/library/ee517273.aspx

Poniższy kod może znaleźć zastosowanie w urządzeniach mobilnych i aplikacjach okienkowych w których nie jest dopuszczalne przekierowanie adresu URL na serwis autoryzujący. Wadą takiego rozwiązania jest fakt, że aplikacja posiada przez pewien czas login oraz hasło użytkownika w swoich zasobach. Metoda delegacji autoryzacji zwraca już uwierzytelnionego użytkownika przez co aplikacja nie posiada na żadnym etapie działania loginu oraz hasła użytkownika jednocześnie.

Klasa która została przedstawiona poniżej umożliwia poprzez podanie swojego loginu i hasła oraz adresu uwierzytelnienia uzyskać token z STS’a. Jest to tylko działający model który w warunkach idealnych ma zwrócić odpowiedź. Klasa służy mi tylko do testów innego, ciekawszego rozwiązania które niedługo opiszę.

<?php

/**
 * @author Paweł Kasztelan (pawel@kasztelan.me)
 */
class SimpleSAML_Neuca {

    private $SAML_ApplicationLoginUser;
    private $SAML_ApplicationPasswordUser;
    private $SAML_AuthorizationEntityIDUser;
    private $SAML_AuthorizationService;
    private $samlToken;

    /**
     * Wywołanie klasy, uzyskuje token dla poniższych danych
     */
    public function __construct() {
        $this->SAML_ApplicationLoginUser = "UserLogin";
        $this->SAML_ApplicationPasswordUser = "UserPassword";
        $this->SAML_AuthorizationEntityIDUser = "http://samltest.local/";
        $this->SAML_AuthorizationService = "https://example.com/adfs/services/trust/13/usernamemixed";
        $this->parseAuthToken();
    }

    /**
     * Zwraca zyskany token
     * @return type
     */
    public function getAuthToken() {
        return $this->samlToken;
    }

    /**
     * SOAP+XML request
     * @return type
     */
    function requetTokens() {

        $xml = <<<XML
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
        <a:To s:mustUnderstand="1">$this->SAML_AuthorizationService</a:To>
        <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" >
            <o:UsernameToken u:Id="uuid-6a13a244-dac6-42c1-84c5-cbb345b0c4c4-1">
                <o:Username>$this->SAML_ApplicationLoginUser</o:Username>
                <o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">$this->SAML_ApplicationPasswordUser</o:Password>
            </o:UsernameToken>
        </o:Security>
    </s:Header>
    <s:Body>
        <trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
            <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
                <a:EndpointReference>
                    <a:Address>$this->SAML_AuthorizationEntityIDUser</a:Address>
                </a:EndpointReference>
            </wsp:AppliesTo>
            <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType>
            <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
            <trust:TokenType>urn:oasis:names:tc:SAML:2.0:assertion</trust:TokenType>
        </trust:RequestSecurityToken>
    </s:Body>
</s:Envelope>
XML;

        $result = $this->send($xml);
        return $result;
    }

    /**
     * Wysyła request do serwisu prosząc o SAML Token
     * @param type $xml
     * @return type
     */
    function send($xml) {
        $TakeToken = curl_init();
        curl_setopt($TakeToken, CURLOPT_URL, $this->Neuca_AuthorizationService);
        curl_setopt($TakeToken, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($TakeToken, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($TakeToken, CURLOPT_POST, 1);
        curl_setopt($TakeToken, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($TakeToken, CURLOPT_HTTPHEADER, array('Content-Type: application/soap+xml; charset=utf-8'));
        curl_setopt($TakeToken, CURLOPT_POSTFIELDS, $xml);
        $result = curl_exec($TakeToken);

        return $result;
    }

    /**
     * Wykonuje zapytanie o request, potem sprawdza czy jest odkodowany
     * czy może jednak zakodowany i dopiero ciśnie malina dalej
     * @return type
     */
    function parseAuthToken() {

        $request = $this->requetTokens();

        $token = new DOMDocument();
        $token->loadXML($request);
        $doc = $token->documentElement;

        // jeżeli wartość jest zakodowana, to ją odkoduj a jak nie to wytnij assections z xmla i masz token!F
        if (!empty(base64_decode(@$doc->getElementsByTagname('CipherValue')->item(0)->nodeValue))) {
            $encKey = base64_decode($doc->getElementsByTagname('CipherValue')->item(0)->nodeValue);
            
            // TWÓJ CERTYFIKAT PRYWATNY
            $sts_key = dirname(__FILE__) . '/cert/saml.pem';
            
            $privkey = openssl_pkey_get_private(file_get_contents($sts_key));
            $key = NULL;
            openssl_private_decrypt($encKey, $key, $privkey, OPENSSL_PKCS1_OAEP_PADDING);
            openssl_free_key($privkey);
            $encSamlToken = base64_decode($doc->getElementsByTagname('CipherValue')->item(1)->nodeValue);
            $this->samlToken = $this->decryptMcrypt($encSamlToken, $key);
        } else {
            $tokens = preg_match('/<Assertion[\s\S]*?<\/Assertion>/', $request, $matches);
            $this->samlToken = $matches[0];
        }

        return $this->samlToken;
    }

    /**
     * Dekoduje dane na język zrozumiały dla mugoli 
     * @param type $data
     * @param type $key
     * @return type
     */
    function decryptMcrypt($data, $key) {
        $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
        $iv_length = mcrypt_enc_get_iv_size($td);

        $iv = substr($data, 0, $iv_length);
        $data = substr($data, $iv_length);

        mcrypt_generic_init($td, $key, $iv);
        $decrypted_data = mdecrypt_generic($td, $data);
        mcrypt_generic_deinit($td);
        mcrypt_module_close($td);

        $dataLen = strlen($decrypted_data);
        $paddingLength = substr($decrypted_data, $dataLen - 1, 1);
        $decrypted_data = substr($decrypted_data, 0, $dataLen - ord($paddingLength));

        return $decrypted_data;
    }

}