Does anyone have a working example of two-legged OAuth using Java?
I am a service provider with a RESTful API based on Restlet and Spring. I would like to provide my consumers with 2-legged OAuth AKA "signed fetch". There are some Java libraries, but they tend to be completely undocumented or provide two-line examples with no context. (spring-security-oauth provides a fabulous working example for 3-legged OAuth, but does not support 2-legged OAuth.)
I am going crazy trying to find a single documented example of 2-legged OAuth using any Java library. Any working example will do - I'll use a standalone Java library, a custom Spring security filter, a Restlet extension.
I am wandering alone in the desert and my canteen is empty. Help.
I am going crazy trying to find a single documented example of 2-legged OAuth using any Java library. Any working example will do - I'll use a standalone Java library, a custom Spring security filter, a Restlet extension.
I am wandering alone in the desert and my canteen is empty. Help.
2
people have this question
I have this question, too!
Tell me when someone answers.
The more people who ask this question, the more it gets noticed.
The more people who ask this question, the more it gets noticed.
-
Inappropriate?I have the same needs except I am using Jersey instead of Restlet. From what I understand about Spring Security, a single solution should accommodate both frameworks equally. Please let me know if you learn anything interesting.
-
Inappropriate?Hey there, OP here. Here's what worked for me. At this point in time, I think if you want two-legged OAuth in Java, you're going to have to roll up your sleeves and extend an existing library. In addition, if you need to provide OAuth in a distributed environment, you're going to have to write and plug in your own components for consumer and timestamp/nonce storage.
I didn't use Restlet support for OAuth because it was undocumented and I felt that with the upcoming release of Restlet 2.0, everything might change anyway.
I didn't use OAuth for Spring Security (http://spring-security-oauth.codehaus...) because it only supports 3-legged OAuth at this time. They have a ticket to add two-legged OAuth, but I couldn't wait, and I don't understand enough about Spring Security to do it myself.
I didn't use the Netflix contributed OAuth Java libraries (http://oauth.googlecode.com/svn/code/...) because I thought it was a good start but I just didn't like the code style.
In the end, I went with Asemantics OAuth Framework at http://asmx-oauth.googlecode.com/svn/.... I liked the code, and once I understood how two-legged OAuth should work, it was really easy to extend their code to write my own Provider and a tokenless HmacSha1 request signer. This code also does not work out-of-box in a distributed environment, but I was able to use the interfaces to plug in my own consumer storage and timestamp/nonce storage.
Here are some links I found useful:
* Two-legged OAUth API specs at Gliffy - http://www.gliffy.com/developer/apido...
* Signpost helped me to understand how an OAuth client should behave - http://code.google.com/p/oauth-signpost/
* term.ie has a super-useful webpage with a PHP OAuth client at http://oauth.googlecode.com/svn/code/... - I used their client to debug my provider, and also to document how PHP developers should connect to my API
* Good explanation of two-legged OAuth at http://nagiworld.net/2008/11/oauth-if...
I think in a year or so, this rigamarole will not be necessary, but this is what worked for me right now.
-
Hi Prasinous, I am trying to implement an OAuth Provider myself and was looking at the Restlet oauth extension initially, but after playing around with the API for about 2 weeks, realized that the API is not usable and ditched restlet.
I am now looking at the Jersey Oauth extension which seems to be better documented than Restlet. I would appreciate if you could post a sample of a how to write a custom service provider. -
Inappropriate?Here is the TokenlessHmacSha1RequestSigner, which is my modification of the oauth-asmx HmacSha1RequestSigner.
import com.asemantics.oauth.core.Service;
import com.asemantics.oauth.core.message.AbstractRequestMessage;
import com.asemantics.oauth.core.message.MessagesConstants;
import com.asemantics.oauth.core.parameters.Parameter;
import com.asemantics.oauth.core.parameters.ParameterListBuilder;
import com.asemantics.oauth.core.parameters.ParameterListSerializer;
import com.asemantics.oauth.core.signers.ConsumerSecret;
import com.asemantics.oauth.core.signers.SignatureException;
import com.asemantics.oauth.core.utils.Base64Utils;
import com.asemantics.oauth.core.utils.UTF8Utils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;
public class TokenlessHmacSha1RequestSigner
{
private static final Log log = LogFactory.getLog(TokenlessHmacSha1RequestSigner.class);
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static final String HTTP_PROTOCOL = "http";
private static final String HTTPS_PROTOCOL = "https";
private static final String PATH_SEPARATOR = "/";
private static final String SCHEME_SEPARATOR = "://";
private static final int DEFAULT_HTTP_PORT = 80;
private static final int DEFAULT_HTTPS_PORT = 443;
private static final char BASE_STRING_SEPARATOR = '&';
protected String encode(String consumerSecret, String baseString) throws SignatureException
{
try
{
String urlEncodedConsumerSecret = UTF8Utils.urlEncode(consumerSecret) + '&';
SecretKeySpec signingKey = new SecretKeySpec(urlEncodedConsumerSecret.getBytes(), HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
byte[] rawHmac = mac.doFinal(baseString.getBytes());
String encoded = Base64Utils.encode(rawHmac);
if (log.isDebugEnabled())
log.debug("encode():\n secret:\t\t" + urlEncodedConsumerSecret + "\nbaseString:\t\t" + baseString + "\nencoded:\t\t" + encoded);
return encoded;
}
catch (Exception e)
{
log.error(e, e);
throw new SignatureException(e);
}
}
public boolean verify(String signature, ConsumerSecret consumerSecret, Service service, AbstractRequestMessage message, Parameter... parameterList) throws SignatureException
{
if (consumerSecret == null)
throw new IllegalArgumentException("Consumer secret can't be null!");
if (message == null)
throw new IllegalArgumentException("Message can't be null!");
String baseString = createBaseString(service, message, parameterList);
if (log.isTraceEnabled())
{
StringBuilder sb = new StringBuilder();
sb.append("\nOAuthRequest Object\n(\n");
sb.append("\t[parameters:private] => Array\n(\n");
sb.append("\t\t\t[oauth_version] => " + message.getVersion());
sb.append("\n\t\t\t[oauth_nonce] => " + message.getNonce());
sb.append("\n\t\t\t[oauth_timestamp] => " + message.getTimestamp());
sb.append("\n\t\t\t[oauth_consumer_key] => " + message.getConsumerKey());
sb.append("\n\t\t\t[oauth_signature_method] => " + message.getSignatureMethod());
sb.append("\n\t\t\t[oauth_signature] => " + message.getSignature());
sb.append("\n\t)\n");
sb.append("\t[http_method:private] => " + service.getHttpMethod().name());
sb.append("\n\t[http_url:private] => " + getRequestUrl(service.getUrl()));
sb.append("\n\t[base_string] => " + baseString);
sb.append("\n)");
log.trace(sb.toString());
}
return verify(signature, consumerSecret.getValue(), baseString);
}
private String createBaseString(Service service, AbstractRequestMessage message, Parameter... parameterList) throws SignatureException
{
String method = service.getHttpMethod().name();
String requestUrl = getRequestUrl(service.getUrl());
String requestParameters = normalizeRequestParameters(message, parameterList);
// if (log.isDebugEnabled())
// log.debug("createBaseString(): method=" + method + ", requestUrl=" + requestUrl + ", requestParameters=" + requestParameters);
String baseString = new StringBuilder().append(method).append(BASE_STRING_SEPARATOR)
.append(UTF8Utils.urlEncode(requestUrl)).append(BASE_STRING_SEPARATOR).append(UTF8Utils.urlEncode(requestParameters)).toString();
if (log.isTraceEnabled())
log.trace("createBaseString(): baseString=" + baseString);
return baseString;
}
private String getRequestUrl(URL url)
{
String scheme = url.getProtocol().toLowerCase();
String authority = url.getAuthority().toLowerCase();
int port = url.getPort();
if ((HTTP_PROTOCOL.equals(scheme) && port == DEFAULT_HTTP_PORT) || (HTTPS_PROTOCOL.equals(scheme) && port == DEFAULT_HTTPS_PORT))
{
int index = authority.lastIndexOf(':');
if (index >= 0)
authority = authority.substring(0, index);
}
String path = url.getPath();
if (path == null || path.length() <= 0)
path = PATH_SEPARATOR; // conforms to RFC 2616 section 3.2.2
String requestUrl = scheme + SCHEME_SEPARATOR + authority + path;
if (log.isTraceEnabled())
log.trace("getRequestUrl(): requestUrl=" + requestUrl);
return requestUrl;
}
private String normalizeRequestParameters(AbstractRequestMessage message, Parameter... parameterList) throws SignatureException
{
ParameterListBuilder listBuilder = new ParameterListBuilder(true)
.append(MessagesConstants.CONSUMER_KEY, message.getConsumerKey())
.append(MessagesConstants.NONCE, message.getNonce())
.append(MessagesConstants.SIGNATURE_METHOD, message.getSignatureMethod())
.append(MessagesConstants.TIMESTAMP, message.getTimestamp())
.append(MessagesConstants.VERSION, message.getVersion());
// add all HTTP parameters
listBuilder.append(parameterList);
// now the param list is complete and ordered, builds the required string
StringWriter stringWriter = new StringWriter();
ParameterListSerializer serializer = new ParameterListSerializer(stringWriter, true);
try
{
serializer.serialize(listBuilder.getParameterList());
}
catch (IOException e)
{
throw new SignatureException(e);
}
return stringWriter.toString();
}
protected boolean verify(String signature, String consumerSecret, String baseString) throws SignatureException
{
String expectedSignature = encode(consumerSecret, baseString);
if (log.isDebugEnabled())
log.debug("verify(): signature=" + signature + ", expectedSignature=" + expectedSignature);
return expectedSignature.equals(signature);
}
}
-
Inappropriate?Here is the interface for the two-legged OAuth provider, which is based on asmx-oauth Provider:
import com.asemantics.oauth.core.HTTPMethod;
import com.asemantics.oauth.core.OAuthException;
import com.asemantics.oauth.core.parameters.Parameter;
import java.net.URL;
import java.util.List;
public interface TwoLeggedOAuthServiceProvider
{
static final String BEAN_NAME = "oauthServiceProvider";
boolean verifySignature(URL requestURL, HTTPMethod method, String authorizationHeader, List<parameter> parameterList) throws OAuthException;
void destroy();
}
</parameter> -
Inappropriate?Here is my two-legged oauth provider implementation - it's a Spring component. It is a very modest modification of asmx-oauth code whose biggest difference is using a tokenless HmacSha1 request signer.
Notes:
* due to some asmx-oauth code being package local, I had to copy a few files of their code locally. Look for imports of com.prasinous.ws.security.
* com.prasinous.ws.security.HybridConsumerStorage is my own implementation of asmx-oauth Consumer Storage interface. It hits the database instead of a local hashmap. Not included here. Pretty trivial to do - either replace with the same in-memory consumer storage being used in asmx-oauth provider or just implement their interface and roll your own.
* timestamp/nonce storage is a yucky problem for a distributed system. I am continuing to use asmx-oauth in-memory storage while we are still in development/integration testing, but I will need to swap it out for a custom implementation that hits an H2 or Derby database at some point in the future.
import com.asemantics.oauth.core.ConsumerDescriptor;
import com.asemantics.oauth.core.HTTPMethod;
import com.asemantics.oauth.core.OAuthException;
import com.asemantics.oauth.core.Service;
import com.asemantics.oauth.core.message.AbstractRequestMessage;
import com.asemantics.oauth.core.message.BaseMessage;
import com.asemantics.oauth.core.parameters.Parameter;
import com.asemantics.oauth.core.problems.*;
import com.asemantics.oauth.core.provider.storage.access.Access;
import com.asemantics.oauth.core.provider.storage.access.AccessStorage;
import com.asemantics.oauth.core.provider.storage.access.AccessStorageException;
import com.asemantics.oauth.core.provider.storage.access.InMemoryAccessStorage;
import com.asemantics.oauth.core.provider.storage.consumer.ConsumerStorageException;
import com.asemantics.oauth.core.signers.ConsumerSecret;
import com.asemantics.oauth.core.signers.SignatureException;
import com.asemantics.oauth.core.utils.UTF8Utils;
import com.prasinous.ws.security.HybridConsumerStorage;
import com.prasinous.ws.security.TokenlessHmacSha1RequestSigner;
import com.prasinous.ws.security.TwoLeggedOAuthServiceProvider;
import com.prasinous.ws.security.asmx.AbstractOAuthMessageParser;
import com.prasinous.ws.security.asmx.HeaderMessageParser;
import com.prasinous.ws.security.asmx.ParameterAbsentVerifyier;
import com.prasinous.ws.security.asmx.ParameterMessageParser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@Component(TwoLeggedOAuthServiceProvider.BEAN_NAME)
public class TwoLeggedOAuthServiceProviderImpl implements TwoLeggedOAuthServiceProvider
{
private static final Log log = LogFactory.getLog(TwoLeggedOAuthServiceProviderImpl.class);
private static final String HMAC_SHA1 = "HMAC-SHA1";
@Resource
private HybridConsumerStorage consumerStorage; // local cache of consumer key/secret
private AccessStorage nonceStorage = new InMemoryAccessStorage(); // TODO: bad implementation - figure out better dependency injection
public boolean verifySignature(URL requestURL, HTTPMethod method, String authorizationHeader, List<parameter> parameterList) throws OAuthException
{
log.info("verifySignature(): requestURL=" + requestURL);
log.info("verifySignature(): method=" + method);
log.info("verifySignature(): authorizationHeader=" + authorizationHeader);
for (Parameter parameter : parameterList)
log.info("verifySignature(): parameter=" + parameter);
AbstractRequestMessage message = checkRequestMethodParameters(requestURL, method, authorizationHeader, parameterList);
String consumerKey = message.getConsumerKey();
ConsumerDescriptor consumer = getConsumerByKey(consumerKey);
ConsumerSecret consumerSecret = consumer.getConsumerSecrets().get(message.getSignatureMethod());
checkTimestampAndNonce(consumerKey, message.getTimestamp(), message.getNonce());
// TODO: only supporting HMAC_SHA1 for the time being...
if (!HMAC_SHA1.equals(message.getSignatureMethod()))
{
log.error("verifySignature(): Unsupported signature method: " + message.getSignatureMethod());
throw new SignatureMethodRejectedException();
}
TokenlessHmacSha1RequestSigner requestSigner = new TokenlessHmacSha1RequestSigner();
String signature = message.getSignature();
try
{
String decodedSignature = UTF8Utils.urlDecode(signature);
Service service = new Service(method, requestURL);
if (!requestSigner.verify(decodedSignature, consumerSecret, service, message, getNonOAuthParams(parameterList)))
{
log.error("Signature '" + signature + "' for consumer key '" + consumerKey + "' has not been verified!");
throw new SignatureInvalidException();
}
}
catch (SignatureException e)
{
log.error("Impossible to verify the signature!", e);
throw new SignatureInvalidException();
}
return true;
}
private void checkTimestampAndNonce(String consumerKey, long timestamp, String nonce) throws TimestampRefusedException, NonceUsedException, ServerErrorException
{
try
{
Access lastAccess = nonceStorage.getLastAccess(consumerKey);
if (lastAccess == null)
{
nonceStorage.add(consumerKey, timestamp, nonce);
}
else
{
if (timestamp < lastAccess.getTimestamp())
{
log.error("checkTimestampAndNonce(): timestamp " + wrap(timestamp) + " < lastAccess.getTimestamp()=" + wrap(lastAccess.getTimestamp()));
throw new TimestampRefusedException(lastAccess.getTimestamp());
}
if (lastAccess.containsNonce(nonce))
{
log.error("checkTimestampAndNonce(): nonce " + wrap(nonce) + " has already been used!");
throw new NonceUsedException();
}
nonceStorage.add(consumerKey, timestamp, nonce);
}
}
catch (AccessStorageException e)
{
log.error("Impossible to add timestamp/nonce to storage!", e);
throw new ServerErrorException("An error occurred while verifying timestamp/nonce");
}
}
private ConsumerDescriptor getConsumerByKey(String consumerKey) throws ConsumerKeyUnknownException, ServerErrorException
{
log.info("getConsumerByKey(): " + consumerKey);
ConsumerDescriptor consumer;
try
{
consumer = consumerStorage.read(consumerKey);
if (consumer == null)
throw new ConsumerKeyUnknownException();
}
catch (ConsumerStorageException e)
{
String errorMsg = "An error occurred while finding consumer key " + consumerKey;
log.error(errorMsg, e);
throw new ServerErrorException(errorMsg);
}
return consumer;
}
private AbstractRequestMessage checkRequestMethodParameters(URL requestURL, HTTPMethod method, String authorizationHeader, List<parameter> parameterList)
throws AdditionalAuthorizationRequiredException, ParameterRejectedException, ParameterAbsentException
{
boolean useAuthorizationHeader = authorizationHeader != null && authorizationHeader.length() > 0;
List<parameter> oauthParams = getOAuthParams(parameterList);
if (!useAuthorizationHeader && oauthParams.isEmpty())
{
log.error("allowAccess(): missing both authorization header and OAuth params for " + method + " request: " + requestURL);
throw new AdditionalAuthorizationRequiredException();
}
AbstractRequestMessage requestMessage;
AbstractOAuthMessageParser parser;
try
{
if (!useAuthorizationHeader)
{
log.info("allowAccess(): using parameter list...");
ParameterMessageParser<basemessage> parameterMessageParser = new ParameterMessageParser<basemessage>(BaseMessage.class);
parameterMessageParser.parse(oauthParams);
parser = parameterMessageParser;
}
else
{
log.info("allowAccess(): using authorization header...");
HeaderMessageParser<basemessage> headerMessageParser = new HeaderMessageParser<basemessage>(BaseMessage.class);
headerMessageParser.parse(authorizationHeader);
parser = headerMessageParser;
}
}
catch (Exception e)
{
if (useAuthorizationHeader)
log.error("Impossible to verify authorization header: " + wrap(authorizationHeader), e);
else
log.error("Impossible to verify OAuth parameter list: " + wrapList(oauthParams));
throw new AdditionalAuthorizationRequiredException();
}
if (parser.containsRefusedParameters())
{
log.error("allowAccess(): refused parameters: " + wrapList(parser.getRefusedParameters()));
throw new ParameterRejectedException(parser.getRefusedParameters());
}
requestMessage = parser.getMessage();
ParameterAbsentVerifyier verifier = new ParameterAbsentVerifyier();
verifier.verify(requestMessage);
if (verifier.containsMissingParameters())
{
log.error("allowAccess(): missing expected parameters: " + wrapList(verifier.getMissingParameters()));
throw new ParameterAbsentException(verifier.getMissingParameters());
}
return requestMessage;
}
private List<parameter> getOAuthParams(List<parameter> parameterList)
{
List<parameter> filteredParameters = new ArrayList<parameter>();
for (Parameter parameter : parameterList)
{
if (parameter.isOauthParam())
filteredParameters.add(parameter);
}
if (log.isTraceEnabled())
log.trace("getOAuthParams(): found " + filteredParameters.size() + " params");
for (Parameter parameter : filteredParameters)
if (log.isTraceEnabled())
log.trace("getOAuthParams(): param=" + parameter);
return filteredParameters;
}
private Parameter[] getNonOAuthParams(List<parameter> parameterList)
{
List<parameter> filteredParameters = new ArrayList<parameter>();
for (Parameter parameter : parameterList)
{
if (!parameter.isOauthParam())
filteredParameters.add(parameter);
}
if (log.isTraceEnabled())
log.trace("getNonOAuthParams(): found " + filteredParameters.size() + " params");
for (Parameter parameter : filteredParameters)
if (log.isTraceEnabled())
log.trace("getNonOAuthParams(): param=" + parameter);
return filteredParameters.toArray(new Parameter[filteredParameters.size()]);
}
// This belongs in a separate utility class, copied here for clarity
private String wrap(Object o)
{
return o != null ? o.toString() : "[null]";
}
// This belongs in a separate utility class, copied here for clarity
private String wrapList(List list)
{
if (list == null)
{
return "[null]";
}
else if (list.isEmpty())
{
return "[empty]";
}
else
{
StringBuilder sb = new StringBuilder();
sb.append(" size=").append(list.size()).append(" [");
int penultimate = list.size() - 1;
for (int i = 0; i < list.size(); i++)
{
Object o = list.get(i);
sb.append(wrap(o));
if (i < penultimate)
sb.append(", ");
}
sb.append("]");
return sb.toString();
}
}
@PreDestroy
public void destroy()
{
log.info("destroy(): @PreDestroy ...");
nonceStorage = null;
}
}
</parameter></parameter></parameter></parameter></parameter></parameter></parameter></basemessage></basemessage></basemessage></basemessage></parameter></parameter></parameter> -
Inappropriate?The final touch was a very simple Spring service to check if the user was allowed to access a URL or not. You can create something similar and drop it into place wherever necessary. Since I'm using Restlet, I put it at the top of my handle(request, response) method in my top level Restlet.
@Service(SignedFetchService.BEAN_NAME)
public class SignedFetchServiceImpl implements SignedFetchService
{
private static final Log log = LogFactory.getLog(SignedFetchServiceImpl.class);
@Resource
private TwoLeggedOAuthServiceProvider provider;
public boolean allowAccess(URL requestURL, HTTPMethod method, String authorizationHeader, List<parameter> parameterList) throws OAuthException
{
return provider.verifySignature(requestURL, method, authorizationHeader, parameterList);
}
}
</parameter> -
Inappropriate?Hope this helps! It was a quick and dirty code dump, so you'll need to clean it up to use it. Best of luck.
-
Inappropriate?For some reason the message board puts a bunch of parameter tags at the end of some of my copy-pastes. Tried to fix it but couldn't. Just delete those!
Loading Profile...



