Reputation: 12717
I'm trying to implement WSSE authentication on a Web Services API. I followed this tutorial on symfony official site. I send requests with SoapUI, with security headers :
POST http://dev.sellerphp.com/app_dev.php/wsapi/soap/ordersfollowup HTTP/1.0
Accept-Encoding: gzip,deflate
Content-Type: text/xml;charset=UTF-8
SOAPAction: "http://dev.sellerphp.com/app_dev.php/wsapi/soap/ordersfollowup"
Content-Length: 1520
Host: dev.sellerphp.com
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5)
<soapenv:Envelope xmlns:ord="https://dev.sellerphp.com/app_dev.php/wsapi/soap/ordersfollowup?wsdl" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsap="http://wsapi.sellerphp.com/">
<soapenv:Header>
<wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsu:Timestamp wsu:Id="TS-14">
<wsu:Created>2012-10-10T09:36:10Z</wsu:Created>
<wsu:Expires>2012-10-10T09:52:50Z</wsu:Expires>
</wsu:Timestamp>
<wsse:UsernameToken wsu:Id="UsernameToken-13">
<wsse:Username>myUsername</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">hb5AJ7CT2tMSQymSsxwvc8J/xoI=</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">PEo//Z/yGF5/HCW6rkCuwQ==</wsse:Nonce>
<wsu:Created>2012-10-10T09:36:10.485Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<wsap:setTracking>
<input>
<!--1 to 10 repetitions:-->
<OrderItemTracking>
<order-item-id>?</order-item-id>
<tracking-number>?</tracking-number>
</OrderItemTracking>
</input>
</wsap:setTracking>
</soapenv:Body>
</soapenv:Envelope>
On the server-side, the WsseListener class doesn't read SOAP headers... The example only involves that headers are defined on the Request level, but they are only embedded in SOAP envelope :
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches))
{
return;
}
// ########### It never reached this line, never found any headers...
var_dump($matches);exit;
$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->securityContext->setToken($authToken);
}
catch (AuthenticationException $failed)
{
// ... you might log something here
// To deny the authentication clear the token. This will redirect to the login page.
// $this->securityContext->setToken(null);
// return;
// Deny authentication with a '403 Forbidden' HTTP response
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
}
I don't know how to read soap:Header headers from the listener. Can you help me ?
Upvotes: 0
Views: 2002
Reputation: 12717
I finally admitted that samples provided by Symfony implies that WSSE headers are brought by HTTP layer, not by the SOAP envelope. So I made a workaround to read SOAP headers :
The WsseListener handle method :
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$decodedHeaders = WsseHeadersDecoder::getHeaders($request);
if (!$decodedHeaders || !is_array($decodedHeaders))
{
return;
}
$token = new WsseUserToken();
$token->setUser($decodedHeaders['username']);
$token->digest = $decodedHeaders['passwordDigest'];
$token->nonce = $decodedHeaders['nonce'];
$token->created = $decodedHeaders['created'];
// ...
}
WsseHeadersDecoder::getHeaders method :
static public function getHeaders(Request $request)
{
//HTTP headers (as described here : http://symfony.com/doc/2.0/cookbook/security/custom_authentication_provider.html#the-listener
if ($request->headers->has('x-wsse'))
{
$wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
if (1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches))
{
return false;
}
else
{
$username = $matches[1];
$passwordDigest = $matches[2];
$nonce = $matches[3];
$created = $matches[4];
}
}
//Classic SOAP headers
else
{
//PéCé: Clear XML namespace prefixes to handle with SimpleXML
$decodedRequest = preg_replace("/(<\/?)([-\w]+):([^>]*>)/", "$1$3", $request->getContent());
$xmlRequest = simplexml_load_string($decodedRequest);
if (
!isset($xmlRequest->Header->Security->UsernameToken->Username)
|| !isset($xmlRequest->Header->Security->UsernameToken->Password)
|| !isset($xmlRequest->Header->Security->UsernameToken->Nonce)
|| !isset($xmlRequest->Header->Security->UsernameToken->Created)
)
{
return false;
}
else
{
$username = (string) $xmlRequest->Header->Security->UsernameToken->Username;
$passwordDigest = (string) $xmlRequest->Header->Security->UsernameToken->Password;
$nonce = (string) $xmlRequest->Header->Security->UsernameToken->Nonce;
$created = (string) $xmlRequest->Header->Security->UsernameToken->Created;
}
}
return array (
'username' => $username, 'passwordDigest' => $passwordDigest, 'nonce' => $nonce, 'created' => $created
);
}
Upvotes: 2