Print
Category: PowerBuilder
Hits: 7176

User Rating: 5 / 5

Star ActiveStar ActiveStar ActiveStar ActiveStar Active
 

REST support was added to PowerBuilder in 2017 R2 and enhanced in 2017 R3.  PowerBuilder 2019 contains additional significant enhancements to REST support, including the following:

 

 

RetrieveOne

client.RetrieveOne( dw_1, "https://jsonplaceholder.typicode.com/posts/1")

Submit

client.Submit("https://jsonplaceholder.typicode.com/posts", ls_response, dw_1, Primary!, 1, 1, false )

SetRequest Header

GZip 

client.SetRequestheader( 'Accept-Encoding', 'gzip’, TRUE)

Statistics for a REST call without GZip compression:

Statistics for the same of REST call with GZip compression:

With GZip compression enabled, the call was 95% faster and the size of the data transferred was 96% smaller.

Send[Method]Request

OAuth

OAuth support was originally added in PowerBuilder 2017 R3 through an OAuthClient object and an option to add a TokenRequest argument to the Retrieve method of the RESTClient.  In PowerBuilder 2019, the RESTClient was further modified with specific GetOAuthToken and SetOAuthToken methods that can be used instead.  In particular the RESTClient has many additional methods in 2019 (RetriveRow, Submit, SendGetRequest, SendPostRequest, etc.).  Using the GetOAuthToken/SetOAuthToken approach works with all of the methods.

For example, implementing the Twitter API referenced in the earlier article linked above, but using the new methods, looks like this.

string ls_url, ls_response, accessToken

ls_url = "https://api.twitter.com/1.1/search/tweets.json" + '?q=' + sle_searchterm.Text

TokenRequest request

request.clientid = sle_consumerkey.Text
request.clientsecret = sle_consumersecret.Text
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 RESTClient
client.GetOAuthToken( request, accessToken )
lient.SetOAuthToken( accessToken )
client.SendGetRequest(ls_url,ls_response)

JSON Web Tokens (JWT)

Support for JWT is new in PowerBuilder 2019.  A JWT request is composed of three pieces:

The 3 pieces are then base64url encoded and concatenated with periods between them to form a single request token.  The response upon a successful authentication is a bearer token.

Encoded and decoded JWT request objects

JWT is a bit less structured than OAuth.  For example, JWT allows the implementor to create custom claims to include in the payload.  As a result, the GetJWTToken method that has been added to PowerBuilder doesn't send a highly structured object (like the TokenRequest for OAuth) but instead just sends simple JSON, so that you can add whatever claims you need to include.

Also, in practice, there are several different approaches for how JWT tokens are requested and returned to a client.  Some examples follow, which for lack of better terminology I'll just refer to as Scenerios 1, 2 and 3:

Scenerio 1

In Scenerio 1 the client doesn't prepare the JWT request.  Instead, the client sends information about the user (e.g., username, password, etc.) to the REST service.  The REST service prepares the JWT and sends it internally to an identity service, which returns the bearer token to the REST service, which it turn returns it to the client.  When the client then uses the bearer token in subsequent calls to the REST service, the REST service passes them internally to the identity service for validation before returning data to the user.

One example of such an implementation is the JWT authentication used by AirMap, a service that allows drone operators to submit flight plans for their drones and receive information such as weather and airspace restrictions they need to be aware of to ensure proper operation of the drone.  In addition, the recommended approach for JWT authentication REST services created in the SnapDevelop IDE from Appeon also uses such an implementation.  Not suprisingly then, the GetJWTToken method referenced above directly supports Scenerio 1.

The following is an example of calling the AirMap service:

long root
string JWTRequest, JWTToken, ls_response, ls_token

JSONGenerator jg
jg = create JSONGenerator
root = jg.createjsonobject( )
jg.AddItemString ( root, 'grant_type', 'password' )
jg.AddItemString ( root, 'client_id', airmap_client_id )
jg.AddItemString ( root, 'connection', 'Username-Password-Authentication' )
jg.AddItemString ( root, 'username', airmap_username)
jg.AddItemString ( root, 'password', airmap_password)
jg.AddItemString ( root, 'scope', '' )
jg.AddItemString ( root, 'device', device_name );
JWTRequest = jg.GetJSONString()
Destroy jg

RESTClient client
client = create RESTClient
client.SetRequestHeader ("Content-Type", "application/json")
client.GetJWTToken( 'https://sso.airmap.io/oauth/ro', JWTRequest, JWTToken )

JSONParser jp
jp = create JSONParser
jp.loadstring ( JWTToken )
root= jp.GetRootItem()
ls_token = jp.GetItemString(root, "access_token")
Destroy jp

client.SetRequestheader( 'X-API-Key', airmap_api_key)
client.SetJWTToken( ls_token )
client.SendGetRequest( 'https://api.airmap.com/pilot/v2/profile', ls_response)
Destroy client

 

Scenario 2

The main difference between Scenerio 1 and Scenerio 2 is that in Scenerio 2 is is the client and not the service that prepare the JWT request.  Currently, the GetJWTToken method of the RESTClient does not support generation of the JWT token.  However, it can still be requested using a number of the REST and cryptography methods that were introduced in PowerBuilder 2017 R3 and 2019.  One example of this Scenerio is Docusign.

The following is an example of calling the Docusign API:

Blob rsakey, sig_data_sha, sig_data_blob, sig_blob, body_blob, header_blob
DateTime issueAt, expiresAt
Long root, issued, expires, response
String header, header_encoded, body, body_encoded, sig_data, sig_encoded, request_data, response_body

JSONGenerator jg
jg = create JSONGenerator

root = jg.createjsonobject( )
jg.AddItemString ( root, 'typ', 'JWT' )
jg.AddItemString ( root, 'alg', 'RS256' )
header = jg.GetJSONString()
header_blob = Blob ( header, EncodingUTF8! )
header_encoded = of_base64urlencode( header_blob )

issueAt = DateTime(Today(),Now())
issued = of_getunixepoch(issueAt)
expiresAt = DateTime(Today(),RelativeTime ( Now(), 60 * 5 ))
expires = of_getunixepoch(expiresAt)

root = jg.createjsonobject( )
jg.AddItemString ( root, 'iss', docusign_issuer )
jg.AddItemString ( root, 'sub', docusign_subject )
jg.AddItemNumber(root,'iat', issued )
jg.AddItemNumber(root,'exp',expires )
jg.AddItemString(root,'aud','account-d.docusign.com')
jg.AddItemString(root, 'scope', 'signature impersonation' )
body = jg.GetJSONString()
body_blob = Blob ( body, EncodingUTF8! )
body_encoded = of_base64urlencode ( body_blob )

CrypterObject crypter
crypter = create CrypterObject
sig_data = header_encoded + '.' + body_encoded
sig_data_blob = Blob ( sig_data, EncodingUTF8!)
sig_data_sha = crypter.sha(SHA256!, sig_data_blob )
rsakey = of_getrsakey()
sig_blob = crypter.AsymmetricSign ( RSA!, sig_data_sha, rsakey)
sig_encoded = of_base64urlencode ( sig_blob )

request_data = 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer'
request_data += '&assertion=' + header_encoded + '.' + body_encoded + '.' + sig_encoded

RESTClient client
client = create RESTClient
client.SendPostRequest( 'https://account-d.docusign.com/oauth/token', request_data, JWTToken )
Destroy client

JWT requires Base64URLEncoding, which is a variation of Base64 encoding not directly supported by PowerBuilder.  I created the following method to adjust the Base64 encoding done by PowerBuilder to implement Base64URLencoding.

String ls_encoded_data
Integer li_pos

CoderObject coder
coder = create CoderObject
ls_encoded_data = coder.Base64encode( ablob_data )
Destroy coder

//Strip off any trailing =
li_pos = Pos ( ls_encoded_data, '=' )
IF li_pos > 0 THEN
ls_encoded_data = Left ( ls_encoded_data, li_pos - 1 )
END IF

//Replace all + characters with -
li_pos = Pos ( ls_encoded_data, '+' )
DO WHILE li_pos > 0
ls_encoded_data = Replace ( ls_encoded_data, li_pos, 1, '-' )
li_pos = Pos ( ls_encoded_data, '+' )
LOOP

li_pos = Pos ( ls_encoded_data, '/' )
DO WHILE li_pos > 0
ls_encoded_data = Replace ( ls_encoded_data, li_pos, 1, '_' )
li_pos = Pos ( ls_encoded_data, '/' )
LOOP

Return ls_encoded_data

Also, setting the issue and expiration values for the request requires getting date times in Unix Epoch format (number of seconds since January 1st, 1970 GMT).  I created this function to convert PowerBuilder date time values to Unix Epoch values:

long days
long seconds
long epoch
ulong rc1
boolean rc2
TIME_ZONE_INFORMATION tzi
SYSTEMTIME local
SYSTEMTIME utc
datetime l_datetime_utc

rc1 = GetTimeZoneInformation(tzi)

//Convert PowerBuilder datetime to SYSTEMTIME
local.wYear = Year(Date(a_datetime))
local.wMonth = Month(Date(a_datetime))
local.wDay = Day(Date(a_datetime))
local.wHour = Hour(Time(a_datetime))
local.wMinute = Minute(Time(a_datetime))
local.wSecond = Second(Time(a_datetime))
local.wMilliseconds = 0

//Get the time in UTC
rc2 = TzSpecificLocalTimeToSystemTime(tzi, local, utc)

//Convert the UTC SYSTEMTIME back to a PowerBuilder datetime
l_datetime_utc = DateTime ( Date ( utc.wYear, utc.wMonth, utc.wDay ), Time ( utc.wHour, utc.wMinute, utc.wSecond ) )

days = DaysAfter ( Date ( '1970/01/01' ), Date ( l_datetime_utc ) )
seconds = SecondsAfter ( Time ( '00:00:00' ), Time ( l_datetime_utc ) )
epoch = ( days * 24 * 60 * 60 ) + seconds
Return epoch

A couple of Windows API calls are being done here to get the client timezone and to convert times to GMT.  The local external function calls for them are:

Function ulong GetTimeZoneInformation (ref TIME_ZONE_INFORMATION lpTimeZoneInformation) Library "kernel32"
Function boolean TzSpecificLocalTimeToSystemTime(TIME_ZONE_INFORMATION lpTimeZone, SYSTEMTIME lpLocalTime, ref SYSTEMTIME lpUniversalTime) Library "kernel32"

The SYSTEMTIME structure being passed to the Windows API call has this definition:

global type systemtime from structure
  integer wYear
  integer wMonth
  integer wDayofWeek
  integer wDay
  integer wHour
  integer wMinute
  integer wSecond
  integer wMilliseconds
end type

And the TIME_ZONE_INFORMATON structure being passed to the Windows API call has this definition.  Note that it references the SYSTEMTIME structure defined above.

global type time_zone_information from structure
  long bias
  integer standardname[31]
  systemtime standarddate
  long standardbias
  integer daylightname[31]
  systemtime daylighttime
  long daylightbias
end type

The Docusign JWT token has to be asymmetrically signed using a private certificate generated for you when you create an account with them.  The of_getrsakey method referenced above takes the Base64 encoded format of the private key and converts it to a blob to use with the AsymmetricSign method of the PowerBuilder CryperObject.

Scenerio 3

The main difference between Scenerio 2 and Scenerio 3 is that (a) the JWT request is sent on every request rather than to a seperate method and (b) a bearer token is never returned.  Instead, the service re-validates the JWT request on each call and only returns data if the request is valid.  The same approach for generating the request in PowerBuilder that can be used with Scenerio 2 can be used with Scenerio 3 as well.  One example of this Scenerio is Sallling Group.

The following is an example of calling the Salling Group API:

Blob sig_data_sha, sig_data_blob, sig_blob, body_blob, header_blob, key_blob
DateTime expiresAt
Long root, expires, response
String header, header_encoded, body, body_encoded, sig_data, sig_encoded, request_data, response_body, JWTToken

JSONGenerator jg
jg = create JSONGenerator

root = jg.createjsonobject( )
jg.AddItemString ( root, 'typ', 'JWT' )
jg.AddItemString ( root, 'alg', 'HS256' )
header = jg.GetJSONString()
header_blob = Blob ( header, EncodingUTF8! )
header_encoded = of_base64urlencode( header_blob )

root = jg.createjsonobject( )

expiresAt = DateTime(Today(),RelativeTime ( Now(), 60 * 5 ))
expires = of_getunixepoch(expiresAt)
jg.AddItemNumber(root,'exp',expires )
jg.AddItemString ( root, 'iss', issuer )
jg.AddItemString(root,'mth','GET' )
jg.AddItemString(root,'sub','/v1/stores' )

body = jg.GetJSONString()
body_blob = Blob ( body, EncodingUTF8! )
body_encoded = of_base64urlencode ( body_blob )

CrypterObject crypter
crypter = create CrypterObject
sig_data = header_encoded + '.' + body_encoded
sig_data_blob = Blob ( sig_data, EncodingUTF8!)
key_blob = Blob ( secretKey, EncodingUTF8!)
sig_data_sha = crypter.hmac( HMACSHA256!, sig_data_blob, key_blob)
sig_encoded = of_base64urlencode ( sig_data_sha )

JWTToken = sig_data + '.' + sig_encoded

The code is largely the same as for DocuSign, except that SallingGroup uses symmetric signing using a shared key rather than asymmetric signing with a private key.   The main difference between the two scenarios is that in the case of Scenerio 2 you make a call to receive a token:

client.SendPostRequest( 'https://account-d.docusign.com/oauth/token', request_data, JWTToken )

 And then use that in subsequent requests:

client.SetJwtToken( JWTToken )
client.SendGetRequest(url, ls_response)

In Scenario 3 you don't make a separate call to request the token, you just include the JWT request as a request header on every call:

client.SetRequestheader( 'Authorization', 'JWT ' + JWTToken )
client.SendGetRequest( url, ls_response)

 

 

We use cookies which are necessary for the proper functioning of our websites. We also use cookies to analyze our traffic, improve your experience and provide social media features. If you continue to use this site, you consent to our use of cookies.