Azure Active Directory (AD) B2C

Preparations

Before making changes to the PowerBuilder client app, let's follow the steps below to make sure 1) the PowerBuilder application can run successfully, 2) the app has been deployed as an installable cloud app successfully, and 3) the PowerServer C# solution has been successfully generated.

In this tutorial, we will take Sales Demo as an example.

Step 1: Select Windows Start | Appeon PowerBuilder 2022, and then right-click Example Sales App and select More | Run as administrator.

Step 2: When the SalesDemo workspace is loaded in the PowerBuilder IDE, click the Run button in the PowerBuilder toolbar.

Step 3: When the application main window is opened, click the Address icon in the application ribbon bar and make sure data can be successfully retrieved.

Step 4: Create and configure a PowerServer project for the Sales Demo app (detailed instructions are provided in Quick Start > Guide 1).

IMPORTANT: In the .NET Server page > Advanced tab, select Use external Azure Active Directory service from the Auth Template list box.


Step 5: Deploy the application as an installable cloud app and make sure the installable cloud app can run successfully and the PowerServer C# solution is generated.

The PowerServer C# solution provides templates for configuring the address of the authentication server like Azure AD or Azure AD B2C.

  • Authentication.json contains the settings for enabling the authentication feature ("PowerServer:EnableAuthentication") and specifying the address of the authentication server ("Authentication:Authority"). The PowerServer Web APIs will validate the token against the authentication server; and if validation is successful, data will be obtained from the database.

    The "PowerServer:EnableAuthentication" setting is set to true by default. Setting it to false will turn off the authentication feature.

    The "Authentication:Authority" setting is set for JWT by default; you can set the address of Azure AD and Azure AD B2C.


Creating an Azure AD B2C tenant

The following outlines the key steps:

Step 1: Set up an Azure AD B2C tenant.

For detailed instructions, please refer to Tutorial: Create an Azure Active Directory B2C tenant.

Step 2: Register a web application in Azure Active Directory B2C.

For detailed instructions, please refer to Tutorial: Register a web application in Azure Active Directory B2C.

Step 3: Create user flows and custom policies in Azure Active Directory B2C.

For detailed instructions, please refer to Tutorial: Create user flows and custom policies in Azure Active Directory B2C.

Step 4: Manage your Azure Active Directory B2C tenant.

For detailed instructions, please refer to Tutorial: Manage your Azure Active Directory B2C tenant.

During the process of creating the tenant, write down the following information:

  • Tenant ID: for example, ed7837a1-96e2-4243-8ac8-172bc467f42c

  • Primary domain: for example, powerserverb2c.onmicrosoft.com

  • Application (client) ID: for example, ddaf52bf-1039-4f7a-ab85-51a219c1d4d7

  • Client secret: for example, VgJo8X8qu4nCW.gf.FRxe.lhBZfE9F6.MA

  • Application ID URI: for example, https://powerserverb2c.onmicrosoft.com/ddaf52bf-1039-4f7a-ab85-51a219c1d4d7

  • Scope: for example, https://powerserverb2c.onmicrosoft.com/ddaf52bf-1039-4f7a-ab85-51a219c1d4d7/.default

The above information will be used later.

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:

  • Gets the password and the authorization code from the application login window, then authenticates it with the Azure AD B2C tenant and gets a token.

  • 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
//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: Does not support Client Credentials anymore.

  • 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 return the username and password to the f_Authorization() function.

//Integer f_Authorization() for password
//UserName & Password from 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 = "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 return username & password according to actual needs 
//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 application Open event.

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

//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


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.

The INI file specifies the URL for requesting the token from Azure AD B2C.

[Setup]
TokenURL=https://login.microsoftonline.com/powerserverb2c.onmicrosoft.com/oauth2/v2.0/token

To support "scenario 3" which supports Authorization Code (GrantType="authorization_code") and gets the username and password from the login window, you need to add the following section to the CloudSetting.ini file and set the URL accordingly.

[Setup]
TokenURL=https://powerserverb2c.b2clogin.com/powerserverb2c.onmicrosoft.com/B2C_1_ps2/oauth2/v2.0/token
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 Client App page.


Step 2: Select RESTClient Support and WebBrowser Support (Used for scenario 3) under the Runtime files group in the Client App page > Advanced tab.


Step 3: Double check the URL of the PowerServer Web APIs in the .NET Server page.

Make sure the port number is not occupied by any other program. You can execute the command "netstat -ano | findstr portnumber" to check if the port number is occupied by any other program. For details, refer to Choosing an appropriate port number.


Step 4: Double check that Use external Azure Active Directory service is selected from the Auth Template list box in the .NET Server page > Advanced 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.

Modifying the authentication template

The Azure AD B2C server address must be provided so that the PowerServer Web APIs can use it to validate the token passed from the client. And if validation is successful, it can get data from the database.

Note

The authentication template will be restored if the "Auth Template" option is changed and the PowerServer C# solution is re-built from the PowerBuilder IDE. Therefore, do not change the "Auth Template" option if you have made changes to the template in the solution.

Get the Domain, TenantId and ClientId from Creating an Azure AD B2C tenant, then open the Authentication.json file to modify the authentication template.

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "powerserverb2c.onmicrosoft.com",
    "TenantId": "ed7837a1-96e2-4243-8ac8-172bc467f42c",
    "ClientId": "ddaf52bf-1039-4f7a-ab85-51a219c1d4d7"
  }

Scripts for scenario 3:

"AzureAd": {
    "Instance": "https://powerserverb2c.b2clogin.com/powerserverb2c.onmicrosoft.com/",
    "Domain": "powerserverb2c.onmicrosoft.com",
    "TenantId": "B2C_1_ps2",
    "ClientId": "ddaf52bf-1039-4f7a-ab85-51a219c1d4d7"
  }