Reputation: 779
I am trying to create a passwordDigest util which can be used in different environments which are capable of running java byte code.
First of all I create nonce. It is done like so.
public static String buildNonce(){
StringBuffer nonce=new StringBuffer();
String dateTimeString = Long.toString(new Date().getTime());
byte[] nonceByte= dateTimeString.getBytes();
return Base64.encode(nonceByte);
}
Once I have nonce, I build password digest.
public static String buildPasswordDigest(String userName, String password, String nonce, String dateTime){
MessageDigest sha1;
String passwordDigest=null;
try {
sha1= MessageDigest.getInstance("SHA-1");
byte[] hash = MessageDigest.getInstance("SHA-1").digest(password.getBytes("UTF-8"));
sha1.update(nonce.getBytes("UTF-8"));
sha1.update(dateTime.getBytes("UTF-8"));
passwordDigest = new String(Base64.encode(sha1.digest(hash)));
sha1.reset();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return passwordDigest;
In order to test that everything works correctly. I have created a test web service using CXF 2.7. I have manually created SOAP Envelope to test authentication. The envelope looks like this.
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ws="http://ws.mytest.org/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance/">
<soapenv:Header>
<wsse:Security soapenv: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">
<wsse:UsernameToken wsu:Id="UsernameToken-2">
<wsse:Username>TEST_USER</wsse:Username>
<wsse:Password
Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">UZsDSW/vANu6fHg4rAHo2OwsF9s=</wsse:Password
<wsse:Nonce>MTQwMTMwMDQzNjA3OA==</wsse:Nonce>
<wsu:Created>2014-05-28T18:07:16.087Z</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<ws:record>
<val1>1</val1>
<val2>Some Text</val2>
</ws:record>
</soapenv:Body>
</soapenv:Envelope>
When I send the envelope using SOAP UI. I get the following authentication error.
WARNING: Interceptor for {http://ws.mytest.org/}TestService has thrown exception, unwinding now
org.apache.cxf.binding.soap.SoapFault: The security token could not be authenticated or authorized
at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.createSoapFault(WSS4JInInterceptor.java:788)
at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage(WSS4JInInterceptor.java:336)
at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage(WSS4JInInterceptor.java:95)
at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:272)
at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:239)
at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:248)
at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:222)
at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:153)
at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:167)
at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:286)
at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:206)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:646)
at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:262)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:313)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
Caused by: org.apache.ws.security.WSSecurityException: The security token could not be authenticated or authorized
at org.apache.ws.security.validate.UsernameTokenValidator.verifyDigestPassword(UsernameTokenValidator.java:199)
at org.apache.ws.security.validate.UsernameTokenValidator.validate(UsernameTokenValidator.java:97)
at org.apache.ws.security.processor.UsernameTokenProcessor.handleUsernameToken(UsernameTokenProcessor.java:172)
at org.apache.ws.security.processor.UsernameTokenProcessor.handleToken(UsernameTokenProcessor.java:67)
at org.apache.ws.security.WSSecurityEngine.processSecurityHeader(WSSecurityEngine.java:396)
at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage(WSS4JInInterceptor.java:279)
... 31 more
I suspect that I have an issue creating either nonce or password.
Your help is appreciated.
Upvotes: 8
Views: 32627
Reputation: 393
After days and days of searching, Pavel's answer was the only one that worked for me. The only changes I made to his code are 1) Returning the date of token creation, among with nonce and password digest. This is critical to avoid the "expired token" response. 2) returning the whole header block 3) converting the code to Java 1.8.
Here's the modified Pavel's code.
package test;
import javax.xml.datatype.*;
import java.security.*;
import java.time.Instant;
import java.util.Base64;
import java.util.Base64.Encoder;
import static java.lang.System.currentTimeMillis;
import static java.nio.charset.StandardCharsets.UTF_8;
public class Main {
private static final SecureRandom RANDOM;
private static final int NONCE_SIZE_IN_BYTES = 16;
private static final String MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1 = "SHA-1";
private static final String SECURE_RANDOM_ALGORITHM_SHA_1_PRNG = "SHA1PRNG";
static {
try {
RANDOM = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM_SHA_1_PRNG);
RANDOM.setSeed(currentTimeMillis());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws DatatypeConfigurationException {
String passwordMd5 = "4C2d85FBEE80145B4";
generateHeader(passwordMd5);
}
public static void generateHeader(String password) throws DatatypeConfigurationException {
final byte[] nonceBytes = generateNonce();
final XMLGregorianCalendar createdDate = DatatypeFactory.newInstance()
.newXMLGregorianCalendar(Instant.now().toString());
final byte[] passwordDigestBytes = constructPasswordDigest(nonceBytes, createdDate, password);
final Encoder base64Encoder = Base64.getEncoder();
final String nonceBase64Encoded = base64Encoder.encodeToString(nonceBytes);
final String passwordDigestBase64Encoded = base64Encoder.encodeToString(passwordDigestBytes);
//System.out.println(String.format("nonce: [%s], password digest: [%s]", nonceBase64Encoded, passwordDigestBase64Encoded));
StringBuilder sb = new StringBuilder();
sb.append("<soapenv:Header>\n");
sb.append("<wsse:Security xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">\n");
sb.append("<wsse:UsernameToken>");
sb.append("<wsse:Username>thisIsMyUsername</wsse:Username>");
sb.append("<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"+
passwordDigestBase64Encoded+"</wsse:Password>\n");
sb.append("<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004 /01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" +
nonceBase64Encoded+"</wsse:Nonce>\n");
sb.append("<wsu:Created xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" +
createdDate.toString()+"</wsu:Created>\n");
sb.append("</wsse:UsernameToken>\n");
sb.append("</wsse:Security>\n");
sb.append("</soapenv:Header>");
System.out.println(sb.toString());
System.out.flush();
System.exit(0);
}
private static byte[] generateNonce() {
byte[] nonceBytes = new byte[NONCE_SIZE_IN_BYTES];
RANDOM.nextBytes(nonceBytes);
return nonceBytes;
}
private static byte[] constructPasswordDigest(byte[] nonceBytes, XMLGregorianCalendar createdDate,
String password) {
try {
final MessageDigest sha1MessageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1);
sha1MessageDigest.update(nonceBytes);
final String createdDateAsString = createdDate.toString();
sha1MessageDigest.update(createdDateAsString.getBytes(UTF_8));
sha1MessageDigest.update(password.getBytes(UTF_8));
return sha1MessageDigest.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
Upvotes: 2
Reputation: 5142
The nonce and the digest of the password should be constructed, as it done in the snippet below.
Please, note the order of the elements, and the fact that the non-encoded version of the nonce
is used for passwordDigest
construction.
import javax.xml.datatype.*;
import java.security.*;
import java.time.Instant;
import java.util.Base64;
import static java.lang.System.currentTimeMillis;
import static java.nio.charset.StandardCharsets.UTF_8;
class Snippet {
private static final SecureRandom RANDOM;
private static final int NONCE_SIZE_IN_BYTES = 16;
private static final String MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1 = "SHA-1";
private static final String SECURE_RANDOM_ALGORITHM_SHA_1_PRNG = "SHA1PRNG";
static {
try {
RANDOM = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM_SHA_1_PRNG);
RANDOM.setSeed(currentTimeMillis());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws DatatypeConfigurationException {
final var nonceBytes = generateNonce();
final var password = "password";
final var createdDate = DatatypeFactory.newInstance().newXMLGregorianCalendar(Instant.now().toString());
final var passwordDigestBytes = constructPasswordDigest(nonceBytes, createdDate, password);
final var base64Encoder = Base64.getEncoder();
final var nonceBase64Encoded = base64Encoder.encodeToString(nonceBytes);
final var passwordDigestBase64Encoded = base64Encoder.encodeToString(passwordDigestBytes);
System.out.println(String.format("nonce: [%s], password digest: [%s]", nonceBase64Encoded, passwordDigestBase64Encoded));
System.out.flush();
}
private static byte[] generateNonce() {
var nonceBytes = new byte[NONCE_SIZE_IN_BYTES];
RANDOM.nextBytes(nonceBytes);
return nonceBytes;
}
/**
* @noinspection SameParameterValue
*/
private static byte[] constructPasswordDigest(byte[] nonceBytes, XMLGregorianCalendar createdDate, String password) {
try {
final var sha1MessageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1);
sha1MessageDigest.update(nonceBytes);
final var createdDateAsString = createdDate.toString();
sha1MessageDigest.update(createdDateAsString.getBytes(UTF_8));
sha1MessageDigest.update(password.getBytes(UTF_8));
return sha1MessageDigest.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
The output of this snippet is:
nonce: [KEyJbsmxL1JdsDHo7kWD6Q==], password digest: [+OiJDs2sEycZECHhQdJ8T9Lt2ns=]
Upvotes: 3
Reputation: 106
WS-Security defines password digest as
Base64 ( SHA1 ( nonce + created + password ) )
not
Base64 ( SHA1 ( password + nonce + created) )
And nonce is supposed to be 128 bits (16 bytes) encoded as Base64. e.g.
java.security.SecureRandom random = java.security.SecureRandom.getInstance("SHA1PRNG");
random.setSeed(System.currentTimeMillis());
byte[] nonceBytes = new byte[16];
random.nextBytes(nonceBytes);
String nonce = new String(org.apache.commons.codec.binary.Base64.encodeBase64(nonceBytes), "UTF-8");
Upvotes: 9