OAuth.
OAuth isn't really a big deal if you're working on an application with a user interface that can react to security prompts and such, but I'm not, my application runs as a web service that does not have the opportunity to prompt the end user.
So what to do?
If you're using the JVM platform I highly recommend the Scribe library, it really does make things easier to deal with.
So anyway, let's get started...
The first thing you need to do is generate an initial access token, access secret, and an OAuth session handle (I'll explain in a bit).
Using the following Groovy script I was able to generate everything I needed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import org.scribe.builder.ServiceBuilder import org.scribe.builder.api.YahooApi import org.scribe.model.OAuthRequest import org.scribe.model.Response import org.scribe.model.Token import org.scribe.model.Verb import org.scribe.model.Verifier import org.scribe.oauth.OAuthService def yahooConsumerKey = '<Your Yahoo Consumer Key>' def yahooConsumerSecretKey = '<Your Yahoo Consumer Secret>' def service = new ServiceBuilder().provider(YahooApi).apiKey(yahooConsumerKey).apiSecret(yahooConsumerSecretKey).build() def requestToken = service.getRequestToken() println "Go to the following URL and authenticate: ${service.getAuthorizationUrl(requestToken)}" print "Enter the verification code >> " def verifier = new Verifier(System.console().readLine()) def accessToken = service.getAccessToken(requestToken, verifier) println "Access Token: ${accessToken.token}" println "" println "Access Secret: ${accessToken.secret}" println "" println "OAuth Session Handle: ${accessToken.rawResponse.split('&')[3].split('=')[1]}" |
The access token and access secret are only good for an hour, but that's OK because we'll be able to refresh the access token and secret using that OAuth session handle that we extracted.
Now that we've gathered our tokens and secrets and sessions handles (OH MY!), we need a method that will retrieve a fresh token for us when appropriate. I have the following method setup so that if a retrieved token is more than thirty minutes old we go ahead and refresh.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | package com.example.yahooboss import org.joda.time.DateTime import org.joda.time.DateTimeConstants import org.scribe.builder.ServiceBuilder import org.scribe.builder.api.YahooApi import org.scribe.model.OAuthRequest import org.scribe.model.Token import org.scribe.model.Verb import org.scribe.oauth.OAuthService class YahooBossExampleUtil { // Timestamp starts at zero so the first call to the method will always refresh the token. static long yahooAccessTokenTimestamp = 0 // The following values should never change and could be put in a config file or something. static final String yahooConsumerKey = '<Your Yahoo Consumer Key>' static final String yahooConsumerSecretKey = '<Your Yahoo Consumer Secret>' static final String yahooSessionHandle = '<Your Generated Yahoo Session Handle>' // These values will change. static String yahooAccessToken = '<Your Generated Yahoo Access Token>' static String yahooAccessSecret = '<Your Generated Yahoo Access Secret>' static final long maxTokenDuration = DateTimeConstants.MILLIS_PER_MINUTE * 30 // Generate a new token at least every 30 minutes. static synchronized Token getYahooAccessToken() { def tokenAge = new DateTime().getMillis() - yahooAccessTokenTimestamp if (tokenAge >= maxTokenDuration) { def service = getYahooOAuthService() def accessToken = new Token(yahooAccessToken, yahooAccessSecret) def request = new OAuthRequest(Verb.GET, "https://api.login.yahoo.com/oauth/v2/get_token") request.addOAuthParameter("oauth_session_handle", yahooSessionHandle) service.signRequest(accessToken, request) def response = request.send() def yahooApi = new YahooApi() def refreshedAccessToken = yahooApi.getAccessTokenExtractor().extract(response.getBody()) yahooAccessToken = refreshedAccessToken.getToken() yahooAccessSecret = refreshedAccessToken.getSecret() yahooAccessTokenTimestamp = new DateTime().getMillis() } new Token(yahooAccessToken, yahooAccessSecret) } static OAuthService getYahooOAuthService() { new ServiceBuilder().provider(YahooApi).apiKey(yahooConsumerKey).apiSecret(yahooConsumerSecretKey).build() } } |
Notice on line 38 in the above example we're adding an OAuth parameter called "oauth_session_handle" and providing the session handle that we gathered using the script from the beginning of the post. The session handle allows us to identify the app as having been previously authorized even if the access token and secret have expired.
At this point we have everything we need to finally make a request for data from one of the Yahoo BOSS APIs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static String getAddressFromYahoo(double latitude, double longitude) { // The request URL needs to be URL encoded or else you'll get a "signature invalid" error. def url = "http://yboss.yahooapis.com/geo/placefinder?location=${latitude}%2B${longitude}&gflags=R" // The getYahooOAuthService() method from the previous example. def service = getYahooOAuthService() def request = new OAuthRequest(Verb.GET, url) service.signRequest(getYahooAccessToken(), request) def response = request.send() if (response.code == 200) { def xml = new XmlSlurper().parseText(response.body) def firstResult = xml.placefinder.children().first().result return "${firstResult.line1}, ${firstResult.city}, ${firstResult.statecode} ${firstResult.uzip}, ${firstResult.countrycode}" } '' } |
Pay special attention to the comment on line 2 in the above example, it's super important. If the request URL isn't properly escaped (URL encoded) you'll get an error response similar to the following:
1 2 3 4 5 | <?xml version='1.0' encoding='UTF-8'?> <yahoo:error xmlns:yahoo='http://yahooapis.com/v1/base.rng' xml:lang='en-US'> <yahoo:description>Please provide valid credentials. OAuth oauth_problem="signature_invalid", realm="yahooapis.com"</yahoo:description> </yahoo:error> |
So that's about all there is to it.
Still not a big fan of dealing with OAuth, though after this implementation and writing this post it doesn't seem that big a deal.
Happy coding!