Get your own customer support community
 

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.
 
sad I’m frustrated
Inappropriate?
2 people have this question

  • Karl Garske
    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.
  • prasinous
    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.
     
    silly
  • Comment_icon
    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.
  • prasinous
    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);
    }
    }
  • prasinous
    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>
  • prasinous
    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>
  • prasinous
    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>
  • prasinous
    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.
  • prasinous
    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!
User_default_medium