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 method – For REST methods return one row
  • Submit method - sends request and returns data in one operation
  • SetRequestHeader behavior modified
  • GZIP compression handled automatically
  • Send[Method]Request, where method can be Get, Put, Post, Patch, Delete
  • Get and Set OAuth token methods
  • Get and set JWT token methods

 

 

RetrieveOne

  • Similar to the Retrieve method, but for calling services that only return one row
  • If the result set contains more than one row, only the first row is added to the DataWindow

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

Submit

  • Sends data from a DataWindow, DataStore or JSONPackage object to a server.
  • It only uses the POST method
  • Primarily intended for sending updates from a DataWindow/DataStore to a SnapObjects based REST API
  • Syntax
    • objectname.Submit(string urlName, ref string response, DWControl dwObject{, boolean format})
    • objectname.Submit(string urlName, ref string response, DWControl dwObject {,DWBuffer dwbuffer}, boolean changedonly, boolean format)
    • objectname.Submit(string urlName, ref string response, DWControl dwObject, boolean primarydata, boolean filterdata, boolean deletedata, boolean dwcdata {, boolean format})
    • objectname.Submit(string urlName, ref string response, DWControl dwObject, DWBuffer dwbuffer{,long startrow{, long endrow{, long startcol{, long endcol}}}} {, boolean format})
    • objectname.Submit(string urlName, ref string response, ref JsonPackage package)
    • Where:
      • urlName – the url for the service
      • response – the data returned from the service
      • dwObject – DataWindow or DataObject
      • format - true for DataWindow JSON or ModelStore JSON, false for Plain JSON
      • dwBuffer – the DataWindow buffer to sent if you just want to send one buffer. Otherwise all are sent.
      • changed-only – whether to send just changed rows or all of them
      • primarydata, filterdata, deletedata, dwcdata - Boolean flags indicating whether that data should be sent
      • startrow, endrow, startcol, endcol – controls the range of data sent
      • package – the JSONPackage object to send data from

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

SetRequest Header

  • New optional replace argument
  • Syntax
    • client.SetRequestHeader ( string headerName, string headerValue, { Boolean replace } ):
      • TRUE – if the value already exists in the header, replace it
      • FALSE (default) – add the value even if it already exists in the header

GZip 

  • If the data is sent in gzip format, PowerBuilder automatically handles the decompression of the data
  • To enable, set the Accept-Encoding header to gzip on the request to the service
    • If the service supports it, the data will be sent back in gzip format
    • If not, the data will be sent back in JSON format

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

  • Request is sent and response received in single method
  • Improvement on RESTClient method: Submit - limited to POST
  • Improvement on HTTPClient method: SendRequest – Only sends request, separate method has to be used to get result
  • Syntax
    • SendGetRequest(string urlName, ref string response)
    • SendPostRequest(string urlName, string data, ref string response)
    • SendPatchRequest(string urlName, string data, ref string response)
    • SendPutRequest(string urlName, string data, ref string response)
    • SendDeleteRequest(string urlName{, string data }, ref string response) 

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.

  • GetOAuthToken ( TokenRequest request, ref String token )
    • Gets a token from the service
      • request – Same object used by OAuthClient
      • token – Token returned by service
  • SetOAuthToken ( String token )
    • Sets the token to be used for requests from the RESTClient

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:

  • Header: indicating what algorithm is used to sign the token
  • Payload: various claims about the user and how long the token request is good for
  • Signature: the header and payload are base64url encoded, concatenated with a period between them, and then signed, usually with HMAC-SHA256 (symmetric) or RSA-SHA256 (asymmetric).

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.

  • client.GetJWTToken( String url, String json_data, ref String token )
  • client.SetJWTToken( String token )

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)

 

 

Comments (9)

  1. Daniel Johnson

This is all really great.

Do we have any idea if the RestClient will ever be supported by the PowerServer Web/Mobile deployment?

  Attachments
Your account does not have privileges to view attachments in the comment
  Comment was last edited about 4 years ago by Daniel Johnson Daniel Johnson
  1. Daniel Johnson    Kai Zhao @Appeon

Thanks for the reply. It would be helpful to see an example, as the most basic RestClient example does not seem to retrieve data when deployed to PowerServer, even when the gzip header is not used.

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Sally Li    Daniel Johnson
  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Daniel Johnson    Sally Li

Thank you, Sally -- this is exactly what I was looking for.

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. muhammad nur akbar

i will try..thanks

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Thorsten Kummer

Dear Bruce,

is there a chance that you support me to get it done (Oauth2) against a Microsoft 365 system.
Currently I am struggling to get the code param from the response of the authorize call.

Best regards
from Germany
Thorsten

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Bruce Armstrong    Thorsten Kummer

Which oauth protocol are you using? Are you using something like response_type=code? If so, what you need to do is have your PowerBuilder app listen on a port and send the hostname and port in the redirect_uri parameter. I've got an example I'm working on where I use that approach for openid connect (a layer on top of OAuth calls). I use Roland Smith's winsock code to have the client listen for the response. https://www.topwizprogramming.com/freecode_winsock.html. My example isn't quite complete (still working on the SSO part where the server would recognize the user already has a session and skips the login, one of the openid features), so I'm not quire ready to share it yet.

If that isn't the issue tell me more about what isn't working.

  Attachments
Your account does not have privileges to view attachments in the comment
 
  1. Bruce Armstrong    Bruce Armstrong

A belated update. I got OpenID working and the sample code is at: https://github.com/bruce-armstrong/powerbuilder_openid

  Attachments
Your account does not have privileges to view attachments in the comment
 
There are no comments posted here yet