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:
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.
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.