Testy integracyjne są szczególnie kłopotliwe, gdy w aplikacji wykorzystujemy wiele rozwiązań zewnętrznych. Musimy posiadać faktyczną bazę danych, z której skorzystamy w teście. Jeżeli zapisujemy pliki na zewnętrzny dysk/serwis (np. Azure Blob Storage), to jego instancję testową również musimy użyć (akurat w przypadku Bloba nie jest to trudne – używamy po prostu lokalnego emulatora). Czy w końcu musimy mieć użytkownika na którym testować będziemy nasze metody. Jeżeli mamy tych użytkowników w naszej bazie danych, nie jest to aż takie trudne. Po 1. sami mamy nad niemi kontrolę, po 2. zapewne sami pisaliśmy mechanizmy uwierzytelniania i autoryzacji – możemy więc sami w niego zaingerować w testach. Natomiast większy problem pojawia się, gdy korzystamy z zewnętrznego dostawcy tożsamości.
Czym jest Azure Active Directory B2C
Azure Active Directory B2C jest usługą dostarczającą tożsamość użytkownika w modelu SaaS. Bardziej po ludzku, dzięki AAD B2C nie musimy się przejmować w jaki sposób i gdzie przechowywane są dane użytkownika. To Microsoft zapewnia bezpieczeństwo. My podłączając tę usługę nie musimy się martwić o zabezpieczanie, czy zarządzanie danymi użytkowników. Po zalogowaniu, identity użytkownika jest nam po prostu dostarczane. AAD B2C pozwala na rejestrację, logowanie, zmianę danych. Mamy możliwość użycia MFA 1, innych dostawców tożsamości niż email i hasło (np.: Google, Microsoft, Twitter itp.), czy tworzenia grup użytkowników o czym dzisiaj.
Kontekst
Powiedzmy, ze mamy w aplikacji metody, które może wykonać tylko zalogowany użytkownik. Do tego służy nam annotacja [Authorize]
w ASP.NET. Natomiast, mamy dodatkowo kilka grup użytkowników. Powiedzmy, że pewne akcje mogą wykonywać tylko administratorzy. Jest to przypadek bardzo często spotykany prawie w każdej aplikacji. Problem pojawia się, gdy trzeba przetestować integracyjnie taką metodę do której dostęp ma tylko odpowiednia grupa użytkowników. I ta grupa sprawdzana jest nie poprzez middleware, a dopiero w ciele danej metody kontrolera którą testujemy. Przykład który wykorzystam do pokazania pochodzi z projektu HRMaster, który można znaleźć na moim GitHubie.
To co należy zrobić, to stworzyć ClaimsPrincipal z Identity tworzonym na podstawie Claimów. Claim to nic innego jak para klucz-wartość. W naszym przypadku tym Claimem jest id użytkownika należącego do odpowiedniej grupy AAD B2C – takiej, która może wykonywać metodę którą testujemy. Identity na podstawie Claimów użytkownika dostarcza nam klasa ClaimsIdentity
. Podczas jej tworzenia musimy dostarczyć tablicę Claimów, które ma posiadać nasz „użytkownik”. Zabierzmy się więc do tworzenia takiego „użytkownika”.
Na temat ClaimsPrincipal ani ClaimsIdentity rozpisywał się nie będę. Można zerknąć do dokumentacji co te klasy robią. Zajmiemy się więc rzeczą, która nas interesuje w tym momencie, czyli tworzeniem Claima, który zidentyfikuje zaślepkę użytkownika jako należącego do interesującej nas grupy AAD B2C.
Pierwszym argumentem metody tworzącej nowy Claim jest jego nazwa. W tym przypadku aplikacja wykorzystuje claim o nazwie nameidentifier, którego schemę można znaleźć pod adresem: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier. Musimy więc podać ten URL jako pierwszy parametr naszej metody ustawiającej Claimy użytkownika. Drugi argument to value – id użytkownika z nadaną odpowiednią grupą. Jest to guid mający następujący format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
.
Tworzenie użytkownika o odpowiednich claimach wygląda więc następująco:
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") }));
Gdy mamy stworzony claim, musimy go ustawić jakoś dla zapytania, które chcemy wykonać w teście. ClaimsPrincipal ustawiamy w kontekście HTTP dla całego kontrolera. Robimy to w następujący sposób:
// Tworzymy instancję kontrolera var controller = new CompaniesController(); var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") })); controller.ControllerContext = new ControllerContext() { HttpContext = new DefaultHttpContext() { User = user } };
Tym sposobem, możemy wywołać metodę z kontrolera, która wymaga użytkownika z dane grupy AAD B2C, np.:
await controller.Create();
Bezpieczeństwo w testach integracyjnych
Oczywiście niedopuszczalne są dwie sytuacje:
- Korzystanie w testach faktycznego AAD B2C, które działa również na produkcji (czyli wykorzystywanie id claima faktycznego użytkownika z danej grupy);
- Wpisywanie id użytkownika w kod (jak zrobiłem to w powyższym przykładzie.
Rozwiązaniem pierwszego problemu jest posiadanie po prostu dwóch AAD B2C – produkcyjnego i testowego. Drugi natomiast rozwiązujemy poprzez wyciągnięcie id użytkownika z pliku appsettings.json
(którego oczywiście nie commitujemy do repozytorium, bo wyszłoby na to samo). Aby pobrać wartości z appsettings.json
w kodzie wystarczy zbudować ConfigurationBuilder i pobrać z niego wartość, a następnie wpisać ją jako wartość Claima:
//Pobieram ścieżkę do folderu z plikami wynikowmi kompilacji var codeBaseUrl = new Uri(Assembly.GetExecutingAssembly().CodeBase); var codeBasePath = Uri.UnescapeDataString(codeBaseUrl.AbsolutePath); var dirPath = Path.GetDirectoryName(codeBasePath); //Tworzę config z pliku appsettings.json var config = new ConfigurationBuilder() .SetBasePath(dirPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .Build(); //Pobieram userId z pliku appsettings.json var userId = config.GetSection("TestData")["AdminUserClaim"]; //Tworzę claim z uzyciem id z konfiguracji var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] { new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", userId) }));
Plik appsettings.json
wyglądać będzie następująco:
{
"TestData": {
"AdminUserClaim": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
Musimy pamiętać, żeby nie commitować tego pliku. Najlepiej dodać appsettings.json do pliku .gitignore. Żeby powyższe rozwiązanie działało musimy jeszcze ustawić kopiowanie pliku appsettings.json do folderu wynikowego. Robimy to w Visual Studio tak:
- PPM na appsettings.json (w projekcie z testami integracyjnymi) i wybieramy Properties:
- Ustawiamy Build Action na Context i Copy to output Directory na Copy if newer:
Jak widać, nawet przy wykorzystaniu zewnętrznego dostawcy identity, testy integracyjne nie komplikują nam się znacznie (o ile oczywiście wszystko jest poprawnie zaimplementowane). Prosto tworzymy nowego IdentityProvidera z odpowiednim Claimem. Należy jedynie pamiętać, żeby poprawnie zabezpieczyć dane naszej aplikacji i AAD B2C.
Multi Factor Authentication↩