Monday, September 17, 2018

Parsing SAML 1.1 (WS-Federation) tokens without the WSFam module

Ocassionally there's a scenario where a SAML token must be parsed without the WSFederationAuthentication module. Note that when the WSFam can be used, parsing is straightforward.

For us, it was one of our old applications that still can't be upgraded to .NET 4.5, because of reasons ;), and we wanted to drop the old WIF runtime (the one that targets older .NET versions). For someone else it can be another scenario, e.g. you have the SAML token as string and just want the IPrincipal out of the token.

The solution is to think of the token as it was the regular XMLDsig signed XML - the assertion node is signed and the signature's reference points back to it:

<?xml version="1.0"?>
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
  <t:Lifetime>
    <wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-09-18</wsu:Created>
    <wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-09-18</wsu:Expires>
  </t:Lifetime>
  <wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
    <wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
      <wsa:Address>https://foo.bar/</wsa:Address>
    </wsa:EndpointReference>
  </wsp:AppliesTo>
  <t:RequestedSecurityToken>
    <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" MajorVersion="1" MinorVersion="1" AssertionID="_assertionID" 
        Issuer="http://issuer" IssueInstant="2018-09-18">
      <saml:AttributeStatement>
        <saml:Subject>
          <saml:SubjectConfirmation>
            <saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
          </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Attribute AttributeName="windowsaccountname" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
          <saml:AttributeValue>username</saml:AttributeValue>
        </saml:Attribute>
      </saml:AttributeStatement>
      <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
          <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
          <ds:Reference URI="#_assertionID">
            <ds:Transforms>
              <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
            <ds:DigestValue>digest</ds:DigestValue>
          </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>signature</ds:SignatureValue>
        <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
          <X509Data>
            <X509Certificate>certificate</X509Certificate>
          </X509Data>
        </KeyInfo>
      </ds:Signature>
    </saml:Assertion>
  </t:RequestedSecurityToken>
  <t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
</t:RequestSecurityTokenResponse>
What you should do is to
  1. validate the signature
  2. accept or reject the signature's certificate
  3. parse the token to retrieve claims required to create the IPrincipal
The code is rather simple, what's interesting however is that the SignedXml class has to be inherited to have the signature validator that follows the AssertionID attribute (the default convention is that the signed node's id attribute is called just ID and the default validator just won't find the node that has the id attribute called differently):
    public class SamlSignedXml : SignedXml
    {
        public SamlSignedXml(XmlElement e) : base(e) { }

        public override XmlElement GetIdElement(XmlDocument document, string idValue)
        {
            XmlNamespaceManager mgr = new XmlNamespaceManager(document.NameTable);
            mgr.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
            mgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
            mgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");

            XmlElement assertionNode = 
                   (XmlElement)document.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/"+
                                                         "trust:RequestedSecurityToken/saml:Assertion", mgr);

            if (assertionNode.Attributes["AssertionID"] != null &&
                string.Equals(assertionNode.Attributes["AssertionID"].Value, idValue, StringComparison.InvariantCultureIgnoreCase)
                )
                return assertionNode;

            return null;
        }
    }
Note that the XPath assumes the token has the RequestSecurityTokenResponseCollection in the root, make sure your tokens follow this convention (in case of a single token, the collection node can be missing and the token's root could be just RequestSecurityTokenResponse, update the code accordingly).

The validation code is then

// token is the string representation of the SAML1 token
// expectedCertThumb is the expected certificate's thumbprint
protected bool ValidateToken( string token, string expectedCertThumb, out string userName )
{
 userName = string.Empty;

 if (string.IsNullOrEmpty(token)) return false;

 var xd = new XmlDocument();
 xd.PreserveWhitespace = true;
 xd.LoadXml(token);

 XmlNamespaceManager mgr = new XmlNamespaceManager(xd.NameTable);
 mgr.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
 mgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
 mgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");

 // assertion
 XmlElement assertionNode = (XmlElement)xd.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:RequestedSecurityToken/saml:Assertion", mgr);

 // signature
 XmlElement signatureNode = (XmlElement)xd.GetElementsByTagName("Signature")[0];

 var signedXml = new SamlSignedXml( assertionNode );
 signedXml.LoadXml(signatureNode);

 X509Certificate2 certificate = null;
 foreach (KeyInfoClause clause in signedXml.KeyInfo)
 {
  if (clause is KeyInfoX509Data)
  {
   if (((KeyInfoX509Data)clause).Certificates.Count > 0)
   {
    certificate =
    (X509Certificate2)((KeyInfoX509Data)clause).Certificates[0];
   }
  }
 }

 // cert node missing
 if (certificate == null) return false;

 // check the signature and return the result.
 var signatureValidationResult = signedXml.CheckSignature(certificate, true);

 if (signatureValidationResult == false) return false;

 // validate cert thumb
 if ( !string.IsNullOrEmpty( expectedCertThumb ) )
 {
  if ( !string.Equals( expectedCertThumb, certificate.Thumbprint ) )
   return false;
 }

 // retrieve username

 // expires = 
 var expNode = xd.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:Lifetime/wsu:Expires", mgr );

 DateTime expireDate;

 if (!DateTime.TryParse(expNode.InnerText, out expireDate)) return false; // wrong date

 if (DateTime.UtcNow > expireDate) return false; // token too old

 // claims
 var claimNodes =                 
   xd.SelectNodes("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:RequestedSecurityToken/"+
                  "saml:Assertion/saml:AttributeStatement/saml:Attribute", mgr );
 foreach ( XmlNode claimNode in claimNodes )
 {
  if ( claimNode.Attributes["AttributeName"] != null && 
              claimNode.Attributes["AttributeNamespace"] != null &&
       string.Equals( claimNode.Attributes["AttributeName"].Value, "name", StringComparison.InvariantCultureIgnoreCase ) &&   
                     string.Equals( claimNode.Attributes["AttributeNamespace"].Value, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims", StringComparison.InvariantCultureIgnoreCase ) &&
         claimNode.ChildNodes.Count == 1 
      )
  {
   userName = claimNode.ChildNodes[0].InnerText;
   return true;
  }
 }

 return false;
}
A couple of comments here.

First, the XPath could possibly be shortened to reflect the possibility of a missing collection node.

Then, the code assumes there's the name claim that contains the username but it could be the windowsaccountname or maybe yet another claim type.

Friday, September 14, 2018

WCF and default serialization of requests and responses

A short story of something new we've learned about how exactly WCF serializes the data that is sent over the wire.

Introduction

Before WCF, the default way to serialize objects to XML was to use the XmlSerializer. It works and of course has its shortcomings when it comes to serialization of complex types and collections.

When WCF was introduced, a couple of new serializers were brought into the Base Class Library, including the DataContractSerializer and NetDataContractSerializer. New serializers mean new features, comparision charts are available (e.g. this one by Sebasian Krysmanski).

If you, like we did, live in a simple world where WCF just uses the new set of serializers, read on.

Usually, where both the service and the client are .NET apps, web services can be designed by writing down C# interfaces and data models first. I'd call this common approach the code first approach - you share a code between the service and the client:

// common, shared between the service and the client
[DataContract]
public class DataModel 
{
   [DataMember]
   public string Whatever { get; set; }
}

public interface IServiceContract
{
    void DoWork( DataModel model );
}
Then, the server just implements the interface and exposes the service using a service host (IIS/self-host):
[ServiceBehavior(...)]
public class ServiceImpl : IServiceContract
{
   ...
}
and the client uses the ChannelFactory or the ClientBase to easily have the proxy based on the same interface.

A case of a unit test

Working on a complex integration project involving interoperable calls between a .NET client and a Java WebService, we were faced with an approach we haven't followed often before. Instead of the usual code first approach, we were given a couple of *.WSDL/*.XSD files, which makes a valid model first approach. Given these, you use an automated tool like the xsd.exe or the newer svcutil.exe to automatically create code from models:

svcutil.exe /syncOnly /n:*,Test *.wsdl *.xsd

This approach was used, the code has been generated and someone tried to write a unit test to make sure the request body is correctly serialized so that it meets the XML structure expected at the Java's side. The unit test code first used the DataContractSerializer as we believed this is what WCF uses under the hood. The test code was basically something like:

DoWorkRequest request = new DoWorkRequest();
request.model = 

var serializer = new DataContractSerializer();
var ms = new MemoryStream();

serializer.WriteObject( ms, request );

var requestXML = Encoding.UTF8.GetString( ms.ToArray() );

Assert.....

As it turned out, the serializer's output was something like

<DoWorkRequest ....
while the server's expectation was
<DoWork ....
(note the Request suffix missing from the root's name)

The test was obviously failing. We started an investigation.

What is really going on under the hood

After a couple of different trials and errors involving other serializers and their settings, we've found something that we never manually put into the code in the code-first approach. It was the MessageContractAttribute put over the request class by the generator:

[MessageContractAttribute(WrapperName="DoWork"....]
public class DoWorkRequest {
Things started to get interesting, it looks like there's yet another serializer, not mentioned that much, that obviously respects this attribute. Googling around reveals that there is indeed yet another layer used by WCF on top of different serializers to have even more control on how your data is serialized when a web service is called. This directly leads to the TypedMessageConverter class and code snippets people already posted (e.g this one by Stanislav Dvoychenko).

A solution, finally

The solution was to rewrite the unit test to actually use the TypedMessageConverter:

var request                       = new DoWorkRequest(...);

var converter                     = TypedMessageConverter.Create( request.GetType(), "*", string.Empty, new XmlSerializerFormatAttribute());
var message                       = converter.ToMessage(request, MessageVersion.Soap11WSAddressing10);            

var writerSettings                = new XmlWriterSettings();
writerSettings.OmitXmlDeclaration = true;

var stream                        = new MemoryStream();
var writer                        = XmlWriter.Create(stream, writerSettings);

message.WriteMessage(writer);
writer.Flush();

var requsetXML = Encoding.UTF8.GetString(stream.ToArray());
which gives the exact SOAP message that can be peeked using an HTTP debugger (you can possibly unpack the soap envelope it's wrapped into in your unit test code).

Monday, July 30, 2018

A Fairy Tale of an Old Music Box

One of things I really like doing in my spare time is playing the piano we bought recently. I also decided to check whether there is some decent score writing software out there and I was really surprised to find that the software not only gets much, much better over years but also that there are even some free yet advanced apps like the MuseScore.

Anyway, this looks like my chance to write down few ideas I had on my mind for all these years since I finished my music education. And since sharing is one of nice features of the software, please enjoy one of my compositions, the first one I wrote down with MuseScore, A Fairy Tale of an Old Music Box.

A Fairy Tale of an Old Music Box

Friday, July 13, 2018

.NET 4.7.1 (and higher) no longer supports SHA1 in SignedXml

There are plenty of subtle changes between .NET 4.7.0 (and lower) and .NET 4.7.1, however one of the changes hurt us badly. It looks like the SignedXml no longer supports SHA1 as the hashing method.

What it causes is the

System.Security.Cryptography.CryptographicException : Invalid algorithm specified
...
     at System.Security.Cryptography.Utils.SignValue(SafeKeyHandle hKey, Int32 keyNumber, Int32 calgKey, Int32 calgHash, Byte[] hash, Int32 cbHash, ObjectHandleOnStack retSignature)
     at System.Security.Cryptography.Utils.SignValue(SafeKeyHandle hKey, Int32 keyNumber, Int32 calgKey, Int32 calgHash, Byte[] hash)
     at System.Security.Cryptography.RSACryptoServiceProvider.SignHash(Byte[] rgbHash, Int32 calgHash)
     at System.Security.Cryptography.Xml.SignedXml.ComputeSignature()

The resolution is to put an additional section in the app's config file that switches the use of insecure hashes on:

  <runtime>
    <AppContextSwitchOverrides value="Switch.System.Security.Cryptography.Xml.UseInsecureHashAlgorithms=true;
                                         Switch.System.Security.Cryptography.Pkcs.UseInsecureHashAlgorithms=true" />
  </runtime>

If these switches seem to be ignored (we observed this in web apps where this was put in the web.config rather than an app.config, simply replace it with the code that you put in the global app class in the Application_Start:

protected void Application_Start(object sender, EventArgs e)
{
   ...
   AppContext.SetSwitch("Switch.System.Security.Cryptography.Xml.UseInsecureHashAlgorithms", true);
   AppContext.SetSwitch("Switch.System.Security.Cryptography.Pkcs.UseInsecureHashAlgorithms", true);
}

Monday, July 2, 2018

Integracja z ePUAP - dzień bez zmiany dniem straconym

Przed weekendem 30-06/01-07 blogowałem o zmianie na środowisku testowym ePUAP, przywracającym SHA1 w trybie wymuszenia (SHA256 przestało być obsługiwane) w konstruowaniu sygnatur WS-* w żądaniach do usług integracyjnych. Napisałem też że mam nadzieję na uporządkowanie sytuacji.

Cóż, sytuacja uporządkowała się o tyle że dziś (02-07) od południa na środowisku testowym ePUAP (int.pz.gov.pl) stoi kolejna wersja, która znów wymusza SHA256 i nie obsługuje SHA1.

Pytanie retoryczne: jak długo jeszcze może trwać ten kołowrotek i jak długo poczekamy na wersję która obsłuży oba rodzaje funkcji skrótu?

Friday, June 29, 2018

Integracja z ePUAP - zamiana powrotna SHA256 na SHA1

Dziś czyli w piątek, 29 czerwca 2018, około godziny 14-tej, środowisko testowe ePUAP zostało ponownie zaktualizowane. Tym razem do wersji która nie obsługuje całkowicie algorytmu SHA256, zamiast tego wraca do sytuacji w której w komunikacji wymagane jest SHA1.

Ta zmiana ociera się o skandal i sprawia wrażenie że COI nie panuje nad sytuacją. Byłoby zrozumiałe, gdyby nowa wersja przywracała wsparcie dla SHA1 ale poprawnie obsługiwała oba, SHA1 i SHA256. Natomiast udostępnienie wersji która na działającym kodzie wykorzystującym SHA256 zwraca z serwera wyjątki bezpieczeństwa, na które lekarstwem jest przywrócenie SHA1 po stronie integrowanej aplikacji, bardzo źle świadczy o kulturze wytwarzanego kodu i wsparcia integratorów.

Być może jest to tylko niedopatrzenie i sytuacja wróci do normy (=będą obsługiwane oba rodzaje funkcji skrótu) w niedługim czasie, ale zamieszanie jakie powoduje COI nieprzemyślaną jak widać do końca migracją, powoduje perturbacje po stronie integratorów.

Wednesday, June 13, 2018

Integracja z ePUAP - zmiana SHA-1 na SHA-256

Środowisko testowe ePUAP zostało właśnie dostosowane do zmian wymuszonych Ustawą o usługach zaufania oraz identyfikacji elektronicznej z września 2016, która w art. 137 mówi:
Do dnia 1 lipca 2018 r. do składania zaawansowanych podpisów elektronicznych lub zaawansowanych pieczęci elektronicznych można stosować funkcję skrótu SHA-1, chyba że wymagania techniczne wynikające z aktów wykonawczych wydanych na podstawie rozporządzenia 910/2014 wyłączą możliwość stosowania tej funkcji skrótu.
Istotnie, wdrożona na środowisku testowym zmiana powoduje zwracanie statusu A security error was encountered when verifying the message przy komunikacji z dowolną usługą.
Komunikat błędu nie jest może bardzo przydatny w diagnozie problemu, niemniej warto odnotować, że faktycznie chodzi o konieczność wymiany SHA-1 na SHA-256, co jest szalenie istotne w kontekście generowania podpisów XMLDsig w komunikacji z usługami - przynajmniej w .NET, domyślnie dostawca podpisów obiektu SignedXml wybiera SHA-1, ponieważ tak podpowiada sygnatura certyfikatu. A nadpisanie tej domyślnej konwencji w taki sposób żeby do wyliczenia sygnatury został użyty algorytm SHA-256 wcale nie jest takie oczywiste. A skąd wiadomo że akurat SHA-256? Cóż, na środowisku testowym zaktualizowano również dokumentację dla integratorów i tam w wyciągach z przykładowych żądań i odpowiedzi systemu pojawia się właśnie ten algorytm, wszędzie tam gdzie we wcześniejszych wersjach widniał sha1.
Niewykluczone że z początkiem lipca czeka nas wysyp "awarii" systemów zewnętrznych zintegrowanych z ePUAP, które z różnych powodów nie zaimplementują tej zmiany. W kontekście wczorajszej (2018-06-12) dużej awarii e-usług - być może ta awaria i planowana zmiana mają jakiś związek.

Friday, April 20, 2018

Retaining document relative links when copying from HTML to Word

We have an automated document generation tool that creates a HTML documentation of a given database. The generated HTML structure uses relative links between elements, this is very convenient for users who browse the documentation.
For example, somewhere in the document a database table is documented
<h2>Table <a name="TheTable">TheTable</a></h2>
and somewhere else the table is referenced
<a href="#TheTable>TheTable</a>
Let's create a simplest HTML as an example
<html>
<body>

<div>
This is a reference to <a href="#TheTable">TheTable</a>.
</div>

<div>
This is the definition of <a name="TheTable">TheTable</a>
</div>


</body>
</html>
and copy/paste it to Word
Take a closer look at what happened with the relative link - it has been pasted as a link to the source HTML document! That's kind of a disaster, I definitely don't want my relative links to suddenly become absolute and what's worse - point from Word to an external, source HTML document!
This inconvenience can be fixed manually, I can just right-click at the link, edit its properties and change the link type from existing file or a web page to a bookmark in this document:
however, manually fixing hundreds of links sounds like a daunting task.
Fortunately, this can be fixed automatically, with a local VBA script in the Word document that basically creates another link in the very same range of the document but with empty Address property - by multiple trials and errors I've determined this is the only difference between external and internal, relative links:
Do follow these steps to have your links fixed then:
  1. Copy/paste your HTML into Word
  2. alt+F11 to open VBA editor
  3. Double click ThisDocument to open a code editor for scripts in current document
  4. Paste the script into the editor window
    Sub FixHyperlinks()
    
    Dim h As Hyperlink
    For Each h In ActiveDocument.Hyperlinks
    
        ActiveDocument.Hyperlinks.Add h.Range, "", h.SubAddress
    
    Next h
    End Sub
    
  5. Place cursor somewhere inside the script and hit F5 (or click the green triangle) to run the macro
Relative links now correctly point to elements in the very same document, even exporting to PDF retains the correct behavior.

Monday, April 9, 2018

Extending Generator prototype to get LINQ-like experience in Javascript

Generator functions let us write iterators in a very concise way. Take a simple example:

function* range(n,m) {
    for ( var i=n; i<m; i++ ) {
        yield i;
    }
}
This lets me write
for ( var e of range(0,10) ) {
   console.log( e );
}
to get an iterator that returns numbers from 0 to 9.

An interesting question has been asked at the SO - can such generators be extended with extension methods that would work in a similar way C#'s LINQ works, where one can chain filtering, groupping or sorting operators.

My suggestion there was to extend the Generator prototype, which is technically possible and could possibly be surprising as the Generator literal doesn't resolve at the top-level. A trick here is to first resolve it manually and only then extend the prototype.

var Generator = Object.getPrototypeOf( function*() {});

Generator.prototype.filter = function*( predicate ) {
    for ( var e of this ) {
        if ( predicate(e) )
            yield e;
    }
}
This leads to following convention:
for ( var e of 
    range(0,10)
        .filter( x => x<8 )
        .filter( x => x > 2 ) ) {
    console.log(e);
}
Other operators can be added in a similar way.

Thursday, March 8, 2018

SSO - integracja z ePUAP (Dostawca Tożsamości) w .NET

2020-08: na github znajduje się repozytorium kodu OldMusicBox.ePUAP.Client, które ilustruje opisane niżej perypetie działającym kodem.

Planowanie

Post wyjątkowo w języku polskim, z garścią spostrzeżeń i uwag po udanej implementacji integracji z modułem Dostawcy Tożsamości usługi ePUAP, zrealizowanej w środowisku .NET.

Na początek spostrzeżenie ogólne - całość prac technicznych to łącznie kilkadziesiąt godzin pracy dwóch bardzo doświadczonych developerów, mających w przeszłości wielokrotne i szerokie doświadczenie w temacie SSO i podpisów cyfrowych w różnych standardach. To doświadczenie wielokrotnie procentowało pozwalając szybko weryfikować błędne ścieżki, co przy podsumowaniu ostatecznego bilansu nakładu nastraja nieco pesymistycznie - jeśli tego typu pracę wiele zespołów programistycznych w różnych organizacjach wykonuje niezależnie od siebie, oznacza to że dziesiątki godzin pracy wielu ludzi w pewien sposób marnuje się na coś co można zrealizować zupełnie inaczej.

Sam ePUAP udostępnia bowiem dokumentację dla integratorów, na dzień dzisiejszy znajduje się ona na stronie głównej ePUAP, w zakładce Strefa Urzędnika / POMOC / Dla integratorów / Specyfikacja WSDL - integratora interesują dwa dokumenty

  • Instrukcja dla integratora DT - opis sposobu implementacji protokołu SAML2 przez ePUAP
  • Instrukcja dla integratora PZ - opis usług sieciowych, wywołanie co najmniej jednej z nich, getTpUserInfo, jest niezbędne przy implementacji SSO
Problemem jest tu nie tyle sama dokumentacja, co brak referencyjnych implementacji części klienckiej co przy projekcie o tej skali wydaje się wymaganiem dość oczywistym. Zamiast tego, na stronie ePUAP w zakładce Przykładowe aplikacje na dzień dzisiejszy jest po prostu pusto:

Zamiast tego, chciałoby się w tej zakładce znaleźć materiały ułatwiające implementację integracji. Tu sugestia dla COI. Otóż, mając doświadczenia w dostarczaniu mechanizmów integracyjnych dla zewnętrznych partnerów rozumiem jak trudne może być dostarczenie przykładowych aplikacji. Jeśli zespół implementujący ePUAP jest biegły w Javie, to może stosunkowo łatwo wyprodukować przykładowe kawałki kodu dla Javy. Z kolei przy braku silnych kompetencji technicznych, przygotowanie makiet dla innych technologii będzie trudniejsze. Do tego dochodzi jeszcze ryzyko zarzutu stronniczości - bo które platformy technologiczne wybrać do przygotowania makiet? .NET? A może też PHP? A dlaczego nie node.js czy django? To potencjalna puszka Pandory.

Istnieje jednak alternatywa - przykładowy program wcale nie musi pokazywać referencyjnej implementacji części klienckiej. Zamiast tego może być otwartą implementacją części serwerowej, dostarczoną w dowolnym, wybranym języku programowania (na przykład w Javie), uruchamiającą się na localhost na dowolnym porcie, nawet bez serwera aplikacyjnego, a tylko hostowaną z aplikacji konsolowej. Taka implementacja miałaby punkty końcowe zgodne formalnie z implementacją docelową, ale pozwalałaby na prowadzenie wczesnych testów integracyjnych w całkowicie izolowanych środowiskach deweloperskich, będących w całości pod kontrolą integratorów. Przez formalną zgodność rozumiem tu te same kontrakty, ale już swobodnie potraktowaną implementację - na przykład na pokazującej się stronie logowania można wpisać dowolne dane użytkownika a certyfikat podpisujący może być dowolny. Albo - zarówno listę profili jak i certyfikatów w takiej zastępczej implementacji konfiguruje się w jakimś jawnym XMLowym pliku konfiguracyjnym leżącym obok aplikacji. W każdym przypadku - otwartość implementacji zastępnika części serwerowej pozwala integratorowi implementującemu część kliencką mieć oba systemy pod kontrolą, w szczególności nawet - debugować żądania zarówno od strony aplikacji klienckiej jak i od strony usługi. A po dojściu w pracy z zastępnikiem do momentu poprawnego przepływu, test integracyjny na testowej wersji ePUAP byłby tylko formalnością, zamiast całkowicie zmyślonych kont byłyby faktyczne konta testowe na testowej platformie, ale od strony technicznej cały przebieg protokołu byłby ten sam, już przetestowany z lokalnym zastępnikiem rzeczywistej usługi.

Takie dość oczywiste podejście - zastepnika usługi integracyjnej możliwego do uruchomienia lokalnie - stosujemy z powodzeniem od wielu lat i doświadczenia są tylko pozytywne. Jest to równocześnie znacznie tańsze niż dostarczanie makiet części klienckiej w wielu wybranych technologiach, ciężar implementacji części klienckiej nadal spoczywa na integratorze, ale implementacja taka jest łatwiejsza.

Przygotowując się do implementacji należy więc zaopatrzyć się w w/w dokumentację, warto również mieć pod ręką te nieliczne ślady po doświadczeniach innych integratorów, którzy zdecydowali się pozostawić po sobie jakiś ślad w blogosferze. Na chwilę obecną znane mi są dwie takie wzmianki

  • dość stare teksty (2011?) opisujące integrację z poziomu PHP z poprzednią wersją ePUAP: SSO i usługi sieciowe, sporo drobnych szczegółów niestety jest już nieaktualnych
  • tekst z połowy 2017 roku opublikowany na blogu apilia opisujący integrację z aktualną wersją ePUAP z poziomu Javy

Ten ostatni materiał jest prawdopodobnie bardzo wartościowy i prawdopodobnie znacząco skraca czas niezbędny do wykonania integracji. Jeśli w dodatku opisany materiał jest nadal aktualny (implementacja działa), to mniej więcej taki poziom techniczny materiału mógłby znaleźć się we wzmiankowanej chwilę temu zakładce Przykładowe aplikacje.

Niestety - z punktu widzenia integratora pracującego w .NET materiał ten ma wartość wyłącznie ogólną - m.in. jako ściągawka poprawnych adresów punktów końcowych usług w wersji produkcyjnej i testowej. Niestety bowiem zaproponowane tam biblioteki OPENSAML3 i Apache CXF nie mają swoich bezpośrednich odpowiedników w .NET. Można więc wyłącznie pozazdrościć Koleżeństwu tego że ścieżka integracji dla Javy jest łatwiejsza i przetarta. Prawdopodobnie, gdyby ten materiał był nam znany wcześniej, próbowalibyśmy mimo wszystko najpierw zweryfikować jego poprawność i przygotować jakąś formę adaptera SSO wykorzystującego komponent Javowy i opisaną ścieżkę. Z uwagi bowiem na brak bezpośrednich odpowiedników dla SAML2 i WS-Security po stronie biblioteki standardowej .NET, integracja dla .NET oznacza pracę na dużo niższym poziomie, w szczególności

  • .NET nie ma wsparcia dla protokołu SAML2 w bibliotece standardowej, mimo że ma fantastyczne wsparcie dla SAML1. Istnieją nieliczne implementacje darmowe (np. Sustainsys.SAML2), jednak ich użycie wiąże się z ryzykiem braku wsparcia. Istnieją nieliczne implementacje płatne (np. ComponentSpace). Ostatecznie zdecydowaliśmy o pójściu własną ściezką, czyli własnej implementacji SAML2 w części klienckiej i serwerowej. Ta część prac pożarła lwią część łącznego czasu implementacji, spłaci się jednak w kolejnych tygodniach kiedy, we własnych rozwiązaniach, oprócz usług serwerowych SSO dla SAML1/OAuth2 będziemy również dostarczać w pakiecie część serwerową SAML2
  • .NET teoretycznie dobrze wspiera WS-Security, ponieważ wsparcie zostało całkowicie włączone do WCF. W praktyce, osiągnięcie takiego dokładnie formatu wywołania jakiego oczekuje konkretny serwer (w przypadku ePUAP są to ścisłe wymagania na to jak do wiadomości dołączony jest certyfikat i który węzeł jest podpisany) jest to droga przez mękę, z odkrywaniem metodą prób i błędów tego jak skonfigurować CustomBinding dla wygenerowanej przez automat klasy proxy z WSDL usługi ePUAP. Mimo wielu prób nie udało się nam poprawnie wywołać usługi przez automatycznie generowane WCF proxy, ostatecznie podzbiór WS-Security został zaimplementowany od zera na potrzeby tej integracji
Z perspektywy integratora pracującego w .NET wydaje się więc, że dostarczenie referencyjnych implementacji części klienckiej przez dostawcę usługi byłoby bardzo, bardzo wskazane. Sam system ePUAP w części serwerowej jest zaimplementowany w Javie - co można wnioskować z otrzymywanych w trakcie prób integracji wyjątków zawierających ślady stosów wywołań konkretnych metod po stronie serwera - przez co integracja części klienckiej również w Javie może być, z uwagi na zgodność wykorzystywanych zewnętrznych bibliotek, najłatwiejsza. Integratorzy pracujący w innych technologiach mogą być w trudniejszej sytuacji

Przygotowanie do implementacji

Implementację integracji trzeba podzielić na dwa niezależne obszary
  1. implementacja części klienckiej protokołu SAML2, w wyniku której po stronie aplikacji integrującej się z ePUAP (Service Provider, SP) uzyskuje się identyfikator sesji użytkownika utworzonej po stronie ePUAP
  2. implementacja wywołania usługi sieciowej w standardzie WS-Security, w wyniku której identyfikator sesji zamienia się na informacje o użytkowniku (imię, nazwisko, email i PESEL)
Do obu części niezbędny jest certyfikat którym podpisywane są żądania - certyfikat dla środowiska testowego można pozyskać otwierając zgłoszenie mailowe w COI (pz-pomoc@coi.gov.pl)
  1. na testowej instancji witryny Profilu Zaufanego należy utworzyć konto, dla tego konta wyklikać wniosek o Profil Zaufany
  2. w zgłoszeniu mailowym do COI należy poprosić o zatwierdzenie wniosku o profil zaufany (w przeciwnym razie logowanie SAML2 do aplikacji nie uda się, konta ePUAP niepotwierdzone, czyli nie posiadające Profilu Zaufanego, w trakcie logowania zgłaszają komunikat:
  3. W zgłoszeniu mailowym należy również poprosić o wygenerowanie certyfikatu dla testowej integracji oraz dołączyć
    1. identyfikator testowej aplikacji, w języku SAML2 to Issuer - może to być adres uri, np. https://saml2test.mycompany.org
    2. adres punktu końcowego na który usługa DT przekieruje przeglądarkę po poprawnym zalogowaniu, w języku SAML2 to Consumer SSO powinien to być adres który jest lokalnie w środowisku developerskim adresowalny, ale nie musi być adresem rozwiązywanym przez publiczne DNSy, może to być więc np. https://saml2test.mycompany.org/logon pod warunkiem że jakikolwiek lokalny DNS (w tym ten w /etc/hosts) rozwiąże taką domenę na jakiś lokalny serwer
    3. adres punktu końcowego wylogowania, w języku SAML2 to SLO, Single Logout Service - podobnie jak wyżej, w przypadku braku implementacji wylogowywania punkt końcowy może fizycznie w ogóle nie być zaimplementowany

Wysłanie poprawnego zgłoszenia bardzo przyspieszy pracę, doświadczenie tu jest takie że początkowo kontakt z COI to oczekiwanie na odpowiedzi rzędu 10 dni roboczych (czyli 2 tygodni), przy nieprecyzyjnym opisaniu sprawy i konieczności doprecyzowania szczegółów czas kontaktu wydłuża się przez oczekiwanie na kolejne odpowiedzi. Dobra, sprawna komunikacja uruchomiła się dopiero po przejściu dialogu na poziom bardzo techniczny (czyli przy próbie rozwiązania problemu z konkretnymi błędnymi żądaniami).

Ten etap przygotowań można zakończyć w momencie uzyskania z COI testowego certyfikatu, zarejestrowaniu punktów końcowych aplikacji po stronie ePUAP oraz po potwierdzeniu PZ dla testowego konta.

Implementacja

Samodzielna implementacja zarówno SAML2 i jak i WS-Security nie jest aż tak skomplikowana, ponieważ oba standardy de facto bazują na podpisach XMLDsig, które są interoperacyjne między platformami. W szczególności zarówno do podpisywania jak i walidacji można użyć standardowego obiektu SignedXml z biblioteki standardowej oraz wielokrotnie dokumentowanego (m.in. przez mnie lata temu) idiomatycznego kodu podpisującego i weryfikującego podpis. Usługi ePUAP nie są tu kapryśne, standardowy podpis wygenerowany z poziomu .NET (wystarczą poprawnie wskazane referencje z węzła podpisu na podpisywany węzeł) jest akceptowany mimo tego że lista transformat (m.in. normalizacja) różniła się w żądaniach wysyłanych od nas do ePUAP od listy widocznej w przykładowych żądaniach w dokumentacji. Oznacza to że komponenty ePUAP po stronie serwera wykorzystują jakąś generyczną formę kodu walidującego podpisy, akceptującą w rzeczywistości więcej formatów podpisów zgodnych z XMLDsig niż tylko te z przykładów z dokumentacji. To istotne ułatwienie. Pewien problem pojawia się w drugą stronę - z walidacją z poziomu .NET podpisu odpowiedzi, wygenerowanej przez kod Javowy po stronie ePUAP. O tym za chwilę.

Cały flow procesu logowania wygląda z punktu widzenia aplikacji integrującej się z PZ następująco
  1. przygotowanie i podpisane AuthnRequest, przekierowanie na punkt końcowy PZ w celu autentykacji użytkownika
  2. odebranie artefaktu SAMLArt dla wiązania Artifact binding protokołu SAML
  3. zbudowanie i podpisanie żądania ArtifactResolve i wysłanie go na punkt końcowy usługi ePUAP
  4. odebranie i zweryfikowanie ArtifactResponse, wyciagnięcie identyfikatora sesji
  5. zbudowanie i podpisanie żądania WS-Security do usługi getTpUserInfo z identyfikatorem sesji jako argumentem
  6. odebranie i zweryfikowanie odpowiedzi, wydobycie z niej adresu email, imienia, nazwiska i PESELu logującego się użytkownika

Poniżej garść wskazówek, które mogą pozwolić przeskoczyć niespodziewane trudności

  1. przy podpisywaniu należy bezwzględnie używać transformaty SignedXml.XmlDsigExcC14NTransformUrl (czyli http://www.w3.org/2001/10/xml-exc-c14n#), domyślna dla SignedXml transformata http://www.w3.org/TR/2001/REC-xml-c14n-20010315 nie będzie poprawnie obsługiwana
  2. przy podpisywaniu żądań SAML2 (AuthnRequest, ArtifactResolve) należy bezwzględnie zwrócić uwagę na kolejność węzłów w dokumencie oraz na to w które miejsce trafia węzęł z podpisem - blogowałem o tym przy okazji integracji SAML2 z ADFS (jest to p.9 na liście z tamtego posta)
  3. w sygnaturach SAML2 ePUAP wymaga bezwzględnie dołączenia całego certyfikatu (X509Data/X509Certificate) do węzła KeyInfo, jest to pewna nadmiarowość ponieważ oba żądania zawierają jawny atrybut Issuer który musi być zgodny z identyfikatorem zarejestrowanej aplikacji - co oznacza że technicznie serwer ePUAP nie musiałby oczekiwać powtórzenia zawartości certyfikatu w żądaniach ponieważ mógłby sobie zawartość ceryfikatu wyciągnąć z lokalnej bazy danych na podstawie klucza - identyfikatora aplikacji (Issuer). Tak robi np. ADFS2 przy podpisanych żądaniach SAML2 - nie wymaga powtórzenia certyfikatu, natomiast serwer ePUAP zwraca wyjątek
  4. żądanie ArtifactResolve nie może zawierać żadnej wartości w atrybucie Destination - to dość nieoczekiwane, jednak jeśli w Destination poda się, zgodnie ze specyfikacją SAML2, adres usługi samego PZ (któregokolwiek z punktów końcowych), otrzymany token (ArtifactResponse) jest oznakowany jako Success:OK jednak nie zawiera w ogóle węzła Response (który dopiero zawiera identyfikator sesji):
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
        <SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"/>
        <soap:Body>
            <saml2p:ArtifactResponse xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:eidas="http://eidas.europa.eu/saml-extensions" xmlns:naturalperson="http://eidas.europa.eu/attributes/naturalperson" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" ID="ID-d6225f3e-cd17-4d84-bbf0-9baf271e2600" InResponseTo="_g5a74742e-86ac-4eb1-8729-c8b5f810554e" IssueInstant="2018-02-28T13:23:47.605Z" Version="2.0">
                <saml2:Issuer>pz.gov.pl</saml2:Issuer>
                <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                    <ds:SignedInfo>
                        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                        <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                        <ds:Reference URI="#ID-d6225f3e-cd17-4d84-bbf0-9baf271e2600">
                            <ds:Transforms>
                                <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xpath-19991116">
                                    <ds:XPath xmlns:ds="http://www.w3.org/2000/09/xmldsig#">not(ancestor-or-self::ds:Signature)</ds:XPath>
                                </ds:Transform>
                                <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                                    <InclusiveNamespaces xmlns="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="ds saml2 saml2p xenc"/>
                                </ds:Transform>
                            </ds:Transforms>
                            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                            <ds:DigestValue>gSWXETl+giM/Qcdq65Fe9pB86Cg=</ds:DigestValue>
                        </ds:Reference>
                    </ds:SignedInfo>
                    <ds:SignatureValue>Qq9StzWIqup...</ds:SignatureValue>
                    <ds:KeyInfo>
                        <ds:X509Data>
                            <ds:X509Certificate>MIIE3...</ds:X509Certificate>
                        </ds:X509Data>
                    </ds:KeyInfo>
                </ds:Signature>
                <saml2p:Status>
                    <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
                </saml2p:Status>
    
                <!-- a gdzie saml2p:Response?? -->
    
            </saml2p:ArtifactResponse>
        </soap:Body>
    </soap:Envelope>
    
    Poprawny ArtifactResponse wygląda tak
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
     <SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"/>
     <soap:Body>
      <saml2p:ArtifactResponse xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:eidas="http://eidas.europa.eu/saml-extensions" xmlns:naturalperson="http://eidas.europa.eu/attributes/naturalperson" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" ID="ID-dd7b4bf7-990a-4175-91ab-68f03fe75d8a" InResponseTo="_g93d8952d-a3f8-4d0f-9c4f-da316282fcdf" IssueInstant="2018-03-07T11:56:03.916Z" Version="2.0">
       <saml2:Issuer>pz.gov.pl</saml2:Issuer>
       <ds:Signature>...</ds:Signature>
       <saml2p:Status>
        <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
       </saml2p:Status>
       <saml2p:Response ID="ID-983a786f-aedf-4370-b27c-aedb85608c1c" InResponseTo="guid_4933262e-f10b-4859-8dc1-cb5d837ecf11" IssueInstant="2018-03-07T11:56:03.915Z" Version="2.0">
        <saml2:Issuer>pz.gov.pl</saml2:Issuer>
        <ds:Signature>...</ds:Signature>
        <saml2p:Status>
         <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
        </saml2p:Status>
        <saml2:Assertion ID="..." IssueInstant="2018-03-07T11:56:03.915Z" Version="2.0">
         <saml2:Issuer>pz.gov.pl</saml2:Issuer>
         <saml2:Subject>
          <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">...</saml2:NameID>
          <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
           <saml2:SubjectConfirmationData InResponseTo="guid_4933262e-f10b-4859-8dc1-cb5d837ecf11" NotOnOrAfter="2018-03-07T12:46:03.596Z" Recipient="https://.......edu.pl/Account/Logon"/>
          </saml2:SubjectConfirmation>
         </saml2:Subject>
         <saml2:Conditions NotBefore="2018-03-07T11:56:03.596Z" NotOnOrAfter="2018-03-07T12:46:03.596Z">
          <saml2:AudienceRestriction>
           <saml2:Audience>https://....edu.pl</saml2:Audience>
          </saml2:AudienceRestriction>
         </saml2:Conditions>
         <saml2:AuthnStatement AuthnInstant="2018-03-07T11:56:03.915Z" SessionIndex="_ID-...-344b-4087-97ba-...">
          <saml2:AuthnContext>
           <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
          </saml2:AuthnContext>
         </saml2:AuthnStatement>
        </saml2:Assertion>
       </saml2p:Response>
      </saml2p:ArtifactResponse>
     </soap:Body>
    </soap:Envelope>
    
    i jak widać zawiera aż dwa podpisy - jest to zgodne ze specyfikacją SAML2 - podpisane są węzły ArtifactResponse oraz, niezależnie, Response (są to więc aż dwa podpisy do walidacji) (jako ciekawostka: ADFS2 podpisuje wyłącznie węzeł Assertion
  5. podpis wystawionego dokumentu zawiera niedozwoloną przez Microsoft transformatę REC-xpath-19991116, ta transformata została domyślnie zablokowana przez poprawkę bezpieczeństwa z maja 2017. Efekt jest taki że dokumenty XML podpisane przez ePUAP domyślnie nie walidują się w .NET. Zaproponowane rozwiązanie w postaci wymuszenia klucza w rejestrze nie działa (z nieznanego powodu), działa natomiast wskazanie takiej dozwolonej transformaty wprost w kodzie walidującym
    var signedXml = new SignedXml(doc);
    signedXml.LoadXml(signature);
    // jawne dopuszczenie niedozwolonej transformaty
    signedXml.SafeCanonicalizationMethods.Add("http://www.w3.org/TR/1999/REC-xpath-19991116");
    
    result = signedXml.CheckSignature(SignatureCertificate, true);
    
    Bardzo pomaga tu szczegółowy ślad niepoprawnej walidacji który można opcjonalnie włączyć dla walidatora
  6. wbrew informacjom ze str. 7-8 dokumentu dla integratora PZ
    [...] usługi sieciowe systemu Profil Zaufany [...] posiadają standardowe atrybuty dodawane do żądania i odpowiedzi [...]
    żądanie do usługi getTpUserInfo nie może zawierać atrybutów callId ani requestTimestamp
  7. wbrew dokumentacji usługi getTpuserInfo, atrybut systemOrganisationId nie jest nieobowiązkowy, przeciwnie, jest wymagany przez walidator po stronie serwera, natomiast wartość "0" jest akceptowana

Poprawnie wygenerowana przez usługę sieciową odpowiedź, zawierająca dane o użytkowniku, wygląda tak:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
 <SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <wsse:Security 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" soap:mustUnderstand="1">
   <wsu:Timestamp wsu:Id="TS-a5ad6774-0bd6-4ea6-ac40-3a323990db8f">
    <wsu:Created>2018-03-07T13:01:39.231Z</wsu:Created>
    <wsu:Expires>2018-03-07T13:06:39.231Z</wsu:Expires>
   </wsu:Timestamp>
   <wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" wsu:Id="X509-4cd29820-fa78-4eee-b141-d32e5c7f773f">MII...</wsse:BinarySecurityToken>
   <ds:Signature>...</ds:Signature>
  </wsse:Security>
 </SOAP-ENV:Header>
 <soap:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="_a9238b48-08af-4340-9cbe-d7b7bb71bbe5">
  <ns2:getTpUserInfoResponse xmlns:ns2="http://userinfo.zp.epuap.gov.pl" xmlns:ns3="http://exception.userinfo.zp.epuap.gov.pl">
   <getTpUserInfoReturn>
    <accountEmailAddress>....@.....edu.pl</accountEmailAddress>
    <claimedRole>&lt;ppZP:PodpisZP xmlns:ppZP="http://crd.gov.pl/xml/schematy/ppzp/"
 xmlns:os="http://crd.gov.pl/xml/schematy/osoba/2009/03/06/">&lt;ppZP:DaneZP>&lt;ppZP:DaneZPOsobyFizycznej>&lt;os:Nazwisko rodzajCzlonu="pierwszy"
>TestNazwisko&lt;/os:Nazwisko>&lt;os:Imie>TestImie&lt;/os:Imie>&lt;os:PESEL>...</claimedRole>
   </getTpUserInfoReturn>
  </ns2:getTpUserInfoResponse>
 </soap:Body>
</soap:Envelope>
i to jest już ostatni, drobny szczegół - rzeczywiste dane o użytkowniku mają postać XML zanurzonego w XML jako CDATA, wydobycie danych wymaga więc podwójnego parsowania.