In this section, we will modify the PowerBuilder application settings and source code to achieve the following results:
-
Gets a token from the Azure AD B2C server
-
Creates the user session that includes the token.
-
Refreshes the token when necessary.
The token URL is where you can get a token.
Instead of hard-coding the token URL in the application scripts, we will specify the token URL in an INI file, so that the URL can be modified without affecting the scripts.
Step 1: Create an INI file in the same location as the PBT file and name it CloudSetting.ini.
Step 2: Specify the URL for requesting the token from Azure AD B2C in the INI file.
[Setup] TokenURL=https://login.microsoftonline.com/powerserverb2c.onmicrosoft.com/oauth2/v2.0/token
To support Authorization Code (GrantType="authorization_code") (in "scenario 3"), you need to specify the URL accordingly in the CloudSetting.ini file.
[Setup] TokenURL=https://powerserverb2c.b2clogin.com/powerserverb2c.onmicrosoft.com/B2C_1_ps2/oauth2/v2.0/token
Note
The INI file must be added as external file in the PowerServer project painter, in order to be deployed with the application. For more details, refer to the next section "Modifying and re-deploying the PowerServer project".
To modify the PowerBuilder application source code to get a token from the authentication server:
Step 1: Declare the following global variables.
//Token expiresin Long gl_Expiresin //Refresh token clockskew Long gl_ClockSkew = 3 //Refresh token string gs_RefreshToken
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 authorization server and then gets the identity token from the HTTP Authorization header.
Add scripts to the f_Authorization() function to implement the following scenario:
-
Scenario 1: Scenario 1 is deleted because Client Credentials is no longer supported.
-
Scenario 2: Supports Resource Owner Password (GrantType="password") and gets the username and password from a login window.
-
Scenario 3: Supports Authorization Code (GrantType="authorization_code") and gets the authorization code from a login window.
Scripts for scenario 2:
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 Azure AD B2C to get the token, and when the token expires, the login window displays for the user to input the username and password again.
The following scripts hard code the username and password instead of getting them from the login window. You can change the scripts to use the login window after you implement the login window and pass the username and password to the f_Authorization() function.
Note
If the ClientSecret value contains the escape character "~", you will have to replace it with "~~", like the example below.
//Integer f_Authorization() for password //TokenURL is obtained from the INI file //UserName & Password are hardcoded in the script 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 = "ddaf52bf-1039-4f7a-ab85-51a219c1d4d7" ltr_Request.clientsecret = "VgJo8X8qu4nCW.gf.FRxe.lhBZfE9F6.MA" ltr_Request.scope = "https://powerserverb2c.onmicrosoft.com/ddaf52bf-1039-4f7a-ab85-51a219c1d4d7/.default" ltr_Request.granttype = "password" //login window can be implemented to pass username & password //Open(w_login) //Return UserName & Password ls_UserName = "appeontest" ls_UserPass = "Test2008aa" 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:
When the application starts, the client ID and secret stored in the application as well as the authorization code from the login window will be sent to Azure AD B2C to get the token, and when the token expires, it automatically refreshes the token.
In the PowerBuilder application, the login window is loaded through the web browser, and you can obtain the user name after login: Test1@appeon.com/Test2a.
Scripts for scenario 3.1:
The authorization function is used to get the access token.
//Integer f_Authorization() for authorization_code //Code from login window OAuthClient loac_Client TokenRequest ltr_Request TokenResponse ltr_Response String ls_URL, ls_URL_Code, ls_code String ls_TokenType, ls_AccessToken String ls_type, ls_description, ls_uri, ls_state String ls_ClientID, ls_ClientSecret, ls_Scope, ls_RedirectUri Integer li_Return, li_rtn Long ll_Rand string ls_verify_code = 'exampleThisIsntRandomButItNeedsToBe43CharactersLong' li_rtn = -1 ls_url = profilestring("CloudSetting.ini","setup","TokenURL","") //login window ll_Rand = Rand(32767) ls_ClientID = "ddaf52bf-1039-4f7a-ab85-51a219c1d4d7" ls_Scope = "https://powerserverb2c.onmicrosoft.com/ddaf52bf-1039-4f7a-ab85-51a219c1d4d7/All.Read" ls_RedirectUri = "https://jwt.ms" ls_Url_code = "https://powerserverb2c.b2clogin.com/powerserverb2c.onmicrosoft.com/B2C_1_ps2/oauth2/v2.0/authorize?client_id=ddaf52bf-1039-4f7a-ab85- 51a219c1d4d7&response_type=code&redirect_uri=https://jwt.ms&response_mode=query&scope=https://powerserverb2c.onmicrosoft.com/ddaf52bf-1039-4f7a-ab85- 51a219c1d4d7/All.Read&state=arbitrary_data_you_can_receive_in_the_response&code_challenge=exampleThisIsntRandomButItNeedsToBe43CharactersLong&code_challenge_method=plain" OpenWithParm (w_login,ls_Url_code) ls_code = Message.Stringparm If Len (ls_code) < 1 Then Return li_rtn If Pos(ls_code, "code=") < 0 Then return li_rtn ls_code = Mid (ls_code, pos(ls_code,"code=") + 5) //TokenRequest ltr_Request.Method = "POST" ltr_Request.tokenlocation = ls_url ltr_Request.granttype = "authorization_code" ltr_Request.clientid = ls_ClientID ltr_Request.ClearParams() ltr_Request.AppendParam("grant_type","authorization_code") ltr_Request.AppendParam("client_id", ls_ClientID) ltr_Request.AppendParam("scope", ls_Scope) ltr_Request.AppendParam("code", ls_code) //ltr_Request.AppendParam("redirect_uri", ls_RedirectUri) ltr_Request.AppendParam("code_verifier", ls_verify_code) ////=ThisIsntRandomButItNeedsToBe43CharactersLong ltr_Request.ClearHeaders() ltr_Request.SetHeader("Content-Type","application/x-www-form-urlencoded") 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() gs_RefreshToken=ltr_Response.GetRefreshToken() //Application Set Authorization Header Getapplication().SetHttpRequesTheader("Authorization", ls_TokenType + " " +ls_AccessToken, true) //Set Global Variables gl_Expiresin = ltr_Response.getexpiresin() //gs_Auth_Code = ls_code 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.2:
When the application starts, the application displays the login window. If you don't have an account, click the link "Sign up now " to register a new account and log in.
forward global type w_login from window end type type wb_1 from webbrowser within w_login end type end forward global type w_login from window integer width = 3643 integer height = 2332 boolean titlebar = true string title = " login" boolean controlmenu = true windowtype windowtype = response! long backcolor = 67108864 string icon = "AppIcon!" boolean center = true wb_1 wb_1 end type global w_login w_login type variables string is_url end variables on w_login.create this.wb_1=create wb_1 this.Control[]={this.wb_1} end on on w_login.destroy destroy(this.wb_1) end on event open; is_url = message.StringParm wb_1.Navigate(is_url) end event type wb_1 from webbrowser within w_login integer x = 14 integer y = 28 integer width = 3598 integer height = 2192 end type event resourceredirect;String ls_locationUrl ls_locationUrl = RedirectUrl If Pos (ls_locationUrl, "code=") > 0 Then CloseWithReturn(Parent, ls_locationUrl) end If end event
Scripts for scenario 3.3:
The refresh token function is used to get new access tokens. In this way, the expiration time of the access token can be shortened to ensure security, and the user will not be required to log in again due to frequent expiration.
//f_refreshtoken() OAuthClient loac_Client TokenRequest ltr_Request TokenResponse ltr_Response String ls_URL, ls_URL_Code, ls_code String ls_TokenType, ls_AccessToken String ls_type, ls_description, ls_uri, ls_state String ls_ClientID, ls_Scope Integer li_Return, li_rtn string ls_verify_code = 'exampleThisIsntRandomButItNeedsToBe43CharactersLong' li_rtn = -1 ls_url = profilestring("CloudSetting.ini","setup","TokenURL","") //login window ls_ClientID = "ddaf52bf-1039-4f7a-ab85-51a219c1d4d7" ls_Scope = "https://powerserverb2c.onmicrosoft.com/ddaf52bf-1039-4f7a-ab85-51a219c1d4d7/All.Read" //TokenRequest ltr_Request.Method = "POST" ltr_Request.tokenlocation = ls_url ltr_Request.granttype = "refresh_token" ltr_Request.clientid = ls_ClientID ltr_Request.ClearParams() ltr_Request.AppendParam("grant_type","refresh_token") ltr_Request.AppendParam("client_id", ls_ClientID) ltr_Request.AppendParam("scope", ls_Scope ) ltr_Request.AppendParam("refresh_token", gs_RefreshToken) ltr_Request.AppendParam("code_verifier", ls_verify_code) ////=ThisIsntRandomButItNeedsToBe43CharactersLong ltr_Request.ClearHeaders() ltr_Request.SetHeader("Content-Type","application/x-www-form-urlencoded") 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() gs_RefreshToken=ltr_Response.GetRefreshToken() //Application Set Authorization Header Getapplication().SetHttpRequesTheader("Authorization", ls_TokenType + " " +ls_AccessToken, true) //Set Global Variables 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)
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.
//Authorization f_Authorization()
Scripts for scenario 3:
//Authorization f_refreshtoken()
When displayed in the source editor, the Timer event looks like this:
event timer;//Authorization f_Authorization() end event
Step 4: 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
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: Select the "Dynamic session parameters" option 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 Dynamic session parameters option and click Apply.)
Step 2: Call the BeginSession function in the application Open event.
Place the following scripts before the database connection is established. The scripts create the user session that includes the token information.
Note
1) The BeginSession function works only when the "Dynamic session parameters" option is selected.
2) Do not call BeginSession more than one time, otherwise, it will return -1 (indicating session already exists).
//Authorization If f_Authorization() <> 1 Then Return End If //StartSession 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 //Refresh token for timing If gl_Expiresin > 0 And (gl_Expiresin - gl_ClockSkew) > 0 Then //Timer = Expiresin - ClockSkew //7200 - 3 timing_1.Start(gl_Expiresin - gl_ClockSkew) End If //Connect db