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

WSSE authentication failed #46

Closed Danny-P closed 10 years ago

Danny-P commented 10 years ago

Hi,

I try to integrate FOSUserBundle with EscapeWSSEAuthenticationBundle. I read and worked through the other issues related to this. But I still have an error "WSSE authentication failed" in my logs and it don't work.

Here is my security.yml: security: encoders: FOS\UserBundle\Model\UserInterface: sha512

role_hierarchy:
    ROLE_ADMIN:       ROLE_USER
    ROLE_SUPER_ADMIN: ROLE_ADMIN

providers:
    fos_userbundle:
        id: fos_user.user_provider.username_email

firewalls:
    dev:
        pattern:  ^/(_(profiler|wdt)|css|images|js)/
        security: false

    wsse_secured:
        pattern: ^/api/.*
        wsse:
            realm: "Gesicherte API. Bitte anmelden!"
            profile: "UsernameToken"
            lifetime: 300
            encoder:
                algorithm: sha512
        anonymous: false
        provider: fos_userbundle

    main:
        pattern: ^/
        form_login:
            provider: fos_userbundle
            csrf_provider: form.csrf_provider
        logout: true
        anonymous: true

access_control:
    - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/admin/, role: ROLE_ADMIN }

That's the relevant part of config.yml fos_rest: view: view_response_listener: true

fos_user: db_driver: orm firewall_name: wsse_secured user_class: dpn\DienstplanBundle\Entity\User

escape_wsse_authentication: authentication_provider_class: Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider authentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listener authentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPoint authentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder

In vendor/escapestudios/wsse-authentication-bundle/Escape/WSSEAuthenticationBundle/Security/Core/Authentication/Provider/Provider.php I debugged into validateDiget Method. The value in $expected is not the same as in $digest.

For testing I used http://www.teria.com/~koseki/tools/wssegen/ for generating WSSE Header. I put in my username and the hashed password, taken from the database. Nonce and Created are set to auto. I took the generated WSSE Header and tried to test my api with the Chrome Rest Console.

There I get an error 401.

Are there any known errors or have I missed something?

I use symfony version 2.4.4, FOSUser version 2.0.0 and EscapeWSSEAuthenticationBundle 2.3.1

djoos commented 10 years ago

Hi @Danny-P,

you've set up your WSSE Auth with sha512 (1 iteration) in your security.yml.

What does http://www.teria.com/~koseki/tools/wssegen/ use? In #39 @florin-r used sha1 with 1 iteration (default when not specifying any custom encoder in security.yml. I think that worked for him...

Also: what is the complete error in your log? I'd go for the approach of generating the WSSE-header myself rather than relying on the wssegen - eventually you'll have to do that anyway, right?

Hope this helps!

Kind regards, David

Danny-P commented 10 years ago

Hi! Thank you! sha1 was the point. Following this, I get my api working. For my first tries this is okay. But in future I like to change the provider, that it can works with iterations and sha512. Perhaps @florin-r will be faster ;-)

I used wssegen just for testing. Next step my android app must be able to use the api. The user uses only username / password in the app. Than the app should generate the required WSSE Header to call the api. But for the WSSE Header I need the hashed password, which is stored in the database. The user don't know it. So I will need to provide a "login" method, which returns the hashed password, when user gives this name and plaintext password. The app stores than the hashed password and use this for WSSE Header generation. But how about security? Is this a good idea to do it this way? Or what will be a better way?

Thanks!

Best regards Daniel

Sorendil commented 10 years ago

Hi !

On the same subject, I got a problem !

I'm using Fos_Userbundle as provider. My API worked but not anymore.. I've made some tests and found the problem :

In Provider.php, the validateDigest function, I need to change the following code :

//validate secret $expected = $this->encoder->encodePassword( sprintf( '%s%s%s', base64_decode($nonce), $created, $secret ), $salt );

by this :

//validate secret $expected = $this->encoder->encodePassword( sprintf( '%s%s%s', base64_decode($nonce), $created, $secret ), "" );

I'm also using http://www.teria.com/~koseki/tools/wssegen/ to test the API. In this website, in the Password field, I type the password as it is writed in the SQL table, without any salt.

So, Danny-P and the others, what is the password you type in this field ? Because it doesn't work for me, the salt is the problem (and sorry for my english)..

Thank you =)

Danny-P commented 10 years ago

Hi @Sorendil ,

the password in wssegen tool is the same, which is written in SQL table. I had to change in Provider.php line 100 - 108 from: $expected = $this->encoder->encodePassword( sprintf( '%s%s%s', base64_decode($nonce), $created, $secret ), $salt );

to:

$expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));

Than I had to set in security.yml: security: encoders: FOS\UserBundle\Model\UserInterface: sha1

after generating a new Password for my user by: php app/console fos:user:change-password myUser myPass

I could take the password out from the database, put it into wssegen tool and generate the WSSE header. After that I could test the api with Chrome REST Console and the generated WSSE header.

Hope it would help! How will you consume your api, @Sorendil ? Do you have already a way to generate the WSSE header by your own?

Best regards Daniel

Sorendil commented 10 years ago

Thank you for your reply !

I see you had to change the code of the bundle but how will you do to get last commits ?

For my project, in composer.json, i've locked the version of EscapeWSSEAuth to the commit dev-master@f1bd591f4a3be8b21288d967ddb3b723db70fee3 . For now, i'm testing the API as you do, with Rest console in Chromium and with wssegen. And it works for this commit, but not with the newest.

My project is from school, i'm coding the website and a friend the android software. He will connect to the API. To generate the WSSE header, he will follow this tutorial : http://obtao.com/blog/2013/09/how-to-use-wsse-in-android-app/ (also exists in french).

Hope it will help to consume your API ! ^^

Best regards, Anthony.

djoos commented 10 years ago

Hi guys,

if you don't want to make use of the user's salt, simply extend the provider and overwrite the getSalt() method and return an empty string from there rather than actually changing the bundle's code.

@sorendil: I'd recommend to implement the bundle via...

"require": { ... "escapestudios/wsse-authentication-bundle": "2.3.x-dev", ... }

(sorry for the formatting: no backticks on my mobile)

That way you'll have the latest of the 2.3-branch...

Security-wise: if you have pwd+salt on your client, you can use this to generate the WSSE-header - alternatively (I'd probably go for this option) generate an API token for each user and store that in your db. Use this (overwrite the provider's getSecret to return the user's API-token rather than the user's pwd and getSalt-methods) to generate tokens from the client and pass them to your WSSE-secured API.

What do you think?

Kind regards, David

djoos commented 10 years ago

@sorendil: http://obtao.com/blog/2013/09/how-to-use-wsse-in-android-app/ makes sense - but I'd have the salt-retrieval still not "publicly" available, but behind a firewall with a pwd and salt that is fixed and both your client and your API know. That way only your app(s) can retrieve salts and from there onwards you call the other API-methods secured by other WSSE auth-settings, making use of your user's pwd (known in client app) + user's salt (retrieved in client app).

Hope this helps! David

P.S. It would be great to add some of this to the docs, so please give me your feedback/experience in the project - thanks in advance!

Sorendil commented 10 years ago

Unfortunately, my English is not good enough to understand all the subtleties of your answer. And I have no time enough to test others ways to do this API so I'll probably just override getSalt as you said ! I really didn't think to this, but that's a good idea, thank you !

For the docs, a short introduction to salt could be good ! i've passed a lot of time to know how it didn't work anymore so telling about the salt could be a good idea ! (sorry for my english ^^)

Best regards, Anthony !

Danny-P commented 10 years ago

Thank you for the link to the blog post!

I think I will go this way, to handle the password: The user enters username / password in the android app. Than the app makes a call to a wsse secured API function "login". The app uses a "technical user" for accessing the api Function. This function returns the hashed password for the user and the app stores it for future access. With this Information the app can generate the wsse headers.

When I get this working, I can put the steps together into a documentation. Perhaps you can use it, for further documentation of this bundle, @djoos

Best regards Daniel

djoos commented 10 years ago

That would be great, do keep me posted @Danny-P!

Have a great weekend, David

djoos commented 10 years ago

Hi @Danny-P,

did you manage to resolve the anonymous token-issue? I got an e-mail notification, but can't seem to find it in this thread (anymore?)...

Thanks! David

Danny-P commented 10 years ago

Hi @djoos,

yes, I get it to work in a second try. Therefore I deleted my post here again. I'm working on a Step By Step Guide for others, who try to integrate them.

But I still have a problem with overloading the Provider Class to return a blank salt. I created the following class: <?php

namespace Dpn\Tutorial01Bundle\Security\Provider;

use Escape\WSSEAuthenticationBundle\Security\Core\Authentication\Provider\Provider as BaseProvider;

class Provider extends BaseProvider { protected function getSalt(\Symfony\Component\Security\Core\User\UserInterface $user) { return ""; } }

In app/config/config.yml I added: escape_wsse_authentication: authentication_provider_class: Dpn\Tutorial01Bundle\Security\Provider\Provider authentication_listener_class: Escape\WSSEAuthenticationBundle\Security\Http\Firewall\Listener authentication_entry_point_class: Escape\WSSEAuthenticationBundle\Security\Http\EntryPoint\EntryPoint authentication_encoder_class: Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder

But my Provider class isn't used. The salt still get's loaded from the Default ProviderClass.

Best regards Daniel

djoos commented 10 years ago

Hi Daniel,

thanks for your feedback!

I'm loving the sound of a step by step-guide, as documenting the bundle (specifically in relationship to the FOSUserBundle) is definitely needed...

Re: overloading Could you make sure your Tutorial01Bundle is being loaded? Could you double-check your AppKernel? At first sight there doesn't seem to be anything wrong with what you posted... Also: there's no need to override all classes, just overriding the aithentication_provider_class by your own provider is good enough - even though I admit this is not the root of the issue of extending the base provider.

Hope this helps! David

Danny-P commented 10 years ago

Hi David,

thanks for your reply!

In my AppKernel.php the Bundle is loaded: $bundles = array( new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\MonologBundle\MonologBundle(), new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), new Symfony\Bundle\AsseticBundle\AsseticBundle(), new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new Dpn\Tutorial01Bundle\DpnTutorial01Bundle(), new FOS\UserBundle\FOSUserBundle(), new JMS\SerializerBundle\JMSSerializerBundle(), new FOS\RestBundle\FOSRestBundle(), new Escape\WSSEAuthenticationBundle\EscapeWSSEAuthenticationBundle(), ); I have only overwritten the authentication_provider_class. I added the other one to configuration, just to see if it helps, when listing them all.

Best regards Daniel

djoos commented 10 years ago

Hi Daniel,

I've created this public gist to help you out! https://gist.github.com/djoos/00793b91835eec98e4c6 - please do replace the "__"s by "/"s, as those are not allowed in gist file paths...

I've tested it so it should work like a treat! Let me know how it goes, ok?

Thanks in advance for your feedback! David

Danny-P commented 10 years ago

Hi David,

I did a very very stupid mistake... I was testing in production environment and didn't clean up the cache. Therefore the new configuration wasn't loaded and my class wasn't used.

Now it works fine and I will go on with the step-by-step guide.

Best regards Daniel

djoos commented 10 years ago

Hi Danny-P,

thanks for your reply, glad you found the root of the issue though...

Looking forward hearing more on the step-by-step guide, thanks in advance! David

Danny-P commented 10 years ago

Hi!

I reactivated my old blog and started it from scratch. I've published the promised step-by-step guide there. You can find it here: http://daniel-pomrehn.de/?p=12

Can you have a look at it, if I used something wrong from your Bundle?

Feel free, to copy this to the documentation or simple link to it. I hope this helps. If I should change anything or write more, please let me know!

Thank you very much for this great bundle and your support getting it to work!

Best regards Daniel

djoos commented 10 years ago

Hi Daniel,

thanks for your write-up!

"Your users will have to use their password, as it is stored in the database, not as they choose it when registering." That is true for the setup in your article, but it doesn't need to be so... It is possible to allow (in this case FOS)user calls with their password rather than the hashed one in the db, by making sure you use the FOSUser password encoder either via changing the encoder settings so they are the same as the algorithm used by FOSUser or using the encoder class as a custom authentication_encoder_class. Be aware that in that case you do want the salt to be retrieved from the user and you'll probably don't want to override getSalt returning "".

Let me know if that makes sense - alternatively I'll try and deepen out this possibility, as it is a frequently recurring one recently :-)

Thanks in advance for your feedback! David

Danny-P commented 10 years ago

Hi David,

thank you very much for your feedback!

Not using the encoding password, stored in the DB, will be nice! I tried different steps, to get it working, but unfortunantelly I had some trouble. My encoder settings are the same, I think. Both uses SHA1. I think using an oen authentication_encoder_class will be good. But I didn't get it working :-( Can you give me a hint, how this must look like? Never worked with encoders before...

Thank you in advance!

Best regards Daniel

djoos commented 10 years ago

Hi Daniel,

sure!

When specifying the following (mentioned in the FOSUserBundle docs) encoder for a FOSUserBundle User in app/config/security.yml:

security:
    encoders:
        FOS\UserBundle\Model\UserInterface:
            algorithm: sha512
            encode_as_base64: false
            iterations: 1

...you'll have to make sure to make use of the same settings for the WSSE secured firewall, also in app/config/security.yml:

firewalls:
    wsse_secured:
        #...
        wsse:
            #...
            encoder:
                algorithm: sha512
                encodeHashAsBase64: false
                iterations: 1

When these match up, the same encoding used for storing your FOSUserBundle User's passwords in the db will be used to generate a match to check the digest upon WSSE Authentication. When generating the X-WSSE header, you can then use something along these lines (~ https://github.com/escapestudios/EscapeAPIClientBundle/blob/master/REST/Request.php) to generate the digest on your client:

    $created = gmdate(DATE_ISO8601);
    $nonce = uniqid();

    $encoder = new MessageDigestPasswordEncoder(
        "sha512", /* algorithm */
        false, /* encode_as_base64 */
        $this->get_iterations() /* iterations */
    );

    $digest = $encoder->encodePassword(
        sprintf(
            '%s%s%s',
            base64_decode($nonce),
            $created,
            $this->get_secret() /* user's plaintext password */
        ),
        $this->get_salt() /* user's salt - this could be retrieved via a public - or even better: "system" WSSE-secured API call */
    );

Let me know how it goes! David

djoos commented 10 years ago

In this case, make sure getSalt() is not overridden, as you'll need it to be retrieved from the db rather than returning "".

Hope this helps!

djoos commented 10 years ago

Hi @Danny-P,

how are you getting on? Let me know if you need a hand!

Kind regards, David

djoos commented 10 years ago

I'll close this issue for now, but don't hesitate to provide extra feedback via additional comments! David

Danny-P commented 10 years ago

Hi @djoos!

I didn't find time, to work on it any more. But now I get on, to do the next steps :-) I have some problems understanding your last reply.

You wrote, that the password, which is stored in the DB won't be used. But in your solution the client generated the user's encoded password and sends it to the webservice. Therefore the secured webservice needs to get the encoded password.

As you described it, I would do it, too. In your solution the user will need to use the encoded password for calling the service. The encoded password is generated by the client, the user uses. He don't see, that the encoded password is used for calling the webservice. That's the same, as I would to it in my client (Android app). The user will enter his (not encoded) password in the app, Than the app generated the encoded password and calls the webservice with it.

Or did I missunderstood you?

Best regards! Daniel

djoos commented 10 years ago

Hi Daniel,

yes, both approaches end up sending the encoded password to the webservice. The only difference in my approach being you have the "how to encode the user's plain text password"-logic on your client. In the end we're both talking about the same :-)

David