Saturday 13 January 2024

Splitters Helpers

 - or how to find ways to add own helpers and keeping existing helpers.



Wanted to do a pun on Finders Keepers, but failed - so this post is a sample on how to add your own record helper - in this case for the string type.

Delphi/Object Pascal does not allow for multiple helpers for the same type to be available at the same time.

So in my little example I wanted a string function that would split a string by a char, but up till a given max length.

A scenario for that use could be if you need to feed a system that has limited fixed size fields, spanning over more fields - CompanyName1, CampanyName2 only being 30 chars each. And for readability and UI, you also need to consider not to split mid-word.

To overcome the issue with  the one helper active per type, defined your own matching type:

MyString = type string;

Define the new record helper for that type with its function:

MyStringHelper = record helper for MyString
  function SplitMaxProper(const ch: char; const len: Integer): TArray<string>;
end;

Since we also want to use the normal string helpers within the helper function, we need to do some casting when referring to the helpers type itself:

function MyStringHelper.SplitMaxProper(const ch: char; const len: Integer): TArray<string>;
var
  sl: TStringList;
begin
  var sidx := 0;
  var done := False;
  sl := TStringList.Create;
  try
    while (not done) do
    begin
      var delta := string(Self).LastIndexOf(ch, sidx+len, len);
      if (delta = -1) or (string(Self).Length-sidx <= len) then
      begin
        sl.Add(Trim(string(Self).Substring(sidx)));
        done := True;
      end
      else
        sl.Add(Trim(string(Self).Substring(sidx, delta-sidx)));
      sidx := delta;
    end;
    Result := sl.ToStringArray;
  finally
    sl.Free;
  end;
end;

And when using the new string helper we do need to cast to get to our new function:

procedure TForm6.btnSplitClick(Sender: TObject);
begin
  meSplitText.Clear;
  var len := StrToInt(edSplitLength.Text);
  var str := MyString(edTextToSplit.Text);
  meSplitText.Lines.AddStrings(str.SplitMaxProper(#32, len));
end;

Disclaimer: The code is done so that it does fix better in the narrow width of my blog layout, and the function might also need some optimization :)

A note on the LastIndexOf string helper functions, the current official documentation seems to me a bit unclear:

StartIndex specifies the offset in this 0-based string where the LastIndexOf method begins the search, and Count specifies the end offset where the search ends.

Do remember that the search in this case, of cause goes from right to left - so StartIndex would normally be the length of the string.

I started with a Stephen King book title pun, so I should also comment on the quote used in the image shown - a quote from the brilliant author Tom Holt and the book The Portable Door - which is highly recommended, even if you have watched the movie adaptation, which is also good - but different - since adapting Tom Holt's books is no trivial task.

/Enjoy


Tuesday 2 January 2024

Just Ping someone!

- or a component for Ping Identity's authentication within your Delphi application.


It has often been the rule that native applications either authenticate the user by current OS user or by an application-centric user and role model - but that might for multiple reasons not be good enough anymore.

The old AD or LDAP lookups are being replaced by cloud IAM platforms, to control and secure the authentication of the identity of the user.

After the user is authenticated (and is authorised "access" your application), the application-centric roles flow can continue as-is.

One of these IAM cloud provides is PingIdentity, and they have a fairly extensive Postman collection for their PingOne Platform API - found here. Their developer documentation is also very useful - found here.

I have previously used various ways against various providers, but it seemed that when testing against PingOne with MFA enabled (Multi-Factor Authentication - which might include the annoying phone thing - that everyone uses) - I was hit by either a CORS issue or something else.

When using an OAuth2/OpenID Connect authentication, it does require that you setup and use a redirect uri, to tell the provider who is locally listening/waiting for the "response".

The idea with this type of authentication, is that the flow is handled securely within a web browser session, and the client does at no point in time know the login credentials - only when the user is authenticated do we need an "id token". So no "local" storage/handling of "passwords".

The listener part and possible timeouts has always annoyed me, so it seemed based on the flow, that I could handle it differently by just intercepting the local redirect call with the auth code - when the user is authenticated on the PingOne side - to get the id token needed.

Implementation

The TPingOneAuth component is a descendant of standard TWebBrowser overriding the IDocHostUIHandler interface, and adding a some properties.

The GetHostInfo override is to ensure that all redirects in the browser component is triggering the OnBeforeNavigate2 event - which then on the redirect auth code call, will get the wanted id token.

function TPingOneAuth.GetHostInfo(var pInfo: TDocHostUIInfo): HRESULT;
begin
  pInfo.cbSize := SizeOf(pInfo);
  pInfo.dwFlags := 0;
  pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_NO3DBORDER;
  pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_THEME;
  pInfo.dwFlags := pInfo.dwFlags or DOCHOSTUIFLAG_ENABLE_REDIRECT_NOTIFICATION;
  Result := S_OK;
end;

The id token is then parsed using Pablo Rossi's brilliant JOSE and JWT library, that might as well have been done using a call to one of the PingOne's Token Introspection endpoints.

Since we do need an user id - the OpenID Connect scope must include profile also.

To parse the custom claim a custom TJWTClaims class is added, containing the profile claims we want to read.

TPingOneClaims = class(TJWTClaims)
// Adding some given by the OpenID Connect scope: profile
private
  function GetPreferredUsername: string;
  procedure SetPreferredUsername(const Value: string);
  function GetGivenName: string;
  procedure SetGivenName(const Value: string);
  function GetFamilyName: string;
  procedure SetFamilyName(const Value: string);
public
  property PreferredUsername: string read GetPreferredUsername write SetPreferredUsername;
  property GivenName: string read GetGivenName write SetGivenName;
  property FamilyName: string read GetFamilyName write SetFamilyName;
end;


Usage

Install the TPingOneAuth component in the Delphi IDE, and set the library path - business as usual.

There is a small sample application in the GitHub repo, but steps are at follows:

Drop or Create the TPingOneAuth on a form, setting following properties:

  • AuthEndpoint: /as/authorize
  • AuthPath: https://auth.pingone.eu/
  • ClientId:
  • ClientSecret:
  • EnvironmentId:
  • RedirectUri:
  • ResponseType: code
  • Scope: openid profile
  • TokenEndpoint: /as/token
All these values are found in the PingOne console, under the application you setup as a "authorization" reference to your native Delphi application.


Add code to the OnAuthenticated (and OnDenied) event(s) - were on a successful authentication the public properties UserId and GreetName can be useful.

To start the authentication just call the Authorize method - and with MFA enabled, you will see the prompt bellow, once you initially have given your credentials.


After finding your phone, and having swiped to authenticate in the PingOne mobile companion app, the OnAuthenticated event is called.


The code for the component and the sample is found here.

A thing to note:

When having MFA enabled - and you need to pair a device the first time around - the url Ping provides does contain some "strict mode" Javascript that IE will prompt you about twice, the solution to this is to set the SelectedEngine property to EdgeIfAvailable or EdgeOnly and deploy the EdgeView2 Runtime (WebView2Loader.dll) which can be found in GetIt as the EdgeView2 SDK.

/Enjoy