Thursday, August 09, 2018

PowerBuilder 2017 R3 New Feature: OAuth

PowerBuilder 2017 R2 added a number of REST features, which I covered in a previous blog post.  One area for improvement on the original feature was better support for REST authentication methods.  The HTTPClient and RESTClient objects provided methods to set request headers, but doing the legwork needed for some of the more complex authentication mechanism (i.e., OAuth) were left to the developer.  PowerBuilder 2017 R3 addresses that.  In this blog post we'll look at using this new feature.



The main object that makes this work is the new OAuthClient object.  It has two methods:

AccessToken ( TokenRequest request, TokenResponse response )
RequestResource ( OAuthRequest request, ResourceResponse response )
We're going to look using this new object to do a specific OAuth operation, searching for tweets on Twitter.  To do that, the first thing I'm going to do is go to https://apps.twitter.com/ and create an application for my account.  For this particular demonstration, I'm only going it allow this application to have read permissions, and I'm going to use what Twitter calls application-only authentication.

That is, you can create an application that other users can then use to do things in twitter on their own behalf.  I use applications like Buffer and Flipboard to do just that for me.  When a user wants to use that kind of an app there is a one time authentication process where the user confirms that the app is allowed to use their twitter account.  That's more than involved than I want to get into for a simple demonstration

You can also create applications that do things on their own behalf, not representing a user.  That's what we're going to do.  On the Key and Access Tokens tab on this page you'll find the Consumer ID and Consumer Secret that we're going to use to authenticate to Twitter on behalf of the app.


The process flow is fairly simple.  Our application is going to use the Consumer ID and Consumer Secret as user name and password and perform a Basic Authentication to the Twitter OAuth token service.  In Basic Authentication the user name and password are combined with a colon between them.  That value is then Base64 encoded and included in an Authorization header in the request with the word Basic in front of it.

Assuming the authentication works, Twitter passes back an OAuth token.  The application then makes the request for the resources it wants, this time passing the token with the word "Bearer" in front of it in the Authorization header.  Note that once the application has a token, it can use it for numerous requests, up until the time the token expires.  When the token is returned from that OAuth service, one of the pieces of information that is passed back with it is when the token will expire.  The length of time varies depending on whose services you're using, but as of this writing however, Twitter does not expire tokens at all.

Source:  Twitter

Note that you don't have to worry about these details, the OAuthClient is handling it all for you.  Let's look at the sample application:


So we have two command buttons, one to request the Access Token (one time operation) and one to perform searches (as many as we want using that same token).  The code behind the "Request Access Token) button looks like this:

 Integer            rc  
 OAuthClient        client  
 TokenRequest       request  
 TokenResponse      response  
   
 OpenWithParm ( w_oauth_credentials, request )  
 request = message.PowerObjectParm  
 request.granttype = 'client_credentials'  
 request.method = 'POST'  
 request.secureprotocol = 0  
 request.timeout= 60  
 request.tokenlocation = 'https://api.twitter.com/oauth2/token'  
 request.SetHeader ( "User-Agent", "PowerBuilder OAuth Demo" )  
   
 client = CREATE OAuthClient  
 rc = client.AccessToken ( request, response )  
 Destroy client  
   
 OpenWithParm ( w_access_token_response, response )  
   
 accessToken = response.getaccesstoken( )  
   
 cb_search.enabled = TRUE  

Note that I didn't really need to provide the secureprotocol or timeout properties on the OAuthClient object here.  I included them just to show they were available and set them to their default values.  Timeout is in seconds and you can set it to 0 for an infinite timeout.  A 0 for SecureProtocol means the object will start with TLS 1.2 and then work down the list of SSL protocols until it finds the most secure one that is supported by the service it's connecting to.

You simply set the ClientID and ClientSecret properties.  We're also indicating, per the Twitter API docs,that the grant type we want is "client_credentials", that we're doing a POST request, and the particular URL that we're making the request to.

The w_oath_credentials window that is being opened here just allows the user of the application to provide the appropriate credentials for the OAuth token request.


They're returned back to the calling window on the click of the OK button.

 tr.username = sle_username.Text  
 tr.password = sle_password.Text  
 tr.clientid = sle_consumerkey.Text  
 tr.clientsecret = sle_consumersecret.Text  
 CloseWithReturn ( parent, tr )  

For informational purposes, we open another window to display all the gory details of what was returned from the service as a result of our token request:


This is in the open event of the window to show the data:

 integer            rc  
 string             body, errortype, errordescription, erroruri, errorstate  
 TokenResponse      response  
   
 response = message.PowerObjectParm  
   
 sle_accesstoken.Text = response.Getaccesstoken( )  
 rc = response.getbody( body )  
 mle_body.Text = body  
 sle_expires.Text = String ( response.getexpiresin( ) )  
 sle_refreshtoken.Text = response.getrefreshtoken( )  
 sle_statuscode.Text = String ( response.getstatuscode( ) )  
 sle_statustext.Text = response.getstatustext( )  
 sle_tokentype.Text = response.gettokentype( )  
 rc = response.gettokenerror( errortype, errordescription, erroruri, errorstate )  
 sle_errortype.Text = errortype  
 sle_errordescription.Text = errordescription  
 sle_erroruri.Text = erroruri  
 sle_errorstate.Text = errorstate  
   

In this case we simply got a token, and the expires of 0 indicates that it doesn't expire.

Now the search:

 Integer              rc  
 String               responsebody, requestbody  
 OAuthRequest         request  
 OAuthClient          client  
 ResourceResponse     response  
 Blob                 lblb_body  
   
 request.method = "GET"  
 request.secureprotocol = 0  
 request.timeout = 60  
 request.URL = "https://api.twitter.com/1.1/search/tweets.json" + '?q=' + sle_searchterm.Text  
 request.SetHeader ( "User-Agent", "PowerBuilder OAuth Demo" )  
   
 request.setaccesstoken( accessToken )  
   
 client = CREATE OAuthClient  
 rc = client.requestresource( request, response )  
 Destroy client  
   
 OpenWithParm ( w_resource_response, response )  
   
 rc = response.getbody( lblb_body )  
 mle_results.Text = String ( lblb_body, EncodingUTF8! )  

Once again, timeout and secure protocol are optional.  The only thing we need to do is, per the Twitter API docs, indicate that the method is GET and provide the URL.  We pass in the accessToken that we got in the token request and the OAuthClient sends that along on the request.

And we have another window that shows everything that was returned from the ResourceRequest.


Of course, if you wanted to use the RESTClient or HTTPClient to perform the request, you can do that as well.  The RESTClient in R3 has an added TokenRequest argument for the Retrieve method.
HTTPClient doesn't have (at least not yet) such an option, but you can perform the same thing by adding the following SetHeader statement before sending the request:

object.SetHeader ( "Bearer " + accessToken )

Summary

PowerBuilder 2017 R3 provides an important improvement in REST web services support with the added support for OAuth.  What I'd like to see in the future is support for JWT bearer tokens as well.  However, with the encoding and encryption objects also added 2017 R3 covered in another blog post, it would be fairly easy now to put together those requests manually.

No comments: