I started with a Java OAuth client library known as Scribe, which has a lot going for it. The solution includes the Scribe library, and 9 additional classes. All told, it's less than 650 lines of code, and relatively sparse on documentation. And I managed to get it to work in two days. Two days. I'm done listening to complaints about OAuth complexity.
So, given my lack of copious documentation, here's an explanation of how this stuff works.
InvalidOAuthRequestException
public class InvalidOAuthRequestException extends Exception {public InvalidOAuthRequestException(String reason) {
super(reason);
}
}
This is an exception class that I use to report a variety of different reasons why an OAuth request might not be valid. As far as my code is concerned, I don't need to differentiate these. If someone sent the wrong data, the error message is enough to tell you why my provider rejected the request. Classes don't get much shorter than this.
OAuthProvider
public interface OAuthProvider {
public Token getRequestToken(HttpServletRequest req) throws InvalidOAuthRequestException;
public String authorizeUser(HttpServletRequest req) throws
throws InvalidOAuthRequestException;
public String getCallback(HttpServletRequest req) throws
throws InvalidOAuthRequestException;
public Token getAccessToken(HttpServletRequest req)
throws InvalidOAuthRequestException;
public void validate(HttpServletRequest req)
throws InvalidOAuthRequestException;
public TokenRepository getRepository();
}
This interface describes a provider service, and contains six key methods. In the OAuth workflow, there are four main steps:
- The first step is to get a request token. The getRequestToken() method of OAuthProvider handles most of the effort. It takes a HttpServletRequest that has been generated by calling the request token end point appropriately, and generates a new token.
- The next step is dealing with the login flow. Once it has a request token, the client needs to pass it the "login" page of the service provider. The service provider can either accept or reject the users credentials, and the user can agree or cancel on the authorization.
- If the user credentials are accepted AND the user authorizes the calling application to access their data, then a temporary token and verification string is created and the user is redirected to the client application with those values. The temporary token is created using the authorizeUser() method. That method returns the callback string which can be used to redirect the user appropriately.
- If the user credentials are not accepted, OR the user does not authorize the calling application, we need to return back to the calling application WITHOUT passing a temporary token. That's what getCallback() helps with. It generates the appropriate callback string to handle this "unauthorized" path.
- Having been authorized (following from 2.1 above), the client application now must request an access token. The getAccessToken() method of OAuthProvider again, handles most of that effort.
- Finally, API requests need to verify that they are being accessed with a valid access token. That's what the validate() method does.
- Tokens, Tokens, Tokens. Every provider deals with a ton of tokens. It has to be configured with a place to create and store these things persistently. getRepository() provides access to the provider's token repository.
OAuthProvider10aImpl
There are number of variations on a theme that could be used to implement an OAuth provider. I picked a few reasonable settings and implemented them. It uses the HMAC-SHA1 signature method as the only method that it expects clients to use. I don't need the complexity of RSA-based signing, but I don't want to support PLAINTEXT signing.
The core methods of my implementation follow the same general pattern:
- Map the ServletRequest into an OAuthRequest.
- Validate the signature of the OAuthRequest.
- Perform some business logic with various OAuth parameters, manipulating tokens as necessary.
- Return the token (or other data) needed for the next step.
TokenRepository
import org.scribe.model.Token;
public interface TokenRepository {
enum TokenType {
CLIENT_KEY,
REQUEST_TOKEN,
TEMP_TOKEN,
ACCESS_TOKEN
};
public Token get(TokenType type, String key);
public void put(TokenType type, Token t);
public Token remove(TokenType type, String key);
public Token create(String data);
public boolean isNonceUsed(String nonce);
public void saveNonce(String nonce);
}
MemoryTokenRepositoryImpl
MemoryTokenRepositoryImpl is an implementation of the TokenRepository class. It uses a HashMap to keep track of tokens and nonces. The get/put/remove methods map into get/put/remove on its hashmap with the TokenType prefixed to the key. The same hashmap is also used to store nonce values, with "nonce:" prepended to the nonce value serving as the key. It's a totally simple and useless implementation, unless of course you need something quick and dirty for testing. In which case, it works just fine.
Tokens (and secrets) are created by generating about 24 bytes worth of random bits (using java.util.Random), and then base64 encoding the results, and splitting the string into two parts. The first part becomes the key, and the second part the secret.
Util
This was really intended to be a utility class, but instead it is what does most of the heavy lifting.
public static OAuthRequest createRequest(HttpServletRequest req)
This method creates an OAuthRequet object (a class existing in Scribe) from an existing HttpServletRequest object. OAuthRequest is the class that Scribe uses to generate signatures from, which is why I do this. I don't want to have to regenerate the signature creation logic. This method is called very early by each of the request processing methods found in OAuthProvider10aImpl.
public static Map<String,String> parseHeader(String header)
This method is used to parse the "Authorization" header of a request. OAuth parameters can appear either in query parameters, as POST parameters, or in the Authorization header. The parser is dead simple. It splits content at sequences of commas and spaces, and then splits those into a name and value part. Because of the very smart way that OAuth defined the OAuth schema for this header parameter, you can use a dump parser like this. This method is called by createRequest above to read the Authorization header of a request and store the parameters into the OAuthRequest.
public static Verb getVerb(String method)
This method maps a string into an appropriate enumeration type, turning "GET" into Verb.GET, and "POST" into Verb.POST.
public static Token validate(OAuthRequest r, TokenRepository tokens) throws InvalidOAuthRequestException
This method does the real work . It checks the OAuth Version (1.0 only), ensures the request is not too old, verifies the nonce hasn't been used before, maps the consumer_key and token to a consumer_secret and token_secret, regenerates the signature and compares it to the existing one. If, somewhere along the way, one of these tests fails, it throws an InvalidOAuthRequestException explaining the stage at which the failure occured.
public static String getSignature(OAuthRequest r, String consumerSecret, String tokenSecret)
This method computes the signature from the request, given the consumer and token secrets. This is five lines of code. It simply puts together the right parts from elsewhere in Scribe to generate the signature.
OAuthRequest
This is a servlet that implements the token request and access request endpoints. It expects something implementing the OAuthProvider interface I described above to be stored in "OAuth.Provider" attribute of the application. The doGet() method usually returns a 404 (but if the URL includes the word test, it duplicates doPut()). The doPost() method gets the operation to perform from the end of the URL Path (either /request or /access). It calls getRequestToken() or getAccessToken() appropriately from the OAuthProvider. Then it writes the returned token to the response in application/x-www-form-urlencoded format.
init.jsp
The OAuthRequest servlet needs a provider. This JSP creates one and stuffs it into the appropriate application attribute.
Authorize.jsp
This is my login page. Right now, it looks something like this:
By entering your name and password below and clicking on "I Agree", you agree to the following:
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Username:
Password:
The point demonstrated is that Authorization can be worded as "consent".
<% response.setHeader("Content-Type", "application/json");
OAuthProvider provider =
(OAuthProvider)application.getAttribute("OAuth.Provider");
try
{ provider.validate(request); }
catch (InvalidOAuthRequestException ex)
{ ex.printStackTrace();
response.sendError(401);
}
%>{
"Test": "This is a test",
"name": "Hi Keith",
"screen_name": "Did this work?"
}
Password:
The point demonstrated is that Authorization can be worded as "consent".
validateCredentials.jsp
The whole point of OAuth is to validate that requests have been authorized. So this page is a dummied up API call. It looks something like this right now:<% response.setHeader("Content-Type", "application/json");
OAuthProvider provider =
(OAuthProvider)application.getAttribute("OAuth.Provider");
try
{ provider.validate(request); }
catch (InvalidOAuthRequestException ex)
{ ex.printStackTrace();
response.sendError(401);
}
%>{
"Test": "This is a test",
"name": "Hi Keith",
"screen_name": "Did this work?"
}
It fails if you aren't sending an OAuth signed request, and succeeds if you are. The content is just some JSON that my ABBI application knows how to process.
Download
Everything you need to build this OAuth provider is available for download on Google code. Just download the ABBI-Auth project from svn here.
No comments:
Post a Comment