W trakcie mojej pracy zawodowej napotkałem na konieczność zintegrowania mojego systemu z systemem EPUAP (flagowe Polskie rozwiązanie dla eurzędu). Tak na prawdę to nie potrzebowaliśmy całej funkcjonalności EPUAPu ale na początek tylko obsługi użytkowników tzw. SSO (Single sign-on). Idea jest piękna jeden login jedno hasło we wszystkich systemach. Odpada konieczność powtarzania ciągle tych samych struktur danych i ekranów w każdym nowym systemie, jednocześnie mamy zapewnioną funkcjonalność logowania i autoryzacji do której potencjalny użytkownik naszych usług może mieć zaufanie (w końcu sprawę wspiera MSWIA). Proces autoryzacji i autentykacji chroniony jest przez protokół szyfrowania i wymiany kluczy ssl z certyfikatem podpisanym przez znane centrum certyfikacyjne (Certum), już nawet nie wspominając o tak długo oczekiwanej możliwośći jaką daje EPUAP czyli podpisywanie dokumentów XML profilem zaufanym (wszyscy wiedzą że podpis kwalifikowany niestety się nie przyjął). Same zalety, dlatego bardzo się zdziwiłem jak trudno było to zrobić, a przecież proces integracji powinien być jak najbardziej prosty aby zachęcić kolejne urzędy do skorzystania z tego wielce pożytecznego portalu.
EPUAPhttp://epuap.gov.pl jest portalem na którym można (albo niedługo można będzie) załatwić online, przez internet bez ruszania się z domu wszystkie sprawy pomiędzy obywatelem a urzędem (koncepcja tzw. eUrzędu)
SSO czyli Single sign-on jest koncepcją mającą rozwiązać koszmar konieczności pamiętania dziesiątek loginów i haseł wymaganych do dostawania się do różnych usług w internecie. Koncepcja ta dosyć dobrze ralizowana w sieciach korporacyjnych (np. LDAP itp.) W internecie napotyka na wiele problemów. Jednym z nich jest chyba mnogość rozwiązań proponowanych przez wiele firm i organizacji standaryzacyjnych. W wielkim skrócie polega to na tym że jest jeden centralny serwer (oczywiście to kwestia umowna równie dobrze to może być i farma serwerów) przechowujący unikatowe loginy i hasła użytkowników. Fachowo ta maszyna a wlaściwie serwer programowy nazywa się IDP (Identity Provider) czyli dostawca tożsamości, nazwa ta dosyc dobrze ddaje rolę tego oprogramowania jako czegoś co przechowuje dane autentykujące i autoryzujace ale również potrafi te dane dostarczać do dostarczycieli usług (czyli naszych systemów) tzw. SP (Service Provider). Czyli dostarczyciel usług (SP) może się spytać dostarczyciela tożsamości (IDP) o to czy jakiś użytkownik jest zaufany, lub nawet poprosić IDP o przeprowadzenie pełnego procesu autentykacji. I tu juz w zależności od implementacji i przyjętych standardów serwer IDP może tylko odpowiedzieć i mieć już spokój (np. LDAP) albo może nawet ustanowić sesję dla zalogowanego konta (np. serwery implementujące standard SAML.
SAML jest protokołem opracowanym przez agencję standaryzacyjną OASIS SAML 2.0 zapewniającym właśnie wymianę informacji o danych autentykacyjnych i autoryzacyjnych pomiędzy dostarczycielami usług (SP) a dostarczycielami tożsamości (IDP). SAML jest protokołem opartym na XMLu natomiast sposoby przekazywania dokumentów XMLowych pomiędzy SP i IDP bazują na standardowych metodach dostępnych w technologiach WEB czyli np. wysłanie danych POSTem jako ukryte pole o nazwie "SAMLRequest" odpowiedniego dokumentu XML (zgodny ze specyfikacją SAMLa) np. <AuthnRequest>, dokument ten również może zostać wysłany GETem jako parametr o tej samej nazwie. IDP wtedy odpowiada SP danymi zawartymi w odpowiednio sformatowanym XMLu. Oczywiście nie mam zamiaru tu powtarzać informacji z dokumentacji OASIS dotyczącej SAMLa, dodam tylko że dokumentacja ta niestety jest bardzo rozbudowana i napisana nie po Polsku (a to ci niespodzianka). Natomiast mam wrażenie że mechanizmy autoryzacyjne użyte w portalu EPUAP nie implementują całości tej specyfikacji a jedynie to co jest potrzebne w tym akurat zastosowaniu. Dlatego ja również nie będę się silił na opisanie całości specyfikacji SAML 2.0 tylko skupię się na jak najprostrzej implementacji w PHP która doprowadzi nas do ustanowienia zaufanej sesji z serwerem EPUAP (a właściwie to z adresem https://hetmantest.epuap.gov.pl/DracoEngine2/draco.jsf gdzie znajduje się testowy serwer zarządzania bezpieczeństwem testowej wersji platformy EPUAP)
Na początek ustalmy pewien fakt, mianowicie zaczynając przygodę z integracją naszego systemu z portalem EPUAP na początku będziemy mieli do czynienia z testową wersji portalu EPUAP dostępnego pod adresem http://test.epuap.gov.pl a nie z wersją produkcyjną dostępną pod adresem http://epuap.gov.pl. Obie wersji są prawie identyczne, różnią się tylko kilkoma szczegółami.
System EPUAP składa się z EPUAPu właściwego i z konsoli DRACO (oczywiście to tylko uproszczenie). Konsola DRACO dostępna jest pod adresem https://konsolahetmantest.epuap.gov.pl/DracoConsole/ lub przez portal EPUAP link "Moje konto" a następnie "Uprawnienia" i służy do zarządzania kontem użytkownika (również w organizacji) i jego uprawnieniami.
Żeby móc zarządzać kontam użytkownika w EPUAPie trzeba je najpierw utworzyć, jest to proste więc nie będę tu tego opisywał, trzeba tylko pamiętać żeby w trakcie tego tworzenia dopisać się (lub założyć od nowa) do naszej organizacji w imieniu której integrujemy systemy.
Teraz mała uwaga choć pod odnośnikiem "Pomoc" zakładka "Integratorzy" link "Dokumentacja" istnieje obszerna dokumentacja mająca wspomóc proces integracji platformy EPUAP z innymi systemami informatycznymi lepiej jest korzystać z wersji na produkcyjnym EPUAPie niż tym tyestowym z uwagi na to że dokumenty na testowej wersji nie są w najnowszych wersjach (kto nie wierzy niech sprawdzi dokument "Wykorzystanie SAML 2.0 w systemie ePUAP" na wersji portalu produkcyjnej i testowej, przewińcie tylko dokumenty do końca).
Niestety w trakcie tego zakładania konta nie dostajemy pewnego bardzo ważnego dla nas uprawnienia, mianowicie bardzo jest nam potrzebne uprawnienie "Administrator aplikacji" bez tego uprawnienia nie możemy w prosty sposób dodać ani aplikacji, ani grupy w której będziemy mogli umieszczać nasze aplikacji, ani co równie ważne tzw. "Agenta" który musi być powiązany z naszą aplikacją a który jakby pilnuje aby komunikacja pomiędzy EPUAPem a naszym systemem odbywała się przy pomocy protokołu ws-security wykorzystującego certyfikaty x509 (to akurat przy SSO nie jest ważne, ale nabiera znaczenia przy dalszej komunikacji z usługami Web udostępnianymi przez ePUAP.
Dlatego oszczędzcie sobie męki którą ja przeszedłem (próbując dodać aplikację nie mając do tego uprawnień, hi hi ale udało się) napiszcie maila na konto mailto:integracja.epuap@mswia.gov.pl z prośbą o nadanie wam uprawnienia "Administratora aplikacji". Co prawda na etapie SSO nie będzie to nam aż tak potrzebne, ale jeśli spróbujecie kontynuować komunikację z EPUAPem przez protokół SOAP zabezpieczony przez nagłówki ws-security to niestety bez tego uprawnienia się nie obejdzie.
Słowo "Aplikacja" ma niestety w EPUAPie bardzo szerokie znaczenie i jest to zarówno aplikacja w rozumieniu formatek, procesów, raportów itp. rzeczy zarządzanych przez sam EPUAP, jak i aplikacją jest wpis w konsoli Draco oznaczający wirtulany byt reprezentujący nasz system informatyczny który dla EPUAPu jest jakimś bytem zewnętrznym z którym EPUAP będzie się komunikował, wreszcie jako aplikacja jest traktowana skrytka czyli miejsce gdzie można umieszczać dokumenty.
Na początku nie bardzo nawet rozumiałem z czym mam się łączyć po stronie EPUAPu aby ustanowić tą sesję SSO (co podać jako <Issuer> w zapytaniu o autoryzację), W dokumentacji raz było napisane że ma to być "System" (kolejny byt w EPUAPie) innym razem że "Aplikacja" (Naprawdę czytając dokumentację można dostać kręćka). Nie jestem co prawda na 100% tego pewien, ale z doświadczeń wychodzi mi że powinna to być aplikacja (próby połączenia z systemem mi nie wyszły.
Jednak nie mając uprawnień "Administrator aplikacji" nie można w konsoli Draco dodać żadnaj aplikacji, jednak jeśli macie już uprawnienia Administratora aplikacji w konsoli Draco dodajcie jakąś nową aplikację wpisując w jej parametrach jakąś zgrabną nazwę i unikalny identyfikator aplikacji (nie musi być to wcale url choć może). Jeśli natomiast nie macie jeszcze uprawnienia "Administrator aplikacji" a już bardzo chcecie się pobawić SSO jest na to pewna sztuczka, mianowicie aby dodać coś co będzie aplikacją umożliwiającą nam autoryzację SSO (ale nie połączenia przy pomocy ws-security), tym razem w samym EPUAPie klikamy link "Moje konto" następnie zakładka "Konfiguracja" następnie podzakładka "Podsystem komunikacyjny" i link "Lista skrytek" gdzie przy pomocy guzika "Dodaj skrytkę" możemy dodać nową skrytkę. A tu niespodzianka, skrytka dodana na portalu EPUAP pojawia się w konsoli Draco na liście aplikacji jako aplikacja (choć do dodania aplikacji praw jeszcze nie mieliśmy).
Przy okazji opiszę kolejną sztuczkę odkrytą w trakcie wielogodzinnego bezsensowengo klikania po EPUAPie i konsoli Draco w celu odkrycia o co w nich chodzi (bo z dokumentacji jakoś nie szło zrozumieć celowości elementów EPUAPu). Mianowicie może byśmy chcieli aby nasza aplikacja była dodana do jakiejś grupy aplikacji (czyli aby w aplikacjach była umieszczona w konkretnej grupie a nie w grupie domyślnej. Okazuje się że co prawda nie posiadając uprawnienia "Administrator aplikacji" można dodać coś co się nazywa "Grupa" jednak bez tego uprawniena (z uprawnieniem "Administrator aplikacji" wystarczy po prostu wejść w "Aplikacje" wybrać jakąś aplikację a potem zakładkę "Grupy aplikacji") nie można sprawić aby ta grupa choćby pojawiła się na liście grup w zakładce "Aplikacje" (w konsoli Draco). Ale jak już pisałem jest na to obejście.
Okazuje się że wystarczy (mając wcześniej dodaną już grupę w konsoli Draco) wejść do EPUAPu, wybrać "Moje konto" potem zakładkę "Środowisko do budowy aplikacji" i w tych aplikacjach (które tym razem są aplikacjami wewnętrznymi portalu EPUAP) wybrać przycisk "Dodaj" co spowoduje dodanie aplikacji wewnętrznej. Wypełniając pola z nazwą tej aplikacji poniżej zobaczymy pole które się nazywa "Grupa DRACO" musimy tam wpisać nazwę grupy którą wcześniej dodaliśmy w konsoli Draco. Po zapisaniu nowej aplikacji będziemy mogli przejść do edycji nowo dodanej aplikacji wewnętrznej gdzie będziemy mogli dodać skrytke którą już wcześniej również utworzyliśmy. Te dziwne operacje spowodują że w konsoli Draco wchodząc do aplikacji na liści rozwijalnej grup zobaczymy naszą grupę a w tej grupie skrytkę która choć jest skrytką to jest również aplikacją (nie do końca, ponieważ do takiej aplikacji nie będzie się można łączyć przy pomocy usług WebService chronionych przy pomocy podpisu zawartego w nagłówkach protokołu ws-security). Jak już pisałem jeśli mamy uprawnienie "Administratora aplikacji" nie musimy się tak wygłupiać, bo wszystko jest prostrze i możemy całość konfiguracji wykonać przy pomocy konsoli DRACO.
Dobrze dosyć tego nudnego wstępu konfiguracji itp. (jednak to niezrozumienie potrzeby właściwej konfiguracji było głównym powodem tego że serwer EPUAPu ignorował na początku moje wszystkie próby autoryzacji pzy pomocy protokołu SAML 2.0) Przede wszystkim trzeba zrozumieć co to tak na prawdę jest TGSID (dosyć ciężko to jest znaleść w oficjalnej dokumentacji). A więc (wiem wiem tak się nie zaczyna zdania) jest to identyfikator sesji (phi tyle to i w oficjalnej dokumentacji można znaleść) a tak na prawdę to po prostu zwykłe ciastko (cookie) ustanowione w naszej przeglądarce internetowej (order cierpliwości i uwagi temu kto tą informację znajdzie w dokumentacji) o nazwie właśnie TGSID i odpowiednim czasie trwania. Nie stety parametry tego ciastka musimy najpierw uzyskać od EPUAPu dopiero wtedy możemy zrobić takie ciastko, tak tak sami musimy je zrobić EPUAP tego za nas nie zrobi (a szkoda). Chociaż lekka poprawka EPUAP robi taki ciastko jeśli do zalogowania użyjemy funkcjonalności EPUAPU natomiast jeżeli zostaniemy automatycznie przekierowani na stronę logowania EPUAPu w trakcie wymiany danych protokołem SAML takie ciastko nie jest automatycznie tworzone tylko sami musimy to zrobić.
Poczułem w tym momęcie chęć aby napisać kilka słów wyjaśnenia.Każdy dośwadczony programista powinien się gdzieś tutaj właśnie zastanowić, dlaczego u licha ten gość nie wzioł sobie jakiejść gotowej biblioteki programistycznej do obsługi SAMLa i po co się ręcznie bawił w klecenie tych XMLi. Odpowiadam, a jakże próbowałem, znalazłem dwie PHPowe biblioteki do obsługi SAMLa są to "SimpleSAML" i "OneLogin" (słowo simple w nazwie biblioteki "SimpleSAML" jest niewłaściwe dla tej bardzo skomplikowanej w konfiguracji biblioteki) Jednak po dosyć długo trwających próbach zmuszenia tych bibliotek do dogadania się z EPUAPem musiałem się poddać (dodatkowo biblioteka OneLogin jest płatna), jednak lektura kodów źródłowych biblioteki SimpleSAML lepiej mi wyjaśniła logikę działania SAMLa niż oficjalna jego dokumentacja ze stony OASIS, dodatkowo w bibliotece "SimpleSAML" jest wielce wygodny "debugger" do odkodowywania komunikacji pomędzy klientem a serwerem)
Ale wracając do konkretów. Naszym celem jest w tym momęcie uzyskanie od serwera bezpieczeństwa EPUAPu (który ma ładną nazwę "hetman") tego właśnie identyfikatora sesji o nazwie TGSID. Aby to zrobić musimy wymienić się z serwerem bezpieczeństwa pewnymi dokumentami XML (ściśle zdefiniowanymi w protokole SAML 2.0) zawierającymi nasze zapytania "request" i odpowiedzi serwera "response". Załóżmy że sprawdziliśmy w naszej aplikacji że nie posiadamy u siebie ciastka (cookie) o nazwie TGSID. Oznacza to że nie jesteśmy w tym momęcie zalogowani do portalu EPUAP. Sprawdzenie to można wykonać np. takim kodem PHP
<?php if(!isset($_COOKIE['TGSID'])) //jak ja nie cierpię notacji javy i wstawiania nawiasów po ifie { loginToEPUAP(); // umowna funkcja oznaczająca akcję logowania } ?>
Jeśli nie mamy autoryzacji musimy w parametrze (typu GET) o nazwie "SAMLRequest"
wysłać do serwera bezpieczeństwa na adres https://hetmantest.epuap.gov.pl/DracoEngine2/draco.jsf dokument XML w którym korzeń głowny jest elementem o nazwie <AuthnRequest>
cały URL będzie wyglądał mniej więcej tak https://hetmantest.epuap.gov.pl/DracoEngine2/draco.jsf?SAMLRequest=sHJbBxz+eFNMIBPsxC2SaOf/u+e0UBWXgoIuJx8Hxk5XZq/jTcE+ZU=
Gdzie ta bezsensowna zawartość parametru SAMLRequest
jest po prostu dokumentem XML poddanym operacji deflate
a potem operacji kodowania base64
.
Sam dokument <AuthnRequest>
może wyglądać jak ten poniższy przykład. Oczywiście konieczna jest zmiana pewnych wartości aby dostosować zapytanie do identyfikatora aplikacji jaki posiada nasza aplikacja w konsoli Draco (chodzi o identyfikator aplikacji a nie o nazwę) i do adresu na jakie ma być wykonane przekierowanie po zakończeniu procesu autoryzacji.
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="_e68ca0e50e0ca2d93758" Version="2.0" IssueInstant="2011-07-13T18:07:31Z" Destination="https://hetmantest.epuap.gov.pl/DracoEngine2/draco.jsf" IsPassive="false" AssertionConsumerServiceURL="http://url_na_ktory_nastapi_przekierowanie_po_autoryzacji_przez_epuap"> <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> identyfikator_aplikacji </saml:Issuer> </samlp:AuthnRequest>
Element <Issuer>
powinien zawierać właśnie identyfikator aplikacji z konsoli DRACO
Atrybut <AssertionConsumerServiceURL>
elementu <AuthnRequest>
powinien zawierać adres url skryptu w naszej aplikacji który zajmie się obsługą odpowiedzi od serwera EPUAP.
Atrybut <Destination>
elementu <AuthnRequest>
powinien zawierać adres serwera bezpieczeństwa DRACO
Atrybut <IssueInstant>
elementu <AuthnRequest>
Powinien zawierać czas żądania w formacie TimeStamp
Atrybut <ID>
elementu <AuthnRequest>
Powinien zawierać unikatowy identyfikator żądania
Serwer bezpieczeństwa może się teraz zachować na kilka sposobów. Jeśli stwierdzi że sesja jest aktywna (jest ciastko TGSID) to po prostu wyśle nam odpowiedz na adres podany mu w atrybucie<AssertionConsumerServiceURL>
dopisując do niego parametr o nazwie SAMLart
zawierający referencje do artefaktu.
Referencja do artefaktu jest jakby identyfikatorem który musimy serwerowi podać w zapytaniu typu <ArtifactResolve>
które będziemy serwerowi już wysyłać przy pomocy protokołu SOAP (jeszcze bez nagłówków ws-security) po to aby uzyskać odpowiedź zawierającą (lub nie w przypadku niepoprawniej autoryzacji) identyfikator sesji TGSID.
Jednak w przypadku gdy serwer bezpieczeństwa EPUAPu nie stwierdzi obecnośći ważnego ciastka TGSID (a tym samym nie stwierdzi aktywnej sesji konkretnego użytkownika) przekieruje przeglądarke na ekran logowania EPUAPu gdzie użytkownik będzie mógł wpisać swój login i hasło do swojego konta na EPUAPie. Po poprawnym wpisaniu danych autentykacyjnych przeglądarka ponownie zostanie przekierowana tym razem już na adres przesłany przez nas w atrybucie <AssertionConsumerServiceURL>
a do URLa zostanie dopisany parametr o nazwie SAMLart
zawierający referencje do artefaktu, przy pomocy której będziemy mogli wysłać do serwera zapytanie typu <ArtifactResolve>
.
Niestety z moich doświadczeń (żmudnych) wynika że jakikolwiek błąd w zapytaniu <AuthnRequest>
powoduje wyświetlenie nam ekranu błędu EPUAPu z opisem "Błąd uwierzytelnienia :-( co jest raczej informacją zawierającą zbyt mało treści aby na tej podstawie domyślić się co zrobiliśmy nie tak. Niestety pozostaje nam żmudna metoda prób i błędów aż w końcu się uda.
Proszę uważać z przykładem zapytania<AuthnRequest>
umieszczonym w dokumentacji dla integratorów na EPUApie, dokumencie "Wykorzystanie SAML 2.0 w systemie ePUAP" jest błąd powodujący brak uwierzytelnienia. Błąd jest w domyślnej przestrzeni nazw w elemencie<Issuer>
.
A oto przykładowy kod tworzący właściwe zapytanie typu <AuthnRequest>
i wysyłający je do testowego serwera bezpieczeństwa EPUAPu
<?php /** * Przykladowy kod rozpoczynajacy proces autentykacji SSO na serwerze EPUAP przy pomocy protokolu SAML 2.0 * @author Extern */ error_reporting(E_ALL); require 'common.php'; if(!isset($_COOKIE['TGSID']) || true) { $SAMLRequest="<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"".getUniqueID(20)."\" Version=\"2.0\" " ."IssueInstant=\"".getTimestamp()."\" " ."Destination=\"https://hetmantest.epuap.gov.pl/DracoEngine2/draco.jsf\" " ."IsPassive=\"false\" " ."AssertionConsumerServiceURL=\"http://localhost/consume.php\">" ."<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">myAppId</saml:Issuer> </samlp:AuthnRequest>"; $url ="https://hetmantest.epuap.gov.pl/DracoEngine2/draco.jsf?SAMLRequest=".addslashes(rawurlencode(base64_encode(gzdeflate($SAMLRequest)))); header("Location: $url"); } else { echo "sesja autoryzacyjna już istnieje i nie trzeba jej ustanawiac"; } ?>
Maniakom obiektowego podejścia do programowania, wyjaśniam że kod PHP starałem się umieścić w jak najprostrzej formie a niepotrzebne w tym szkoleniowym przypadku ubieranie kodu w paradygmat obiektowy by niepotrzebnie zaciemniło kwintesencję problemu.
A oto kod funkcji wspólnych umieszczony w pliku o nazwie 'common.php' (includowany w pliku głównym) do generacji unikalnego Id zapytania, znaku czasu timestamp itp. po to aby już niepowtarzać kodu gdyż tych samych funkjcji użyjemy w dalszej części komunikacji. Oczywiście jeśli kody odbierające odpowiedz zdecydujecie się umieścić w tym samym pliku co tworzenie i wysyłanie <AuthnRequest>
to wtedy nie trzeba tych funkcji trzymać oddzielnie i można je wrzucić do jednego pliku
<?php function getUniqueID($length) { $chars = "abcdef0123456789"; $chars_len = strlen($chars); $ID = ""; for ($i = 0; $i < $length; $i++) $ID .= substr($chars,rand(0,15),1); return "_".$ID; } function getTimestamp($interval=0) { date_default_timezone_set('Europe/Warsaw'); return strftime("%Y-%m-%dT%H:%M:%SZ",mktime()+$interval); } ?>
Jak widać kod tworzący zapytanie do serwera, nie jest specjalnie rozbudowany (uwierzyć nie mogę że sklecenie tych kilku linijek zajęło mi dwa tygodnie, ale jeśli ktoś się zaśmieje w tym momęcie to niech natychmiast przestanie czytać ten poradnik niech zapomni całą jego treść i niech zrobi to samodzielnie, ciekawe czy potem jeszcze będzie miał ochotę na śmiech). Najważniejsze to utworzyć właściwą zawartość zmiennej $SAMLRequest, a szczególną uwagę trzeba zwrócić na zawartość elementów <Issuer>
(id aplikacji) i <AssertionConsumerServiceURL>
(adres url powrotu do naszego kodu).
Kolejną ważną rzeczą jest aby ustawić właściwą zawartość zmiennej $url razem z jej parametrem "SAMLRequest" który musi zawierać zawartosć zmiennej $SAMLRequest (czyli dokument xml <AuthnRequest>
) poddaną operacji deflate a następnie kodowaniu base64. Samo wysłanie zapytania można wykonać w najprostrzy z możliwych sposobów czyli jako przekierowanie uzyskane przez ustawienie nagłówka (Header) "Location:" na przygotowany przez nas URL.
Co prawda specyfikacja SAML 2.0 mówi że zmienną "SAMLRequest" można wysłać również POSTem ale chyba EPUAP nie uwzględnia takiej możliwości (szczerze mówiąc to tego nie testowałem) ja zadowoliłem się najprostrzeym sposobem czyli przez GET.
Jeśli mieliśmy już tę odrobinę szczęścia i udało się nam wysłać poprawnie poprawny <AuthnRequest>
. Powinniśmy koniec końców dostać odpowiedz, a konkretnie serwer bezpieczeństwa systemu EPUAP wywoła naszą stronę właśnie tę której adres mu przekazaliśmy w dokumencie <AuthnRequest>
jako atrybut <AssertionConsumerServiceURL>
. Jednak dopiszę do tego urla parametr "SAMLart" zawierający referencję do artefaktu. Pewno wygodniej było by dla nas gdyby EPUAP od razu przekazywał nam w parametrze "SAMLResponse" dokument XML <Response>
jednak EPUAP tak nie robi zamiast tego dostajemy tą referencję do artefaktu dzięki której możemy sobie znowu odpytać EPUAP w zapytaniu <ArtifactResolve>
o dokument <ArtifactResponse>
który w atrybucie ID
elementu <Assertion>
zawiera już w końcu tą upragnioną wartość TGSID.
Oto przykładowy dokument <ArtifactResolve>
który musimy wysłać do serwera bezpieczeństwa EPUAPu aby w odpowiedzi otrzymać TGSID (oczywiście zawarty w dokumencie <ArtifactResponse>
.
<samlp:ArtifactResolve ID="_0fff696fe8c2faca0601" IssueInstant="2011-07-16T18:01:40Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> identyfikator_aplikacji </saml:Issuer> <samlp:Artifact> AAP+FZr4mGfBGOfEUT4MQDxvvklW7wAAAAAAAAA= </samlp:Artifact> </samlp:ArtifactResolve>
Najbardziej dla nas istotnymi elementami w tym dokumencie są tagi <Issuer>
w którym musimy umieścić identyfikator aplikacji (ten sam co w zapytaniu <AuthnRequest>
). Natomiast w elemencie <Artifact>
musimy umieścić referencję do artefaktu czyli zawartość parametru "SAMLart"
który to wcześniej odtrzymaliścy (w urlu jako parametr "SAMLart") w odpowiedzi na nasze zapytanie <AuthnRequest>
.
Nie stety, aby dalej nie było za prosto, EPUAP oczekuje od nas że tym razem wyślemy mu ten dokument nie prostym zapytaniem HTTP typu GET tylko przy pomocy protokołu SOAP jako wywołanie usługi WebService mieszczącej się pod adresem https://hetmantest.epuap.gov.pl/axis2/services/EngineSAMLArtifact
a nazywającej się "artifactResolve"
(a to ci niespodzianka). Dokument WSDL opisującej oczekiwany format koperty SOAP znajduje się pod adresem https://hetmantest.epuap.gov.pl/axis2/services/EngineSAMLArtifact.wsdl
Lektura dokumentu WSDL pozwala stwierdzić że EPUAP oczekuje przekazania koperty SOAP jako stylu akcji o typie "document" używając do serializacji parametrów sposobu "literal". Oznacza to że dokument <ArtifactResolve>
może być umieszczony w kopercie SOAP bezpośrednio w elemencie <SOAP-ENV:Body>
bez otaczania go informacją o akcji i parametrach.
Dokumenty kopert SOAP mogą być budowane w dwóch stylach "rpc" lub "document". Styl rpc oznacza że element "soap:body" musi być zbudowany w specjalny sposób mianowicie musi w sobie zawierać element reprezentujący akcję (metodę) która ma być wykonana oraz parametry jakie mają być do metody przekazane np.<env:Body> <m:nazwaMetody xmlns:m="namespace"> <m:par1>...</m:par1> <m:par2>...</m:par2> </m:nazwaMetody> </env:Body>Natomiast styl "document" oznacza że w "soap:body" może być zawarty dokument dowolnie sformatowany,<env:Body> <dowolnyDokument> . . . </dowolnyDokument> </env:Body>Wymaganiem jednak w takim przypadku jest aby to w nagłównku HTTP był umieszczony zapis "SOAPAction:" oznaczający metodę jaka ma być wywołana w celu obsłużenia prośby o usługę.
Do generacji i wysłania koperty SOAP wykorzystałem bibliotekę "Nusoap", domyśnie ta biblioteka tworzy i wysyła kopertę SOAP przy pomocy stylu "rpc", jednak można to zrobić również używając stylu "document", aby to zrobić funkcję call()
trzeba wywołać z dodatkowym czwartym paametrem odpowiedzialnym za ustawienie nagłówka HTTP o nazwie SOAPAction
na nazwę metody wywoływanej, oraz trzeba ustawić parametr siódmy na styl "document", oczywiście w takim przypadku w parametrze drugim nie ustawiamy tablicy parametrów tylko bezpośrednio wstawiamy string zawierający właściwy dokument xml czyli <ArtifactResolve>
.
Oto przykładowy kod tworzący właściwy dokument <ArtifactResolve>
wysyłany do serwera bezpieczeństwa EPUAPu przy pomocy protokołu SOAP.
<?php /** * Przykladowy kod odbierajacy parametr SAMLart i na tej podstawie przygotowujący zapytanie typu ArtifactResolve celem pobrania identyfikatora TGSID * @author Extern */ error_reporting(E_ALL); require 'Nusoap.php'; require 'common.php'; if(isset($_GET["SAMLart"])) { $artifactResolve="<samlp:ArtifactResolve ID=\"".getUniqueID(20)."\" " ."IssueInstant=\"".getTimestamp()."\" Version=\"2.0\" " ."xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">" ."<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">myAppID</saml:Issuer>" ."<samlp:Artifact>".$_GET["SAMLart"]."</samlp:Artifact>" ."</samlp:ArtifactResolve>"; $wsdl='https://hetmantest.epuap.gov.pl/axis2/services/EngineSAMLArtifact'; $client=new nusoap_client($wsdl); $client->soap_defencoding = 'UTF-8'; $client->decode_utf8 = true; $client->http_encoding = 'UTF-8'; $client->xml_encoding = 'UTF-8'; //$client->setHTTPProxy("IP_PROXY_SERVER",8080,"user_name","user_password"); $result = $client->call('artifactResolve', $artifactResolve,'http://tempuri.org',"artifactResolve",false,null,'document'); if($client->fault) { echo 'Error: '.$client->fault; print_r($result); } else { if ($client->getError()) { echo 'Error2: '.$client->getError(); print_r($result); } else { if(isset($result)) { if($result['Response']['Status']['StatusCode']['!Value'] =='urn:oasis:names:tc:SAML:2.0:status:Success') { setcookie("TGSID", substr($result['Response']['Assertion']['!ID'],1,strlen($result['Response']['Assertion']['!ID'])), strtotime($result['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['!NotOnOrAfter'])); $nameId = $result['Response']['Assertion']['Subject']['NameID']['!']; echo 'AUTORYZACJA POPRAWNA'; echo '<h2>Result</h2><pre>'; print_r($result); echo '</pre>'; } else { echo "zapytanie nie zakończylo sie sukcesem"; } } } } echo '<h2>Request</h2>'; echo '<pre>' . htmlspecialchars($client->request, ENT_QUOTES) . '</pre>'; echo '<h2>Response</h2>'; echo '<pre>' . htmlspecialchars($client->response, ENT_QUOTES) . '</pre>'; echo '<h2>Debug</h2>'; echo '<pre>' . htmlspecialchars($client->debug_str, ENT_QUOTES) . '</pre>'; } else { echo "brak parametru SAMLart"; } ?>
W przypadku jeśli pracujecie w sieci lokalnej i dostęp do internetu jest możliwy tylko przy pomocy serwera PROXY warto przed wywołaniem metody call()
na cliencie "nusoap" wywołać metodę setHTTPProxy()
aby ustawić parametry serwera proxy, nazwę uzytkownika i hasło. Dane do ustawienia tych parametrów można znaleść w ustawieniach połączenia waszej przeglądarki internetowej (lub pomęczyć o nie waszego administratora), szczęśliwcy nie mający takich ograniczeń nie muszą oczywiście tego robić i mogą linię $client->setHTTPProxy(....);
pozostawić zakomentowaną.
Jeśli wywołanie funkcji się powiedzie, czyli $client->fault
nie jest ustawiony (niestety biblioteka "nusoap" tkwi jeszcze głęboko w czasach php4 i nie uznaje wyjątków) warto sprawdzić w wyniku zawartość elementu <Status><StatusCode><Value> czy jest tam ciąg "urn:oasis:names:tc:SAML:2.0:status:Success" i jeśli tak możemy ustawić to upragnione przez nas ciastko (cookie) w ten sposób setcookie("TGSID", substr($result['Response']['Assertion']['!ID'],1,strlen($result['Response']['Assertion']['!ID'])), strtotime($result['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['!NotOnOrAfter']));
.
Jak widać identyfikator sesji TGSID znajduje się w elemencie <Response><Assertion><ID>
(mamy go mamy w końcu ufffff)
Z nieznanych mi przyczyn identyfikator TGSID jaki dostaliśmy, na pierwszej pozycji zawiera znak podkreślenia "_", operatorzy pomocy dla integratorów zalecili mi wyciąć ten znak (po to jest tam substr()
, ale jeśli go zostawimy sesja również zostanie ustanowiona poprawnie, na wszelki wypadek robię tak jak zalecili mi doradcy z MSWIA (a może jednak czegoś nie wiem i ma to znaczenie. Warto również zauważyć że ciastko to warto ustawić na określony czas trwania, a czas ten dostaliśmy w elemencie <Assertion><Subject><SubjectConfirmation><SubjectConfirmationData><NotOnOrAfter>
sesja w końcu nie może trwać wiecznie (EPUAP testowy ustawia ją chyba obecnie na godzinę).
Hmmm to już właściwie chyba wszystko w tym temacie. Z dodatkowych rad jakie jeszcze mogę udzielić to taka, abyście jeśli natraficie na problem z którym macie kłopot, nie męczyli się z nim zbyt długo, tylko po prostu napisali na adres mailto:integracja.epuap@mswia.gov.pl. Mocno zawiedziony dokumentacją dostępną na podstronie dla integratorów, byłem jednak dosyć mile zaskoczony jakością porad jakie dostawałem w odpowiedzi na moje zapytania kierowane na ten adres, choć nie zawsze wyjaśniano mi wszystko o co mi chodziło to jednak zawsze każda odpowiedź popychała mnie odrobinę dalej albo przynajmniej naprowadzała mnie na właściwe rozwiązanie, więc warto pytać, adpowiadają w miarę szybko i rzeczowo. Powodzenia w integracji z EPUAPem życzę, a w następnym odcinku z tej seri (o ile powstanie) postaram się opisać proces komunikacji z EPUAPem z wykorzystaniem protokołu SOAP autentykowanego przy pomocy protokołu "ws-security" umieszczanego w "soap:header" koperty SOAP, też niezła jazda, ale się daje zrobić, co osobiście niedawno przetestowałem.