Modifying the PowerBuilder client app

Purpose

In this section, we will modify the PowerBuilder application source code and the PowerServer project settings to achieve the following results:

  • Sends the user credentials and/or password to the OAuth server and gets a token from the OAuth server if authentication is successful.

  • Uses the token to access data from the PowerServer Web API.

  • Refreshes the token when necessary.

Add scripts

Step 1: Declare the following global variables.

//Token expiresin
Long gl_Expiresin
//Refresh token clockskew 
Long gl_ClockSkew = 3

Step 2: Define a global function and name it f_Authorization().

Select from menu File > New; in the New dialog, select the PB Object tab and then select Function and click OK to add a global function.

This global function uses the HTTP Post method to send the user credentials to the authentication server and then gets the token from the HTTP Authorization header.

Add scripts to the f_Authorization() function according to the following scenarios:

  • Scenario 1: Supports Client Credentials (GrantType="client_credentials") and gets the client ID and secret from the application.

  • Scenario 2: Supports Resource Owner Password (GrantType="password") and gets the username and password from a login window.

  • Scenario 3: Supports Resource Owner Password (GrantType="password") and gets the username and password from the INI file.

Scripts for scenario 1:

Supports Client Credentials (GrantType="client_credentials") and gets the client ID and secret from the application.

When the application starts, the application uses the client ID and secret stored in the application to get the token from the OAuth server, and when the token expires, it automatically refreshes the token.

//integer f_Authorization() for client_credentials
//The URL for requesting token is specified in the INI file
OAuthClient    loac_Client
TokenRequest   ltr_Request
TokenResponse  ltr_Response
String ls_url, ls_UserName, ls_UserPass
String ls_TokenType, ls_AccessToken
String ls_type, ls_description, ls_uri, ls_state
Integer li_Return, li_rtn

li_rtn = -1
ls_url = profilestring("CloudSetting.ini","setup","TokenURL","")

//TokenRequest
ltr_Request.tokenlocation = ls_url
ltr_Request.Method = "POST"
ltr_Request.clientid = "client"
ltr_Request.clientsecret = "511536EF-F270-4058-80CA-1C89C192F69A"
ltr_Request.scope = "serverapi"
ltr_Request.granttype = "client_credentials"

loac_Client = Create OAuthClient
li_Return = loac_Client.AccessToken( ltr_Request, ltr_Response )
If li_Return = 1 and ltr_Response.GetStatusCode () = 200 Then
 ls_TokenType = ltr_Response.gettokentype( )
 ls_AccessToken = ltr_Response.GetAccessToken()
 //Application Set Authorization Header
 Getapplication().SetHttpRequesTheader("Authorization", ls_TokenType + " " +ls_AccessToken, true)
 //Set Global Variables
 gl_Expiresin = ltr_Response.getexpiresin( )
 li_rtn = 1
Else
 li_Return = ltr_Response.GetTokenError(ls_type, ls_description, ls_uri, ls_state)
 MessageBox( "AccessToken Falied", "Return :" + String ( li_Return ) + "~r~n" + ls_description )
End If

If IsValid ( loac_Client ) Then DesTroy ( loac_Client )

Return li_rtn

Scripts for scenario 2:

Supports Resource Owner Password (GrantType="password") and gets the username and password from a login window.

When the application starts, the client ID and secret stored in the application as well as the username and password from the login window will be sent to the OAuth server to get the token, and when the token expires, the login window displays for the user to input the username and password again.

The username and password will be passed to the OAuth server and validated against the DefaultUserStore.cs file (to validate against a database or LDAP server rather than DefaultUserStore.cs, refer to Validate username and password against a database or Validate username and password against an LDAP server for more information).

The following scripts will work only after you implement a login window and return the username and password to the f_Authorization() function.

//Integer f_Authorization() for password from login window
//The URL for requesting token is specified in the INI file
//username & password are passed from the login window
OAuthClient    loac_Client
TokenRequest   ltr_Request
TokenResponse  ltr_Response
String ls_url, ls_UserName, ls_UserPass
String ls_TokenType, ls_AccessToken
String ls_type, ls_description, ls_uri, ls_state
Integer li_Return, li_rtn

li_rtn = -1
ls_url = profilestring("CloudSetting.ini","setup","TokenURL","")

//TokenRequest
ltr_Request.tokenlocation = ls_url
ltr_Request.Method = "POST"
ltr_Request.clientid = "ro.client"
ltr_Request.clientsecret = "08692CED-944D-4DA9-BFEF-0FE503C203AC"
ltr_Request.scope = "serverapi"
ltr_Request.granttype = "password"

//login window can be implemented to return username & password according to actual needs 
//Open(w_login) 
//Return UserName & Password

If IsNull ( ls_UserName ) Or Len ( ls_UserName ) = 0 Then
 MessageBox( "Tips", "UserName is empty!" )
 Return li_rtn
End If
If IsNull ( ls_UserPass ) Or Len ( ls_UserPass ) = 0 Then
 MessageBox( "Tips", "Password is empty!" )
 Return li_rtn
End If

ltr_Request.UserName = ls_UserName
ltr_Request.Password = ls_UserPass

loac_Client = Create OAuthClient
li_Return = loac_Client.AccessToken( ltr_Request, ltr_Response )
If li_Return = 1 and ltr_Response.GetStatusCode () = 200 Then
 ls_TokenType = ltr_Response.gettokentype( )
 ls_AccessToken = ltr_Response.GetAccessToken()
 //Application Set Authorization Header
 Getapplication().SetHttpRequesTheader("Authorization", ls_TokenType + " " +ls_AccessToken, true)
 //Set Global Variables
 gl_Expiresin = ltr_Response.getexpiresin( )

 li_rtn = 1
Else
 li_Return = ltr_Response.GetTokenError(ls_type, ls_description, ls_uri, ls_state)
 MessageBox( "AccessToken Falied", "Return :" + String ( li_Return ) + "~r~n" + ls_description )
End If

If IsValid ( loac_Client ) Then DesTroy ( loac_Client )

Return li_rtn

Scripts for scenario 3:

Supports Resource Owner Password (GrantType="password") and gets the username and password from the INI file.

When the application starts, the client ID and secret stored in the application as well as the username and password from the INI file will be sent to the OAuth server to get the token, and when the token expires, it automatically refreshes the token.

The username and password will be passed to the OAuth server and validated against the DefaultUserStore.cs file (to validate against a database or LDAP server rather than DefaultUserStore.cs, refer to Validate username and password against a database or Validate username and password against an LDAP server for more information).

//Integer f_Authorization() for password from INI file
//The URL for requesting token is specified in the INI file
//username & password are passed from the INI file
OAuthClient    loac_Client
TokenRequest   ltr_Request
TokenResponse  ltr_Response
String  ls_url, ls_UserName, ls_UserPass
String  ls_TokenType, ls_AccessToken
String  ls_type, ls_description, ls_uri, ls_state
Integer  li_Return, li_rtn

li_rtn = -1
ls_url = profilestring("CloudSetting.ini","setup","TokenURL","")

//TokenRequest
ltr_Request.tokenlocation = ls_url
ltr_Request.Method = "POST"
ltr_Request.clientid = "YourClientIdThatCanOnlyRead"
ltr_Request.clientsecret = "yoursecret1"
ltr_Request.scope = "scope.readaccess"
ltr_Request.granttype = "password"

//From CloudSetting.ini
ls_UserName = ProfileString("CloudSetting.ini", "users", "userName", "")
ls_UserPass = ProfileString("CloudSetting.ini", "users", "userPass", "")
If IsNull ( ls_UserName ) Or Len ( ls_UserName ) = 0 Then
 MessageBox( "Tips", "UserName is empty!" )
 Return li_rtn
End If
If IsNull ( ls_UserPass ) Or Len ( ls_UserPass ) = 0 Then
 MessageBox( "Tips", "Password is empty!" )
 Return li_rtn
End If
ltr_Request.UserName = ls_UserName
ltr_Request.Password = ls_UserPass


loac_Client = Create OAuthClient
li_Return = loac_Client.AccessToken( ltr_Request, ltr_Response )
If li_Return = 1 and ltr_Response.GetStatusCode () = 200 Then
 ls_TokenType = ltr_Response.gettokentype( )
 ls_AccessToken = ltr_Response.GetAccessToken()
 //Application sets the authorization header
 Getapplication().SetHttpRequestHeader("Authorization", ls_TokenType + " " +ls_AccessToken, true)
 //Set the global variables
 gl_Expiresin = ltr_Response.getexpiresin( )
 
 li_rtn = 1
Else
 li_Return = ltr_Response.GetTokenError(ls_type, ls_description, ls_uri, ls_state)
 MessageBox( "AccessToken Failed", "Return: " + String ( li_Return ) + "~r~n" + ls_description )
End If

If IsValid ( loac_Client ) Then DesTroy ( loac_Client )

Return li_rtn

Step 3: Insert a timing object (timing_1) to the application and add the following scripts to the Timer event of timing_1.

1) Open the application object and then select from menu Insert > Object > Timing to add a timing object to the application.

2) Add the following scripts to the Timer event of timing_1.

//Authenticates the user
f_Authorization()

When displayed in the source editor, the Timer event looks like this:

event timer;//Authenticates the user
f_Authorization()
end event


Step 4: Add the following scripts to the application Open event.

Place the scripts before the database connection is established. The scripts get the token from the OAuth server and then start the user session (using the BeginSession function) to include the token information in the session.

//Authenticates the user and returns the token
If f_Authorization() <> 1 Then
 Return
End If

//Starts the session
long ll_return
Try
 ll_return = Beginsession()
 If ll_return <> 0 Then
  Messagebox("Beginsession Failed:" + String(ll_return), GetHttpResponseStatusText())
 End if
Catch ( Throwable ex)
 MessageBox( "Throwable", ex.GetMessage())
 Return
End Try

//Refreshes the token for timing
If gl_Expiresin > 0 And (gl_Expiresin - gl_ClockSkew) > 0 Then
 //Timer = Expiresin - ClockSkew 
 //3600 - 3
 timing_1.Start(gl_Expiresin - gl_ClockSkew)
End If

//Connects to db


Step 5: Add the following scripts to the SystemError event.

The scripts will trigger the SystemError event when the session or license encounters an error; and if the token is invalid or expires, the scripts will call the f_Authorization function to get the token again.

Choose Case error.Number
 Case 220  to 229 //Session Error
  MessageBox ("Session Error", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
 Case 230  to 239 //License Error
  MessageBox ("License Error", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
 Case 240  to 249 //Token Error
  MessageBox ("Token Error", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
  //Authorization
  f_Authorization()
 Case Else
  MessageBox ("SystemError", "Number:" + String(error.Number) + "~r~nText:" + error.Text )
End Choose


Add an INI file

Create an INI file in the same location as the PBT file and name it CloudSetting.ini.

Specify the URL for requesting the token from the OAuth server in the CloudSetting.ini file. Notice that TokenURL points to the "/connect/token" API of the built-in OAuth server, and the OAuth server root URL (for example, http://localhost:5000/) is the same as the URL of PowerServer Web APIs. If you change the PowerServer Web API URL, change the root URL here accordingly.

[Setup]
TokenURL=http://localhost:5000/connect/token

To support "scenario 3" which supports Resource Owner Password (GrantType="password") and gets the username and password from the INI file, you need to add the following section to the CloudSetting.ini file and set the user name and password accordingly.

[users]
userName=alice
userPass=alice

Start session manually by code

By default, the user session is automatically created when the application starts; and the session includes no token. For the session to include the token, the session must be started manually by code instead of automatically.

To start the session manually by code,

Step 1: Enable "Begin session by code" in the PowerBuilder IDE. (Steps: Open the application object painter, click Additional Properties in the application's Properties dialog; in the Application dialog, select the PowerServer tab and then select the Begin session by code option and click Apply.)

After this option is enabled, when the BeginSession function in the application Open event is called, it will create a session that includes the token information (See scripts in step 4 in "Add scripts").


Modify and re-deploy the PowerServer project

Step 1: Add the INI file CloudSetting.ini to the Files preloaded in uncompressed format section under the External Files tab.

Step 2: Select OAuth 2.0 Support and Compression Support under the Runtime tab.


Step 3: Double check the URL of the PowerServer Web APIs in the Web APIs tab. Make sure the port number is not occupied by any other program.

Tip: You can execute the command "netstat -ano | findstr portnumber" to check if the port number is occupied by any other program.

The built-in OAuth server will run at the same URL as the PowerServer Web APIs. If the PowerServer Web API URL is changed, change the OAuth server root URL accordingly in the INI file.

Step 4: Double check that Use built-in OAuth server is selected from the Auth Template list box in the Web APIs tab.


Step 5: Save the changes and deploy the PowerServer project (using the "Build & Deploy PowerServer Project" option) so that the above settings can take effect in the installable cloud app.