Saturday, August 23, 2014

Accessing Yahoo BOSS APIs from a Server Application

Recently I was working on a feature that required the use of a reverse geo-coding service. For those of you that don't know, a reverse geo-coding service takes coordinates (latitude and longitude) and translates them to an address. We decided to use Yahoo BOSS's PlaceFinder service as the price was right and the free alternatives didn't provide enough quota for our purposes. But there was a problem...

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!

No comments:

Post a Comment