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

Setup EscapeWSSEAuthenticationBundle and FOSUserBundle #35

Closed Lausebengel closed 10 years ago

Lausebengel commented 10 years ago

Hello,

I'm trying to setup EscapeWSSEAuthenticationBundle and FOSUserBundle to secure my api. Does someone know a good tutorial about this ? I tried it with this info : https://github.com/escapestudios/EscapeWSSEAuthenticationBundle/issues/31 but it is to much fragmented. I didn't get it to work ...

Thanks in advance.

djoos commented 10 years ago

Hi @oliver13,

if I'm not mistaking @timtailor got a working solution:

"I basically used it out of the box."

...

"My android app retrieves the FOS user's salt from a public url, encodes his plain text password with his salt (sha512 and 5 iterations), then passes the WSSE Header (like http://www.teria.com/~koseki/tools/wssegen/) to the API url which is EscapeWSSE protected."

(mentioned in issue #32)

Please do let me know how it goes, it would be great to get some documentation together so we can include this in the bundle!

Thanks in advance for your feedback!

Kind regards, David

timtailor commented 10 years ago

Hi @oliver13,

in my project it worked just by adding WSSE as a firewall and having the FOSUserBundle as a provider. The important logic happens mainly on the client side (in my case an android app).

The client must create a WSSE header with

To generate the hashed password, you need to replicate the FOSUserBundle's hashing mechanism, which uses SHA512, Base64 over x iterations (please google for the details there, because i don't have them at hand right now).

Input for that algorithm is

The plain password is available via user input (naturally), and to get the user's salt i provided a public API call (without firewall protection).

Before you implement it, you can use the form at http://www.teria.com/~koseki/tools/wssegen/ to test the WSSE integration (input the hashed user password as in the fos_user table, and then copy the output into a REST testing tool as provided by many IDEs).

@djoos: I also recommend to put this into the documentation, as many developers might want to use WSSE for their Symfony / FOSUserBundle projects.

djoos commented 10 years ago

Hi @timtailor,

thanks for your reply!

I definitely think having a write up on how to use WSSE+FOSUser in the documentation would be great. Would any of you be willing to help out and send me a PR?

Thanks in advance!

Kind regards, David

P.S. Rather than using a public/unprotected API to get a user's salt: how about using a "system API"-call (as in eg. "android" user with a plaintext pwd)? In that way it is not completely open and once a user's salt has been retrieved, actual "user API"-calls can be made.

Lausebengel commented 10 years ago

Hello @djoos and @timtailor,

thanks for your support ! I think I get it step by step. Until now I have installed the bundles and it seams that they work.

But if I request the API with a REST client in Firefox I get "Status Code: 401 Unauthorized" ... So it is still something wrong.

Here is what I have as configuration, etc.:

REST stuff in config.yml

fos_rest:
    format_listener:
        rules:
            - { priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
    view:
        view_response_listener: true

nelmio_api_doc: ~

fos_user:
    db_driver: orm
    firewall_name: wsse_secured
    user_class: [...]\UserBundle\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 

The 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

    firewalls:
        wsse_secured:
            pattern:   ^/
            wsse:
                lifetime: 300 #lifetime of nonce
                realm: "Secured API" 
                profile: "UsernameToken"
                encoder: #digest algorithm
                    algorithm: sha512
                    encodeHashAsBase64: true
                    iterations: 1
            anonymous: true
            provider: fos_userbundle
            stateless: true

    access_control:
        - { path: ^/, role: ROLE_USER }

That is what I send via the REST Client:

X-WSSE: UsernameToken Username="test123", PasswordDigest="74k64qaoSUYaDO+9TjI/vq2Dwz6GxseD7AAC3OpfjrqwpsrcjOO8Pn7DwKZK5LfxHSS+oPdSPY5FP03LFMWxew==", Nonce="NmY5YTQ5NjJiMjBhMGM0Yw==", Created="2014-03-18T13:57:13Z"

Thank you in advance for further help !

Lausebengel commented 10 years ago

Ok, the request of my REST client seams to be faulty. The listener class can't find the string "X-WSSE" in my request header (Line 42):

if(!$request->headers->has('X-WSSE'))

I use restclient.net in firefox. What do I have to enter here as header ?

Thanks in advance

Update: Using another rest client solves this : http://code.google.com/p/rest-client/ . But still getting "Status Code: 401 Unauthorized" ...

djoos commented 10 years ago

Hi @oliver13,

could you have a look at the validateDigest-method in Security/Core/Authentication/Provider/Provider.php?

Note to myself: adding a verbose option for debugging purposes to the bundle wouldn't actually be a bad thing, would it? :-)

Let me know how it goes!

Kind regards, David

Lausebengel commented 10 years ago

Hi David, thanks for your reply. I have looked into Provider class. It seams that there is a problem with the lifetime check. I the logfile I found : app.DEBUG: Token has expired....

Update: Can this be caused by the missing salt in Line 73 of Provider ?

djoos commented 10 years ago

"Can this be caused by the missing salt in Line 73 of Provider ?" No, it's just a matter of generating a new header and consuming it within the 300 (seconds), which is the default lifetime for the nonce...

Hope this helps!

Lausebengel commented 10 years ago

Now it seams to work ... uff ... :-)

I had to change the following in the Provider class:

        if(
            $user &&
            $this->validateDigest(
                $token->getAttribute('digest'),
                $token->getAttribute('nonce'),
                $token->getAttribute('created'),
                $this->getSecret($user),
                $this->getSalt($user)
           )
        )
        {
            $authenticatedToken = new Token($user->getRoles());
            $authenticatedToken->setUser($user);
            $authenticatedToken->setAuthenticated(true);

            return $authenticatedToken;
        }

Here I have to change this : $this->getSalt($user)

    protected function getSalt(UserInterface $user)
    {
        return $user->getSalt();
    }

Here I change the parameters and return value.

Thanks for the help !

djoos commented 10 years ago

Hi @oliver13,

it sounds like the getSalt could do with the UserInterface $user parameter by default (letting it return "" by default) and then your can simply extend the provider class by a custom one (see: "Specify custom authentication class(es)" in the README.md) and that would sort it out, right?

Let me know/send me a PR if you want to and then I'll try to get this merged in ASAP!

Kind regards, David

djoos commented 10 years ago

P.S. In that way getSecret() can be overwritten as well if you'd like to make use of an API key for a user rather than the user's password, as well as having a separate salt via getSalt() for the API key than the password. #flexibility

I actually think this might be a useful threat (even though quite lengthy, I admit) for issue #27... Let me know what you think @Fraktl!

djoos commented 10 years ago

@Oliver13, could you give the latest version a go? This should work out of the box for you...

Thanks in advance for your feedback! David

Lausebengel commented 10 years ago

Hi @djoos, yes I get it to work with a custom provider class.Thanks a lot !

broncha commented 10 years ago

This is definitely not the way to do that.

Wsse header should be generated by concating base64_decode(nonce), timestamp and secret.

Here the secret is hashed password as generated by FOSUserBundle OR lets say by security component.

Here getSecret should instead return the hashed password and the expected digest vs sent digest comparison logic should be as it was before.

oliver13's client should use the hashed password (generated by the same algorithm as symfony security component) in place of secret and thats is. We dont need any changes in this bundle and validateDigest to address that!

How we actually use this bundle for securing API call is:

we have a different field called token which is regenerated in each login from mobile app and is passed along to the client. The client uses this token to sign all the request. We just overload the getSecret method which returns the token instead of the actual password.

Likewise, in @oliver13 's case the getSecret method should return the hashed password.

This is not something that needs to be changed in the core. There is support for this already

djoos commented 10 years ago

Hi @broncha,

thanks for getting in touch! I'm not 100% sure I got your point though...

The recent change to the getSalt-method in the provider makes it possible to bring a custom salt into play as well when validating the digest - which it actually already did (even though being a fixed salt of "", an empty string).

It should not introduce any BC issues, but allow for more flexible use: how would the validate digest be able to check a user's (eg. FOSUser) digest when encodePassword doesn't have the ability to get the user's salt to encode the password? Allowing this flexibility now allows for the bundle to support salted passwords (as opposed to non-salted, like eg. the token you mentioned) as well.

Thanks in advance for your feedback!

Kind regards, David

skonsoft commented 9 years ago

Hi,

I got it working by doing these steps:

1) Make a custom Provider with code:

 protected function getSalt(UserInterface $user)
    {
        return null;
    }

No need to get the salt, because it was already used in FOS User saved password

2) In Client side we should implement the same algorithm used by Symfony to hash password which is:

sha512 algorithm with 5000 iterations. Here we need to get The Salt to generate the password. So we need to expose User Salt over the API to be used by the client. Here is the function to generate the hashed password in client side:

// Here we suppose that you have already the User SALT. for example exposed by a public rest service
function hashPassword($password, $salt)
{
    $salted = $password . '{' . $salt . '}';
    $digest = hash('sha512', $salted, true);

    for ($i = 1; $i < 5000; $i++) {
        $digest = hash('sha512', $digest . $salted, true);
    }

    return base64_encode($digest);
}

And this is the wsse_header generator:

function wsse_header($username, $hashedPassword)
{
    $nonce = hash_hmac('sha512', uniqid(null, true), uniqid(), true);
    $created = new DateTime('now', new DateTimezone('UTC'));
    $created = $created->format(DateTime::ISO8601);
    $digest = hash('sha512', $nonce . $created . $hashedPassword, true);

    return sprintf(
            'X-WSSE: UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
            $username, base64_encode($digest), base64_encode($nonce), $created
    );
}

Finally, to use it just call:

wsse_header('someLogin',
                 hashPassword('somePassword', 'someSalt'));

Note: This was a PHP client, for Android, you can take a look here: Android Client Tutorial

3) In security.yml, put:

encoder: #digest algorithm
                    algorithm: sha512
                    encodeHashAsBase64: true
                    iterations: 1 #Here one iteration, because in wsse_header we make only 1 iteration

I hope this help you ! regards Skander

xub commented 8 years ago

gooooooooooood @skonsoft