1
votes

I'm working on an RESTful API on where users are allowed to provide their user information in a HTTP header named x-wsse.

Example: UsernameToken Username="JohnDoe", PasswordDigest="PasswordDigest", Nonce="Nonce", Created="CreatedTimestamp"

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

$secret is the hashed, salted password.

This header gets "caught" by a listener which "parses" it and validates it.

The code snippet:

    $request = $event->getRequest();

    $wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
    if (!$request->headers->has('x-wsse') || !preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) {
        return;
    }

    $token = new WsseUserToken();
    $token->setUser($matches[1]);

    $token->digest   = $matches[2];
    $token->nonce    = $matches[3];
    $token->created  = $matches[4];

    try {
        $authToken = $this->authenticationManager->authenticate($token);
        $this->tokenStorage->setToken($authToken);

        return;
    } catch (AuthenticationException $failed) {
        // Authentication failed, some stuff will happen here
    }

The authentication manager has a authenticate method:

public function authenticate(TokenInterface $token)
{
    $user = $this->userProvider->loadUserByUsername($token->getUsername());

    if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
        $authenticatedToken = new WsseUserToken($user->getRoles());
        $authenticatedToken->setUser($user);

        return $authenticatedToken;
    }

    throw new AuthenticationException('The WSSE authentication failed.');
}

The validateDigest method does this:

protected function validateDigest($digest, $nonce, $created, $secret)
{
    // Check created time is not in the future
    if (strtotime($created) > time()) {
        return false;
    }

    // Expire timestamp after 5 minutes
    if (time() - strtotime($created) > 300) {
        return false;
    }

    // Validate that the nonce is *not* used in the last 5 minutes
    // if it has, this could be a replay attack
    if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
        throw new NonceExpiredException('Previously used nonce detected');
    }
    // If cache directory does not exist we create it
    if (!is_dir($this->cacheDir)) {
        mkdir($this->cacheDir, 0777, true);
    }
    file_put_contents($this->cacheDir.'/'.$nonce, time());

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

    if ($digest === $expected) {
        $valid = true;
    } else {
        $valid = false;
    }

    return $valid;
}

All this is covered in "The Cookbook" of Symfony2. Code and info: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html

As advised, all the user passwords are hashed with Bcrypt and - as advised (see http://php.net/password_hash) - I let PHP generate a hash for each password.

I'm testing with Chrome REST console and my WSSE header is generated by: http://www.teria.com/~koseki/tools/wssegen/.

Everything goes well, until I I"m validating the PasswordDigest. I'm getting a mismatch problem. This is caused by an "invalid salt revision".

Since PHP generates my salt, and I can not find it stored anywhere (user table only has columns id, username, password, email, is_active), I can not "salt" the password myself at the "right" moment.

How should I deal with this problem?

I've been thinking of:

  1. Sending the plaintext username and password through a HTTPS request to a Symfony Controller which then returns the hashed password. This hashed password can be stored then to be reused in the next requests with "fresh" x-wsse header.

  2. Sending the plaintext username and password through a HTTPS request to a Symfony Controller which then returns a valid x-wsse header which can be used for only one request, because the nonce is only allowed to be used once.

I'm not very happy with the solutions above and would like to know what you guys think... PHP is generating a salt itself, but where does it store that? Can I create a JS function which does the same / complies with http://php.net/password_hash? The explanation of using wsse with Symfony 2 on http://obtao.com/blog/2013/06/configure-wsse-on-symfony-with-fosrestbundle/ says salts may be public as long as they are unique for each user.

1

1 Answers

1
votes

We implement wsse authentication to our symfony REST endpoints. But we dont use the password field. We added a new field called token to the user entity. The REST api is consumed by mobile app. When users login to the app using either facebook OR their username and password, the credentials/fb access token are sent to symfony app over https, the server responds with the user profile and a fresh token ( token is regenerated on every login ). We are using this bundle tough. https://github.com/escapestudios/EscapeWSSEAuthenticationBundle

So when you are validating the digest, you would build the expected digest using the user's token.

So, this is similar to the step 1 you suggested, just that you don,t give away password and salt. You'll have the flexibility to change the user's token without changing the password.

In any case, the client will need to enter their username and password in order to actually authenticate first and retrieve the token. Even if you implement oAuth, the user will need to first authenticate to your service provider in order to authorize your app.