djoos / EscapeWSSEAuthenticationBundle

Symfony bundle to implement WSSE authentication
http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html
137 stars 59 forks source link

create tests for REST api #37

Closed rollsappletree closed 10 years ago

rollsappletree commented 10 years ago

Hi, I want to create some tests for my REST Api, that i auth with wsse. But I dont know how to write the part about wsse.

Could anyone please write down an example of a test for a call that must be authenticated with wsse?

I know this is not really a issue, but i thought this is the best place where to ask.

Thanks in advance, C

djoos commented 10 years ago

Hi @rollsappletree,

thanks for getting in touch! I'm sorry it has taken a while to get back to you... Would you mind sharing a bit more about what test you've written for the call right now?

Kind regards, David

timtailor commented 10 years ago

I'd also be interested in how to test the WSSE. As this doesn't work unfortunately (with Chrome REST tester it works):

class APIControllerTest extends WebTestCase
{

    public function testWSSE()
    {
        $client = static::createClient();

        $client->request(
            'GET',
            '/de/api/user/testauthorization',
            array(),
            array(),
            array(
                'Authorization'         => 'Authorization profile="UsernameToken"',              
                'X-WSSE'                => 'UsernameToken Username="test16", PasswordDigest="ZLG9XVPRIGNqu/hee2pw2EL0Lak=", Nonce="YzhhZTFiM2YzMWQwOWNlOA==", Created="2014-06-08T21:09:04Z"'
            )
        );

        $this->assertTrue($client->getResponse()->isSuccessful());
}
djoos commented 10 years ago

Hi @timtailor,

could you try giving 'HTTP_Authorization' and 'HTTP_X-WSSE' instead of respectively 'Authorization' and 'X-WSSE' a go? That should do the trick...

Let me know how that works out for you!

Kind regards, David

P.S. I recommend generating the digest dynamically in your tests, but I guess you just posted the "static" X-WSSE value above as a quick example...

djoos commented 10 years ago

FYI: here's a more detailed example - we make use of something along these lines for testing our APIs:

use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;

use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;

//...

private function getUsername()
{
    return self::$kernel->getContainer()->getParameter('api_user_name'); /* eg. "username" */
}

private function getSecret()
{
    return self::$kernel->getContainer()->getParameter('api_user_password'); /* eg. "userpassword" */
}

private function getSalt()
{
    return self::$kernel->getContainer()->getParameter('api_user_salt'); /* eg. "usersalt" */
}

private function getWSSEAuthEncoderAlgorithm()
{
    return self::$kernel->getContainer()->getParameter('api_wsse_auth_encoder_algorithm'); /* eg. sha512 */
}

private function getWSSEAuthEncoderEncodeHashAsBase64()
{
    return self::$kernel->getContainer()->getParameter('api_wsse_auth_encoder_encode_hash_as_base64'); /* eg. true */
}

private function getWSSEAuthEncoderIterations()
{
    return self::$kernel->getContainer()->getParameter('api_wsse_auth_encoder_iterations'); /* eg. 1000 */
}

private function getEncoder()
{
    return new MessageDigestPasswordEncoder(
        $this->getWSSEAuthEncoderAlgorithm(), /* helper method */
        $this->getWSSEAuthEncoderEncodeHashAsBase64(), /* helper method */
        $this->getWSSEAuthEncoderIterations() /* helper method */
    );
}

private function generateDigest($secret, $salt, $nonce, $created)
{
    return $this->getEncoder()->encodePassword(
        sprintf(
        '%s%s%s',
        base64_decode($nonce),
        $created,
        $secret
        ),
        $salt
    );
}

protected function setUp()
{
    $this->serializer = new Serializer(
        array(),
        array(
            'json' => new JsonEncoder(),
        'xml' => new XmlEncoder()
        )
    );
}

public function test_WSSE_API_Call()
{
    $client = static::createClient();

    $username = $this->getUsername(); /* helper method */
    $created = gmdate(DATE_ISO8601);
    $nonce = uniqid();

    $digest = $this->generateDigest(
        $this->getSecret(), /* helper method */
        $this->getSalt(), /* helper method */
        $nonce,
        $created
    ); /* helper method */

    $client->request(
        'GET',
        '/api/call.json',
        array(),
        array(),
        array(
        'HTTP_Authorization' => 'WSSE profile="UsernameToken"',
        'HTTP_X-WSSE' => 'UsernameToken Username="'.$username.'", PasswordDigest="'.$digest.'", Nonce="'.$nonce.'", Created="'.$created.'"',
        'HTTP_CONTENT_TYPE' => 'application/json'
        ),
        $this->serializer->encode(array(), 'json')
    );

    $this->assertEquals(200,$client->getResponse()->getStatusCode());
}

If you make use of actual parameters.yml parameters and use those in your security.yml as well it'll be easier in the long run...

Hope this helps! David

timtailor commented 10 years ago

Hi David,

thanks, adding the "HTTP_' really did the trick. Now i will try to replace my static string with your long example above!

I will confirm back later... Tim

djoos commented 10 years ago

Hi Tim,

great!

FYI: I've updated my comment above with some missing bits... Do let me know how it goes, so we can close this issue :-)

Thanks in advance for your feedback! David

timtailor commented 10 years ago

Sorry, but unfortunately i get a 401 error. It's probably only because of the configuration and not the test itself.

I hardcoded the parameters in your example (user, password in plain text as it is before encoding). In my security.yml i didn't change any setting regarding encoder, etc.

security:
    encoders:
        FOS\UserBundle\Model\UserInterface:
            algorithm:           sha512
            iterations:          5 

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    firewalls:
        wsse_secured:
            pattern:   ^/api/.*
            wsse:
                realm: "Secured API" 
                profile: "UsernameToken" 
                lifetime: 5000 
            provider: fos_userbundle
            anonymous: true

I currently override the Provider->getSalt to return an empty string.

Then i tried all different combinations of parameters in the test (sha1, sha512, 5 iterations, 5000 iterations, etc.) but it didn't work. The digest ist different than expected and thus i get an 401. Any ideas?

djoos commented 10 years ago

Hi Tim,

without specifying the encoder-settings in the wsse-section of your config, the provider is going to go with sha1 with 1 iteration, encoding the hash as base 64. Please have a look at "Specify a custom digest algorithm" in the README and match the settings with the encoder you're using, eg.

firewalls:
    wsse_secured:
        #...
        wsse:
            #...
            encoder: #digest algorithm
                algorithm: sha512
                encodeHashAsBase64: true
                iterations: 5

That should do the trick!

Hope this helps, David

timtailor commented 10 years ago

I tried that already (sha1 & sha512, iterations 1 & 5) without success. But i think you can close this issue anyways, as it was about a functional WSSE test (which you provided in all detail).

I still have to figure out for myself how to set the parameters. Currently the Android app creates a header like the generator at http://www.teria.com/~koseki/tools/wssegen/ It's developed by someone else, and i just failed to duplicate that in PHP ;-)

djoos commented 10 years ago

Hi @timtailor,

thanks for your feedback!

Yep, you've both (@rollsappletree as well) got a heads up to write functional tests for your API-calls.

As you mentioned it will now be a case for you to figure out what http://www.teria.com/~koseki/tools/wssegen/ uses to generate the digest and set your WSSE authentication config accordingly. I actually thought it was sha1 with 1 iteration over there, but apparently you gave that a go already :-) If you find out, do let me know and I'll make a note as every now and then there's someone who uses that generator to give the bundle a spin :-)

I'll close this ticket now, but don't hesitate to add additional comments...

Have a great day! David

timtailor commented 10 years ago

For anyone who is interested: If you developed a WSSE header accordlingly to http://www.teria.com/~koseki/tools/wssegen/ then you have to use SHA1, 1 iteration, base64 for the digest, but with the specialty that you have to use the password as it is in the database (encrypted, not plain text) and no salt (override getSalt to return empty string and empty salt in your test file.

djoos commented 10 years ago

Thanks @timtailor!