Showing posts with label SSO. Show all posts
Showing posts with label SSO. Show all posts

Wednesday, February 7, 2024

Cross-domain front channel federated Single Log Out

The SSO (Single Sign On) can be implemented in various ways, people use WS-Federation, SAML2 or OpenIdConnect, all these protocols are great and work like a charm.
What's troublesome is the SLO (Single Log Out). There are two possible approaches:
  • back-channel logout consists in server-server communication. The identity provider calls the service provider directly, server-to-server, without any user browser support (thus the name "back-channel") and says "hey, it's me, the identity provider. The session 10b7e82f-4a1b-489f-9848-0d8babcd737f should be terminated." The service provider marks the session as terminated then. While this looks great, there's an implementation and performance cost - the service provider must be implemented in a specific way - the very each request from any user must be checked against the active sessions repository, just because the session can be terminated by a server-server call from the identity provider.
  • front-channel logout consists in server-browser-server communication. The usual way of implementing this is by using nested iframes returned from the identity provider. This is where the problem is.
The problem of nested iframes is a new, escalating issue. The more restrictions are added to web browsers because of the more strict security policy, the more problems there are with nested iframes. I've blogged about an issue with FireFox preventing cookies with SameSite=none from being sent in cross-domain iframes. Who knows if other browsers will adopt this seemingly incorrect policy just because someone decides safety is more important than conforming to the spec.
Anyway, because of how some web browsers start to act when cross-domain iframes are involved, marking your cookies with SameSite=none is no longer a solution. Instead, we've concluded that the idea of iframes has to be replaced by something else that would still allow us to use the front-channel logout but would not suffer from browsers quirks.
The idea is as follows. The identity provider maintains a per-user-session list of service providers that were authenticated in current session (it maintains such lists anyway, it's not anything new). The new element here is that the list allows the identity provider to distinguish between an old-service-provider and a new-service-provider. Old service providers will be handled the old way, with nested iframes. New service providers will be handled by redirecting to them and expecting they redirect back.
Let's first discuss how the identity provider is able to distinguish service providers. There are two possible approaches:
  • hard approach - the identity provider can rely on a static whitelist configuration where each possible identity provider is configured and an extra bit of information is available in the configuration.
  • soft approach - the identity provider can still have a whitelist but the information about the service provider type is not a part of the configuration. Instead, whenever a service provider tries to signin, the signin protocol contains an extra bit of information that tells the service provider what type of service provider is currently processed. For us, this extra information in the signin was an extra key in the wctx parameter of the WS-Federation. The wctx allows the service provider to pass an extra info to the identity provider, of the form [key=value]. It's commonly used to pass something like rm=1 (remember me=1) or ru=.... (return url=....). We just add yet another extra key here, nslo=1 (new single log out=1). In case of third-party identity providers, this extra key in wctx is just ignored. In case of a first-party, our own identity provider, we use this extra info from the wctx to mark service providers as old/new at the identity provider side.
And now, comes the important part, the actual log out implemented at service providers and identity providers.
In case of a service provider:
  • signin - a regular wsignin1.0 together with extra wctx=nslo%3D1
  • sign out - a regular wsignout1.0. In case of wsignoutcleanup1.0, the service provider is obliged to return back (302) to the identity provider (with wsignout1.0)
In case of an identity provider:
  • signin - pay attention to wctx and mark service providers as old/new (as explained above)
  • sign out
service_provider = list_of_service_providers_of_new_type_from_session.FirstOrDefault();
if ( service_provider != null ) {
    remove_from_session( service_provider );
    redirect_to_service_provider_with_wsignoutcleanup1.0( service_provider )
} else if ( identity_provider_has_its_own_top_level_identity_provider ) {
    create_page_with_iframes_to_old_type_service_providers();
    redirect_to_top_level_identity_provider_with_wsignout1.0( top_level_identity_provider )
} else {
    // there's no top-level identity provider above the current identity provider
    create_page_with_iframes_to_old_type_service_providers();
    close_session_and_return_information_page()
}
Q&A
  • Why it works? It works because there are no more nested iframes, instead the identity provider redirects to service providers and they redirect back. A 302 redirect always carries cookies.
  • Why the distinction between old/new service providers? It's because the log out is implemented as a redirect to the identity provider with wsignoutcleanup. Old-type service providers usually handle this by just terminating the local session and returning an empty response. Since now log out is a redirect from the identity provider to the service provider, the identity provider has to be sure the service provider will redirect back
  • Is it backwards compatible with existing service providers / identity providers? It is. An old-type identity provider (that just returns iframes) will ignore the extra signin info provided in wctx. The cross-domain sign out would probably still fail but you don't loose anything (it fails anyway). An old-type service provider with a new identity provider will not carry the extra wctx info so that it will be handled with an iframe (because the identity provider handles both type of service providers)

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.