Delphi SOAP and WSSE

Let the adventures begin…
It has been a few years since I last looked at SOAP Web Services.
As far as I can tell, the builtin support in Delphi has not really changed and still sucks.
There is nothing RAD about it, IMHO.
For this I am using Delphi Tokyo 10.2.3.

For basic services Delphi will work okay out of the box.
However if you have nested schemas or duplicate identifiers you may start having issues.
I found the namespace support to be really sub-par.
I could not find a way to build an arbitrary header from classes already defined, the only way I found was to create a class that contained all the required options.

If you need to use any of the WS-* standards (i.e. WS-Security a.k.a. wsse) you are on your own.
There are third parties that have packages that support these standards (i.e. Eldos Blackbox).

To avoid any issues with third parties I went and built my own units to support WSSE.
I think I’ve made it expandable enough to support the various combinations of the Security header.
At present I have two files:

  • soap.wsse.pas which is a slightly modified import of the schema.
  • soap.wsse.security.userpass.pas which contains the support for a username and password.

You can download these files here.

I had to create a WSDL file that imported the schema.
I found directions online but the original file had a flaw in that not all of the namespaces were a complete URI in that they were missing the “http://” which resulted in the importer throwing an error saying “no node defined”.

< ? xml version="1.0" encoding="UTF-8"?>
< wsdl:definitions
      xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
      xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap"
      xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
      xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
      xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl"
      xmlns:wsi="http://ws-i.org/profiles/basic/1.1/xsd"
      xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
      xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" >
  < wsdl:types>
    < xsd:schema targetNamespace="dummy_ns" >
      < xsd:import namespace="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" schemaLocation="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"/>
    < /xsd:schema>
  < /wsdl:types>
< /wsdl:definitions>

With soap.wsse.pas I added some constants, removed a couple of classes, and tweaked some definitions.
The importer did a decent job but it lacks a few things for easy reading.
It lists the schema definitions that it did not include saying they are either predefined or it was unable to identify them but it does not say which reason was applicable for each entry. So you have to track down each of them manually.
IMO it should include the schema details when an item can not be identified.

I had to play around to get the namespaces defined the way I wanted them. I have yet to add namespaces into the main SOAP envelope, just into the SOAP Header node.

With soap.wsse.security.userpass.pas I added the UsernameToken and Security classes.
I had to tweak the ObjectToSOAP method to get the namespaces the way I wanted them as I could find no other way to do this.

(* --------------------------------------------------------------------------
   soap.wsse.security.userpass

   This unit defines the classes needed to build the WSSE headers when
   just a user name and password are required.

   This unit is needed to get around design flaws (as I see it) in Delphi's
   built in soap handling up to and including Tokyo 10.2.3.
   Delphi doe snot seem to be able to build soap headers dynamicly,
   preferring to use staticly defined classes instead.
   I have not found a way to dynamically start with a Security class,
   add a UsernameToken to it, and a Password to that.

-------------------------------------------------------------------------- *)
unit soap.wsse.security.userpass;

interface

uses Soap.InvokeRegistry, Soap.SOAPHTTPClient, System.Types, Soap.XSBuiltIns,
     xml.XMLIntf, soap.wsse;

const
  IS_OPTN=$0001;
  IS_ATTR=$0010;
  IS_TEXT=$0020;
  IS_REF =$0080;
  IS_QUAL=$0100;

  NS_SECEXT = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd';
  NS_UTILITY = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd';


type

  myUsernameToken=class(TRemotable)
  private
    FUserName: soap.wsse.AttributedString;
    FCreated:  soap.wsse.Created;
    FPassword: soap.wsse.Password;
    FNonce:    soap.wsse.Nonce;
    FId:       soap.wsse.Id;
  public
    destructor Destroy; override;
    function   ObjectToSOAP(RootNode, ParentNode: IXMLNode;
                            const ObjConverter: IObjConverter;
                            const NodeName, NodeNamespace, ChildNamespace: InvString;
                            ObjConvOpts: TObjectConvertOptions;
                            out RefID: InvString): IXMLNode; override;
    property Id:Id Index (IS_ATTR or IS_QUAL) read FId write FId;
  published
    property Username: soap.wsse.AttributedString read FUsername write FUsername;
    property Password: soap.wsse.Password read FPassword write FPassword;
    property Nonce: soap.wsse.Nonce read FNonce write FNonce;
    property Created: soap.wsse.Created index (IS_REF) read FCreated write FCreated;
  end;


  Security=class(TSOAPHeader)
  private
    FTimestamp:     soap.wsse.Timestamp;
    FUserNameToken: myUserNameToken;
  public
    constructor Create(const Username, password: string); overload;
    destructor Destroy; override;
    function   ObjectToSOAP(RootNode, ParentNode: IXMLNode;
                            const ObjConverter: IObjConverter;
                            const NodeName, NodeNamespace, ChildNamespace:
                            InvString; ObjConvOpts: TObjectConvertOptions;
                            out RefID: InvString): IXMLNode; override;
  published
    property Timestamp: soap.wsse.TimeStamp index (IS_REF) read FTimestamp write FTimestamp;
    property UsernameToken: myUsernameToken index (IS_REF) read FUserNameToken write FUserNameToken;
  end;


implementation

uses SysUtils;

{ UsernameToken }

destructor myUsernameToken.Destroy;
begin
  FreeAndNil(FCreated);
  FreeAndNil(FPassword);
  FreeAndNil(FNonce);
  inherited Destroy;
end;

function myUsernameToken.ObjectToSOAP(RootNode, ParentNode: IXMLNode;
  const ObjConverter: IObjConverter; const NodeName, NodeNamespace,
  ChildNamespace: InvString; ObjConvOpts: TObjectConvertOptions;
  out RefID: InvString): IXMLNode;
begin
  Result := inherited;
  if (Result <> nil) and (Length(FId) > 0) then
  begin
    Result.DeclareNamespace('wsu', NS_UTILITY);
    Result.SetAttributeNS('Id', NS_UTILITY, FId);
  end;
end;

{ Security }

constructor Security.Create(const Username, password: string);
begin
  inherited Create;
  UsernameToken := myUsernameToken.Create;
  UsernameToken.Username := soap.wsse.AttributedString.Create;
  UsernameToken.Username.Text := Username;
  UsernameToken.Password := soap.wsse.Password.Create;
  UsernameToken.Password.Type_ := 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0#PasswordText';
  UsernameToken.Password.Text := password;
end;

destructor Security.Destroy;
begin
  FreeAndNIL(FTimestamp);
  FreeAndNIL(FUserNameToken);
  inherited Destroy;
end;

function Security.ObjectToSOAP(RootNode, ParentNode: IXMLNode;
  const ObjConverter: IObjConverter; const NodeName, NodeNamespace,
  ChildNamespace: InvString; ObjConvOpts: TObjectConvertOptions;
  out RefID: InvString): IXMLNode;
begin
  ParentNode.DeclareNamespace('wsse', NS_SECEXT);
  ParentNode.DeclareNamespace('wsu', NS_UTILITY);

  Result := inherited;
  if (Result <> nil) then
  begin
    //Result.DeclareNamespace('wsse', NS_SECEXT);
    //Result.DeclareNamespace('wsu', NS_UTILITY);
  end;
end;

initialization
  RemClassRegistry.RegisterXSClass(Security, NS_SECEXT, 'Security');
  RemClassRegistry.RegisterXSClass(myUsernameToken, NS_SECEXT, 'UsernameToken');

end.

You can download these files here.

In the Security.ObjectToSOAP method, the RootNode and ParentNode are both pointing to the SOAP-Header.
I used the ParentNode to declare the “wsse” and “wsu” namespaces.

Originally the UsernameToken.Username was defined as a string but his resulted in an unqualified entry in the SOAP request (i.e. ““) which caused issues with the server I was trying to connect to.
So I changed it to AttributedString which then kept the namespace qualifier.

All this began when I started working on the Omnitracs Interface.

Bookmark the permalink.

Comments are closed.