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

Using FOSUserBundle as Provider #31

Closed Fraktl closed 10 years ago

Fraktl commented 10 years ago

Hi,

I have completed programming an API and I'm looking into securing it using WSSE. I'm having some trouble getting authenticated to the secured parts of the api.

My configuration is as follows:

security.yml

security:
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: ROLE_ADMIN

    firewalls:
       wsse_secured:
          pattern:   ^/api/.*
          wsse:
            lifetime: 300 #lifetime of nonce
            realm: "Secured API" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate)
            profile: "UsernameToken" #WSSE profile (WWW-Authenticate)
            encoder: #digest algorithm
                algorithm: sha512
          anonymous: true
          provider: fos_userbundle

    access_control:
            - { path: ^/api.*, role: ROLE_USER }
            - { path: ^/security.*, role: IS_AUTHENTICATED_ANONYMOUSLY }

config.yml

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

SecurityController.php

/**
     * WSSE Token generation
     *
     * @Rest\View
     *
     * @return FOSView
     * @throws AccessDeniedException
     */
    public function postTokenCreateAction()
    {

        $view = FOSView::create();
        $request = $this->getRequest();

        $username = $request->get('_username');
        $password = $request->get('_password');

        //$csrfToken = $this->container->get('form.csrf_provider')->generateCsrfToken('authenticate');
        //$data = array('csrf_token' => $csrfToken,);

        $um = $this->get('fos_user.user_manager');
        $user = $um->findUserByUsernameOrEmail($username);

        if (!$user instanceof User) {
            throw new AccessDeniedException("Wrong user");
        }

        $created = date('c');
        $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
        $nonceHigh = base64_encode($nonce);
        $passwordDigest = base64_encode(sha1($nonce . $created . $password . "{".$user->getSalt()."}", true));
        $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
        $view->setHeader("Authorization", 'WSSE profile="UsernameToken"');
        $view->setHeader("X-WSSE", "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"");
        $data = array('WSSE' => $header);
        $view->setStatusCode(200)->setData($data);
        return $view;
    }

The tokens are getting generated by doing POST /security/create_token.json?username=myusername&password="plaintextpassword" over SSL But when I use that token with a X-WSSE header to go to GET /api/me I keep getting a 401 Not Authorized.

Any ideas? Did I misinterpreted the documentation?

Fraktl commented 10 years ago

@escapestudios I think I have found the solution.

For reference for other people that might find this topic. The solution is this:

security.yml

security:
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: ROLE_ADMIN

    firewalls:
       wsse_secured:
          pattern:   ^/api/.*
          wsse:
            lifetime: 300 #lifetime of nonce
            realm: "Secured API" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate)
            profile: "UsernameToken" #WSSE profile (WWW-Authenticate)
            encoder: #digest algorithm
                algorithm: sha512
                encodeHashAsBase64: true
                iterations: 1
          anonymous: true
          provider: fos_userbundle

    access_control:
            - { path: ^/api.*, role: ROLE_USER }
            - { path: ^/security.*, role: IS_AUTHENTICATED_ANONYMOUSLY }

Then overload the validateDigest from the Escapist provider to your own provider (and set this class as the provider in config.yml

Replace // validate secret with

....
//validate secret
        $expected = $this->getEncoder()->encodePassword(
            sprintf(
                '%s%s%s',
                base64_decode($nonce),
                $created,
                $secret
            ),
            $user->getSalt()
        );

....

Especially the $user->getSalt() is important.

Then in your SecurityController change the passwordDigest generation to a SHA512 hash:

...
        $created = date('c');
        $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
        $nonceHigh = base64_encode($nonce);
        $container = $this->get('service_container');
        $iterations = $container->getParameter('wsse_iterations');
        $salted = $nonce . $created . $user->getPassword() . "{" . $user->getSalt() . "}";
        $passwordDigest = hash('sha512', $salted, true);
        for ($i = 1; $i < $iterations; $i++) {
            $passwordDigest = hash('sha512', $passwordDigest . $salted, true);
        }
        $passwordDigest = base64_encode($passwordDigest);
        $header = "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
        $view->setHeader("Authorization", 'WSSE profile="UsernameToken"');
        $view->setHeader(
            "X-WSSE",
            "UsernameToken Username=\"{$username}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\""
        );
        $data = array('WSSE' => $header);

...

I hope this helps for anyone looking to integrate FOSUserbundle with salt + SHA512 passwords and WSSE to secure API's

Fraktl commented 10 years ago

After some more testing I found a bug in the above code. Since I'm never testing the password that I'm using in the passwordDigest a user can login with an incorrect password ($user->getPassword()) is used. Right now I'm stuck at this point but help is more than welcome.

djoos commented 10 years ago

Hi @Fraktl,

unfortunately we don't use the FOSUserBundle, but the "encoder of choice" changes made to the WSSEAuthenticationBundle a while ago would mean that you should be able to use the bundle with FOSUserBundle as well - there shouldn't be any need to overload any methods of this bundle to make it work...

I'll copy in @peschee who initially opened a related issue (#20) - he might be able to help you out some more on the short term. In the longer run adding more documentation, specifically about the use of this bundle with the FOSUserBundle is definitely on my list of things to do...

Hope this helps!

Kind regards, David

Fraktl commented 10 years ago

@djoos

The problem is that you need the salt, the encoder type of choice isn't enough. Adding this to my SecurityController.php will generate a SHA512 hashed password with the same original salt as stored in the database.

$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$enc_password = $encoder->encodePassword($password, $user->getSalt());

This works but only if I add this to your validateDigest method

$expected = $this->getEncoder()->encodePassword(
            sprintf(
                '%s%s%s',
                base64_decode($nonce),
                $created,
                $secret
            ),
            $user->getSalt()
        );

So far so great, now looking into https://github.com/FlyersWeb/angular-symfony to make it work in the frontend too.

djoos commented 10 years ago

Hi @Fraktl,

ok, got it!

Please check out 0454e35 - these minor changes will allow for a smoother integration... Thanks in advance for your feedback!

Kind regards, David

djoos commented 10 years ago

Continuing the conversation here on the FOSUser + WSSE Auth implementation issues also mentioned in #32. It would be great if we could get this solved together and documented...

Thanks in advance for your help guys!

Kind regards, David

djoos commented 10 years ago

Mea culpa, I got carried away with @Fraktl's suggestion...

@Fraktl: wouldn't it be better to ther $user->getPassword() (=plain text pwd, encoded + salted) and send this over in the digest which is a then sha1 / sha512 / ... without a salt? If you insist of salting the digest, I've kept the protected getSalt() method in the Provider so that you could still override it and do something with that if really needed. Hope this helps...

Have a great weekend!

Kind regards, David

wiistriker commented 10 years ago

I use https://github.com/davedevelopment/guzzle-wsse-auth-plugin to interact with my secured API based on symfony2 fw. Did i understand it right that after this commit we need to provide Salt to http client for properly generate digest? Isn't a security breach?

djoos commented 10 years ago

Hi @wiistriker,

there's no need to provide a salt to the client - only if you want to... What is your particular use-case?

Thanks in advance for your feedback!

Kind regards, David

wiistriker commented 10 years ago

@djoos I don't want to pass salt to client, i think it's security breach. But it seems salt now required after this merge to generate right digest on client side. I just notice that library https://github.com/davedevelopment/guzzle-wsse-auth-plugin stop working after this merge and after some digging i found that now in validateDigest function salt is required. So salt is required to generate digest on client now, isnt it?

p.s. now i redefine method getSalt() inside WSSE Provider and return "" (empty string) for user salts, so now it works as expected. i still not sure that this commit doint it right

djoos commented 10 years ago

Hi,

in your case as you don't want the user's getSalt() to be used for WSSE Auth: you can specify a custom authentication class for the provider-class that overrides the getSecret() method of the provider returning "".

RE: security breach The only way to create a digest for a user for WSSE Auth is to either use the password as stored in the db (salted) in the client -or- use the plain text and salt it upon digest creation. The bundle is able to cater for both of these setups, it just depends on the developer's implementation. The best way forward is to have a separate key and secret for the user for API-purposes and then override getSecret() and getSalt() in the provider to call on the relevant user methods (eg. $user->getAPIKey() and $user->getAPISecret()). Let me know what you think!

Hope this helps! David