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

Previously used nonce detected. #40

Closed bzzzm closed 10 years ago

bzzzm commented 10 years ago

Hello,

I have a problem in getting a wsse authentication running using your bundle (thx!) and fosuserbundle. I'm able to login (i can see in the log that the user is granted access.. also visible in the profiler security tab, user last_login gets updated in db etc etc).

I also created a Handler that listens for onAuthenticationSuccess and returns the contents of X-WSSE header (i can't remember where i saw a similar script):

    // Acme\AuthBundle\Security\Authentication\Handler;

    public function onAuthenticationSuccess(Request $request, TokenInterface $token) {

        $user = $this->security->getToken()->getUser();

        $created = date('c');
        $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
        $nonceHigh = base64_encode($nonce);
        $passwordDigest = base64_encode(sha1($nonceHigh . $created . $user->getPassword() . "{".$user->getSalt()."}", true));
        $header = "UsernameToken Username=\"{$user->getUsername()}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
        return new Response($header);
    }

After that, I take the header, paste it in the Chrome Rest Console... but I get a 401. Here is the log of such request:

[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "Symfony\Bundle\FrameworkBundle\EventListener\SessionListener::onKernelRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\HttpKernel\EventListener\FragmentListener::onKernelRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelRequest". [] []
[2014-04-15 14:57:36] request.INFO: Matched route "mma_backend_company_get" (parameters: "_controller": "MMAHotel\BackendBundle\Controller\CompanyController::getAction", "_format": "json", "uid": "1234x", "_route": "mma_backend_company_get") [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\HttpKernel\EventListener\LocaleListener::onKernelRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "FOS\RestBundle\EventListener\BodyListener::onKernelRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.request" to listener "Symfony\Component\Security\Http\Firewall::onKernelRequest". [] []
[2014-04-15 14:57:36] doctrine.DEBUG: SELECT t0.username AS username1, t0.username_canonical AS username_canonical2, t0.email AS email3, t0.email_canonical AS email_canonical4, t0.enabled AS enabled5, t0.salt AS salt6, t0.password AS password7, t0.last_login AS last_login8, t0.locked AS locked9, t0.expired AS expired10, t0.expires_at AS expires_at11, t0.confirmation_token AS confirmation_token12, t0.password_requested_at AS password_requested_at13, t0.roles AS roles14, t0.credentials_expired AS credentials_expired15, t0.credentials_expire_at AS credentials_expire_at16, t0.id AS id17, t0.uid AS uid18, t0.firstname AS firstname19, t0.lastname AS lastname20, t0.fullname AS fullname21, t0.phone AS phone22, t0.skype AS skype23, t0.handle AS handle24, t0.company_id AS company_id25 FROM User t0 WHERE t0.username_canonical = ? LIMIT 1 ["mihai"] []
[2014-04-15 14:57:36] app.DEBUG: Previously used nonce detected. [] []
[2014-04-15 14:57:36] event.DEBUG: Listener "Symfony\Component\Security\Http\Firewall::onKernelRequest" stopped propagation of the event "kernel.request". [] []
[2014-04-15 14:57:36] event.DEBUG: Listener "Symfony\Bundle\AsseticBundle\EventListener\RequestListener::onKernelRequest" was not called for event "kernel.request". [] []
[2014-04-15 14:57:36] event.DEBUG: Listener "Symfony\Component\HttpKernel\EventListener\ErrorsLoggerListener::injectLogger" was not called for event "kernel.request". [] []
[2014-04-15 14:57:36] event.DEBUG: Listener "Symfony\Component\HttpKernel\EventListener\ErrorsLoggerListener::injectLogger" was not called for event "kernel.request". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\ResponseListener::onKernelResponse". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\Security\Http\RememberMe\ResponseListener::onKernelResponse". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.response" to listener "Sensio\Bundle\FrameworkExtraBundle\EventListener\HttpCacheListener::onKernelResponse". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelResponse". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.response" to listener "Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener::onKernelResponse". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.response" to listener "Symfony\Component\HttpKernel\EventListener\StreamedResponseListener::onKernelResponse". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\LocaleListener::onKernelFinishRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.finish_request" to listener "Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelFinishRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.finish_request" to listener "Symfony\Component\Security\Http\Firewall::onKernelFinishRequest". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.terminate" to listener "Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate". [] []
[2014-04-15 14:57:36] event.DEBUG: Notified event "kernel.terminate" to listener "Symfony\Component\HttpKernel\EventListener\ProfilerListener::onKernelTerminate". [] []

I also tried generating the header using teria.com generator (got the salt+pass for db), but the problem persists.

Here is my security.yml

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username

    firewalls:
        wsse_secured:
            pattern:    ^/back/.*
            wsse:
                lifetime: 300 
                realm: "Acme API"
                profile: "UsernameToken"
                encoder: #digest algorithm
                    algorithm: sha512
                    encodeHashAsBase64: true
                    iterations: 1
            anonymous : true
            stateless: true

        wsse_auth:
            pattern:    ^/wsse
            form_login:
                provider: fos_userbundle
                login_path: acme_auth_wsse_login
                check_path: acme_auth_wsse_login_check
                success_handler: acme.handler.wsse_login_success
                require_previous_session: false       
            anonymous: true

Does anyone have any idea what I'm missing?

Thank you

djoos commented 10 years ago

Hi @bzzzm,

I spot that you use sha1 to generate your digest on the client side, but expect (see firewalls-section in security.yml) a sha512 with 1 iteration on the server side.

Make use of the encoding used by FOSUserBundle on the server side (security.encoders + wsse.encoder settings) and use the same to generate the digest on the client side as well...

Hope this helps!

Kind regards, David

bzzzm commented 10 years ago

Hey @djoos, Thanks for your comment. I think I solved the encoding part by generating the digest using security.encoder_factory, but I still have the same problem. Here is the Handler now:

    public function __construct(SecurityContext $security, EncoderFactory $factory) {
        $this->security = $security;
        $this->factory = $factory;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token) {

        $user = $this->security->getToken()->getUser();        
        $encoder = $this->factory->getEncoder($user);

        $created = date('c');
        $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
        $nonceHigh = base64_encode($nonce);
        $passwordDigest = base64_encode(
                $encoder->encodePassword(
                        $nonceHigh . 
                        $created . 
                        $user->getPassword(), 
                        $user->getSalt()));
        $header = "UsernameToken Username=\"{$user->getUsername()}\", PasswordDigest=\"{$passwordDigest}\", Nonce=\"{$nonceHigh}\", Created=\"{$created}\"";
        return new Response($header);

    }
}

I also changed security.firewalls.wsse_secured.wsse.interations from 1 to 5000:

// print_r($this->factory->getEncoder($user));
Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder Object
(
    [algorithm:Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder:private] => sha512
    [encodeHashAsBase64:Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder:private] => 1
    [iterations:Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder:private] => 5000
)

I also cleared cache after every change i made in the code. Thank you.

djoos commented 10 years ago

Hi @bzzzm,

I'm not 100% sure what you're trying to achieve returning the WSSE header as a response on authentication success to be honest. It might be worth isolating the WSSE side of things by creating a simple controller/command that posts (via curl) to your WSSE secured API...

Let me know how it goes!

Kind regards, David

bzzzm commented 10 years ago

Hello @djoos,

It's just while testing, I wont have such handler after I solve this issue. I'm doing that for faster copy/paste. The problem is not during login_check, but when I try to access some other controller in /back with the wsse header (the one returned by the handle of one generated on teria.com); that's when I get Previously used nonce detected.

Thank you!

djoos commented 10 years ago

Hi @bzzzm,

you can't reuse nonces within the set lifetime: you'll need a new one every call... You coild set the lifetime to 0, but I would strongly advise against that.

Hope this helps! David

bzzzm commented 10 years ago

Hi @djoos, I solved it after all. In the Handler I had this:

        $passwordDigest = base64_encode(
                $encoder->encodePassword(
                        $nonceHigh . 
                        $created . 
                        $user->getPassword(), 
                        $user->getSalt()));

and should have been this (without base64):

        $passwordDigest = $encoder->encodePassword(
                        $nonce . 
                        $created . 
                        $user->getPassword(), 
                        $user->getSalt());

This is because the encoder will automatically do a base64_encode.

[encodeHashAsBase64:Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder:private] => 1

Also, the nonce should not be base64 encrypted in the passwordDigest.

Thank you for you help!

djoos commented 10 years ago

You're welcome, great to hear you're up and running!

Kind regards, David

skyjooy commented 7 years ago

Hi @djoos

I want to linked WSSE Auth with my Rest API (sf2 with fosrestbundle, fosuserbundle & nelmio) I generate my token via Nelmio doc and i have config your bundle but every time i try my log in _profiler write

this:INFO - Matched route "get_user_role". Context: {"route_parameters":{"_controller":"AppBundle\Controller\UserRoleRestController::getRoleAction","_format":"json","slug":"cpasche","_route":"get_user_role"},"request_uri":"http://bend.example.dev.com/web/app_dev.php/api/v1/users/username/role.json"} WARNING - WSSE authentication failed.

My TokenRestController ` class TokenRestController extends FOSRestController {

/**
 * Create a Token from the submitted data.<br/>
 *
 * @ApiDoc(
 *   resource = true,
 *   description = "Creates a new token from the submitted data.",
 *   statusCodes = {
 *     200 = "Returned when successful",
 *     400 = "Returned when the form has errors"
 *   }
 * )
 *
 * @param ParamFetcher $paramFetcher Paramfetcher
 *
 * @RequestParam(name="username", nullable=false, strict=true, description="username.")
 * @RequestParam(name="password", nullable=false, strict=true, description="password.")
 * @RequestParam(name="salt", nullable=false, strict=true, description="salt.")
 *
 * @return View
 */
public function postTokenAction(ParamFetcher $paramFetcher)
{

    $view = View::create();

    $userManager = $this->get('fos_user.user_manager');
    $user = $userManager->findUserByUsername($paramFetcher->get('username'));

    if (!$user instanceof User) {
        $view->setStatusCode(404)->setData("Data received succesfully but with errors.");

        return $view;
    }

    $factory = $this->get('security.encoder_factory');

    $encoder = $factory->getEncoder($user);
    $password = $encoder->encodePassword($paramFetcher->get('password'), $paramFetcher->get('salt'));

    $header = $this->generateToken($paramFetcher->get('username'), $password);
    $data = array('X-WSSE' => $header);
    $view->setHeader("Authorization", 'WSSE profile="UsernameToken"');
    $view->setHeader("X-WSSE", $header);
    $view->setStatusCode(200)->setData($data);

    return $view;
}

/**
 * Generate token for username given
 *
 * @param  string $username username
 * @param  string $password password with salt included
 * @return string
 */
private function generateToken($username, $password)
{
    $nonce = md5(rand());
    $created = gmdate(DATE_ISO8601);
    $digest = base64_encode(sha1($nonce.$created.$password, true));
    $b64nonce = base64_encode($nonce);
    $token = 'UsernameToken Username="'.$username.'", PasswordDigest="'.$digest.'", Nonce="'.$b64nonce.'", Created="'.$created.'"';
   /* $created = date('c');
    $nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
    $nonceSixtyFour = base64_encode($nonce);
    $passwordDigest = base64_encode(sha1($nonce . $created . $password, true));

    $token = sprintf(
        'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
        $username,
        $passwordDigest,
        $nonceSixtyFour,
        $created
    ); */

    return $token;
}

} `

AND my config security.yml

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

role_hierarchy:
    ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_API, ROLE_ALLOWED_TO_SWITCH]

providers:
    fos_userbundle:
        id: fos_user.user_provider.username_email

firewalls:

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

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

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

`

I had after when i try this with nelmio doc headers

Authorization:"WSSE profile="UsernameToken" X-WSSE: UsernameToken Username ....

Do you have any ideas to help me why i have an error 401 Unauthorized ??

Can you help me ? Thx a lot !

Date: Mon, 19 Dec 2016 20:41:45 GMT X-Debug-Token-Link: /web/app_dev.php/_profiler/abca90 WWW-Authenticate: WSSE realm="Secured with WSSE", profile="UsernameToken" Server: Apache/2.4.9 (Win64) PHP/5.5.12 X-Powered-By: PHP/5.5.12 Allow: GET Content-Type: application/json Access-Control-Allow-Origin: * Cache-Control: no-cache Connection: Keep-Alive Vary: Authorization Content-Length: 0 X-Debug-Token: abca90 Keep-Alive: timeout=5, max=100