data.mdx.frontmatter.hero_image

Integracja Aplikacji .NET z Zitadel

2024-06-19 | .NET | bd90

Uwierzytelnianie i Autoryzacja to dwa procesy, bez których ciężko wyobrazić sobie nowoczesną aplikację. Platforma .NET wspomaga programistów dostarczając .NET Identity jako wbudowany element. Już od wersji 8-mej otrzymaliśmy do dyspozycji local identity na poziomie API. Niestety, na nasze nieszczęście, jest uboga w funkcjonalności. Szczególnie jeśli myślimy o stworzeniu systemu enterprise złożonego z licznych serwisów, bazujących na współdzielonych tożsamościach.

Na nasze szczęście świat it nie pozostawia nas z jednym rozwiązaniem. Jedną z opcji jest wykorzystanie protokołu OIDC aby stworzyć centralny serwis zarządzający dostępami, uwierzytelnieniem itd. Kiedyś w ekosystemie .NET popularnym rozwiązaniem był IdentityServer, natomiast, jeżeli dobrze pamiętam, to gdzieś w okolicy roku 2020 twórcy postanowili zrobić je płatnym, co pozostawiło pewną pustkę. Sporo aplikacji zmigrowało się na usługi chmurowe, takie jak Auth0, Okta, AWS Cognito, duża część przerzuciła się na OpenSource takie jak np. Keycloak. Istnieje też grupa, która pozostała na ostatniej wersji open-source IdentityServer-a. No i oczywiście nie można zapomnieć że zawsze możemy przejść na własną implementację używając bibliotek niższego poziomu jak np. OpenIddict.

W dzisiejszym artykule, który będzie dość długi, chciałbym wam przedstawić rozwiązanie open source, które ostatnio przykuło moją uwagę: Zitadel. Jest to o tyle ciekawy kawałek kodu, że można z niego korzystać na podstawie licencji Apache License 2.0, czyli pod pewnymi warunkami możemy z niego korzystać nawet w komercyjnych projektach. Więcej o tej licencji możecie przeczytać w ich repozytorium na githubie.

Zitadel - Self-Hosted Setup

Zacznijmy może o setup-u lokalnego. Przyda nam się docker, który wraz z docker-compose pozwoli nam na szybkie uruchomienie serwera Zitadel. Dla uproszczenia zebrałem wszystkie potrzebne instrukcje w jeden plik:

services:

	zitadel:
		networks:
		- common
		image: 'ghcr.io/zitadel/zitadel:latest'
		command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled'
		environment:
		- 'ZITADEL_DATABASE_POSTGRES_HOST=postgres'
		- 'ZITADEL_DATABASE_POSTGRES_PORT=5432'
		- 'ZITADEL_DATABASE_POSTGRES_DATABASE=zitadel'
		- 'ZITADEL_DATABASE_POSTGRES_USER_USERNAME=zitadel'
		- 'ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=zitadel'
		- 'ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable'
		- 'ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=myuser'
		- 'ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=mysecretpassword'
		- 'ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable'
		- 'ZITADEL_EXTERNALSECURE=false'
		depends_on:
			postgres:
				condition: 'service_healthy'
		ports:
		- '8080:8080'

	postgres:
		image: postgres:14-alpine
		ports:
		- 5432:5432
		
		environment:
			POSTGRES_PASSWORD: mysecretpassword
			POSTGRES_DB: mydatabase
			POSTGRES_USER: myuser
		
		networks:
		- common
		healthcheck:
			test:
				[
				"CMD",
				"pg_isready",
				"--username=myuser",
				"--dbname=mydatabase",
				"--host=127.0.0.1",
				"--port=5432"
				]
			interval: 2s
			timeout: 1m
			retries: 5
			start_period: 10s

	pg-admin:
		image: dpage/pgadmin4
		ports:
		- 5050:80
		environment:
			PGADMIN_DEFAULT_EMAIL: dbadmin@example.com
			PGADMIN_DEFAULT_PASSWORD: letmein

networks:
	common:

Podsumowując to co tutaj się tworzy... Oczywiście mamy kontener Zitadel wraz z konfiguracją pozwalającą na połączenie się z bazą PostgreSql. Następnie dzięki sekcji:

depends_on:
	postgres:
		condition: 'service_healthy'

informujemy docker-a, aby poczekał, aż kontener o nazwie "postgres" będzie gotowy przyjąć ruch. W ten sposób uzyskujemy pewność, że baza danych będzie dostępna podczas podczas inicjalizacji.

Następnie mamy dodaną bazę danych ze zdefiniowanym healthcheck-iem oraz opcjonalny mały kontenerek z aplikacją pg-admin. W razie braku klienta do bazy Zitadel-a chętnych zapraszam na stronę http://localhost:5050, która umożliwia skonfigurowanie połączenia pgadmin-a z bazą. Niestety instrukcje muszę pominąć, długość tego artykułu i tak zakrawa o kategorie “dla wytrwałych”.

Po uruchomieniu komendy:

docker-compose -f path/to/file/above.yml up

Trzeba poczekać kilka minut i nasze środowisko będzie gotowe do pracy.

Konfiguracja Aplikacji Frontend

No dobra, trochę pojechałem z tytułem. Komu pociekła ślinka na smaczną konfiguracje Next.js - a będzie musiał przełknąć gorycz zawodu. Naszą aplikacją frontend będzie stary, dobry postman. Nie mniej, jeżeli chcielibyście abym zrobił artykuł jak takiego Next-a zintegrować z Zitadel to dajcie znać, chętnie coś takiego upichcę.

No dobrze, mamy infrastrukturę postawioną na nogi, teraz czas skonfigurować... samego Zitadel-a orazi pierwszą aplikację do otrzymania tokeny.

Tak więc zaczynajmy. W pierwszej kolejności, musimy wejść na adres http://localhost:8080 - to właśnie na tym hoście uruchomiliśmy aplikację przez docker-compose.

Po wejściu na stronę powinniśmy ujrzeć widok podobny do tego poniżej:

Zitadel był na tyle miły, że podczas procesu inicjalizacji utworzył pierwszego użytkownika:

username: zitadel-admin@zitadel.localhost
password: Password1!

Po wpisaniu danych logowania będziemy musieli przejść przez dwa procesy:

  1. Zresetowanie standardowego hasła dla admin-a (niestety nie da się go obejść).
  2. Konfiguracja uwierzytelnienia 2FA. Jest to krok opcjonalny, ale! O ile teraz działamy na środowisku lokalnym i polecam ominąć ten krok, to na środowisku produkcyjnym, a nawet i testowym, polecałbym skonfigurowanie 2FA.

Konfiguracja Klienta Frontendowego

Po przejściu obu procesów Zitadel powinien nas przenieść na stronę główną, gdzie znajduje się dość sporych rozmiarów przycisk "Create Application".

Po jego wciśnięciu zostaniemy przekierowani do wieloetapowego kreatora aplikacji.

Nie musimy tworzyć nowego projektu. Wystarczy, że skorzystamy z gotowego projektu o nazwie ZITADEL. Na ten moment, jako framework, polecam wybrać other, dzięki czemu przejdziesz przez cały kreator. Jest to o tyle ważne, że w tym tutorialu będziemy używać postman-a jako frontendu. W przypadku wybrania konkretnego framework-a, wybór możliwych "flow" będzie ograniczony lub całkowicie pominięty.

Następnie zostaniemy przekierowani do kreator-a:

zitadel 4
Opcja, która nas najbardziej interesuje to User Agent. Pozwoli nam to na przejście do kolejnego etapu. Możemy w nim wybrać metodę uwierzytelnienia. Zitadel oferuje dwie możliwości PKCE i Implicit. Nie mając preferencji bezpieczniejszym wyborem będzie PKCE, natomiast nigdy nie wybierajcie Implicit dla aplikacji typu SPA (więcej możecie o tym przeczytać tutaj ). Jeżeli interesuje was na czym polega flow PKCE to polecam artykuł od Auth0 opisujący cały schemat logowania.
zitadel 5
Czas na konfiguracje naszej aplikacji po stronie Identity Provider-a. To tutaj powinniśmy podać adresy na które nasza aplikacja będzie przekierowywać użytkowników po zalogowaniu lub wylogowaniu. W przypadku użycia postman-a musimy podać adres callback-a, tak, aby postman mógł przekazać dane z przeglądarki do swojej aplikacji. Jest to stały adres: https://oauth.pstmn.io/v1/callback (kiedyś zamiast callback był browser-callback i wdalszym ciągu, w niektórych tutorialach, możecie się na to natchnąć).
zitadel 6
Po przejściu formularza otrzymamy ClientId, który będziemy mogli wykorzystać w postmanie do pobrania tokenó z Zitadel-a.
zitadel 7

Konfiguracja Postman-a

Skoro konfigurację aplikacji frontend-owej mamy już zakończoną to przejdźmy do postmana, abyśmy mogli w automatyczny sposób pobrać tokeny z naszego Identity Provider-a. Na nasze szczęście nie ma tu czarnej magii. Musimy przejść do zakładki Authorization i wybrać AuthType jako OAuth 2.0. Postman wyświetli nam formularz do skonfigurowania klienta OIDC. Po ustawieniu podstawowych parametrów jak nazwa tokena, prefix header-a, poniżej znajdziemy znacznie bardziej zaawansowaną sekcję.

To co może się rzucić w oczy to konieczność ustawienia adresów URL dla endpointów służących do autoryzacji i pobierania access token-a. Skąd możemy je pobrać? Na pomoc przychodzi nam specyfikacja OIDC, która mówi, że każdy Identity Provider, który implementuje specyfikację OIDC powinien udostępniać swoją konfigurację pod adresem {HOST}/.well-known/openid-configuration czy w naszym wypadku: http://localhost:8080/.well-known/openid-configuration. Nie inaczej jest w przypadku Zitadel-a. Kiedy wejdziemy pod powyższy adres naszym oczom ukaże się poniższy widok

Dokładnie widać także, że te dwa endpoint-y znajdują się w tej odpowiedzi. No dobrze to co musimy nam zostało?

  1. Upewnić się, że Grant type jest ustawiony na Authorization Code (With PKCE);
  2. Zaznaczyć, że będziemy używali przeglądarki do autoryzacji;
  3. Ustawić ClientId na to, które otrzymaliśmy wcześniej;
  4. Ustawić poprawny scope. To jedyne problematyczne miejsce w tej konfiguracji. Oczywiście nie powinniście mieć problemów z ustawieniem takich standardowych scope-ów jak openid, email czy offline_access (dygresja: pamiętajcie, że aby z niego skorzystać to trzeba skonfigurować Refresh Token w panelu Zitadel-a.). Natomiast jest tutaj jeszcze jeden, dość niestandardowy scope urn:zitadel:iam:org:project:id:269430187052040195:aud. Jako że standardowo tokeny są limitowane do projektu zitadel-a to musimy podać ten scope w formacie: urn:zitadel:iam:org:project:id:{ClientId.Replace("@projectname", "")}:aud Teraz, wystarczy zjechać na sam dół strony w postmanie i nacisnąć duży pomarańczowy przycisk Get New Access Token. Po chwili powinna się otworzyć przeglądarka internetowa i jeżeli jesteś ciągle zalogowany do konsoli zitadel-a zostaniesz niemal natychmiast przekierowany do Redirect Url, który został ustawiony wcześniej. W przypadku braku zalogowania zitadel poprosi Cię o podanie nazwy użytkownika i hasła, a dopiero później nastąpi przekierowanie.

Po przejściu całego flow powinieneś otrzymać tokeny w postmanie. Postman sam powinien je zapisać i umożliwić Ci korzystanie z nich.

Konfiguracja Aplikacji Backend-owej

Już naprawdę niewiele pozostało do końca tego tutorialu. Swoją drogą naprawdę gratuluje cierpliwości, ponieważ jest to jeden z dłuższych artykułów na moim blogu. Nie mniej jak mamy już konfigurację dla aplikacji frontendowej, to potrzebujemy drugiej konfiguracji dla aplikacji backendowej. Sam proces nie będzie się jakoś szczególnie różnił od tego przez co przechodziliśmy dla aplikacji frontendowej. To, co jest ważne: musimy ją stworzyć w tym samym projekcie co poprzednio. Inaczej aplikacja .NET nie będzie w stanie zweryfikować tokenów poprzez introspekcję. Jak wygląda samo flow introspekcji możecie przeczytać tutaj: link

W celu ułatwienia sobie życia na następnym ekranie wybierzmy CODE jako metodę uwierzytelnienia. To pozwoli nam na wygenerowanie dwóch wartości ClientId i ClientSecret dla naszej aplikacji.

Zostaną nam zaprezentowane w podobnym popupie co ClientId po ostatnim kroku kreatora.

W między czasie napotkaliśmy jeszcze krok ustawiający Redirect Url, natomiast w przypadku tego tutoriala nie ma on większego znaczenia. Chcąc być całkowicie zgodni z specyfikacją OIDC coś powinniśmy tam wpisać.

Do Kodu!

Ostatnia część tego tutorialu, pewnie wyczekiwana przez większość czytelników, czyli to co tygryski lubią najbardziej... kodzik 🙂

Tak więc mając nasze aplikacje skonfigurowane w Zitadel musimy odpowiedzieć sobie na pytanie - w jaki sposób zintegrować aplikację .NET? Na pomoc przychodzi nam biblioteka Zitadel.NET. Oczywiście można również wykorzystać standardowe interfejsy OIDC, które są zaimplementowane w .NET... ale po co sobie utrudniać?

Będąc w terminalu, w lokacji z naszym projektem, musimy wpisać

dotnet add package Zitadel

lub dodać paczkę bezpośrednio do pliku csproj

<PackageReference Include="Zitadel" Version="6.1.2" />

Dzięki temu implementacja jest banalnie prosta.

Krok 1: Zdefiniujmy endpoint, który będzie schowany za warstwą uwierzytelnienia:

app.UseAuthentication();  
app.UseAuthorization();  
  
app.MapGet("/", async (context) =>  
{  
    if (context.User.Identity?.IsAuthenticated == true)  
    {        
	    await context.Response.WriteAsync($"Hello, {context.User.Identity.IsAuthenticated}!");  
    }    
    else  
    {  
        await context.Response.WriteAsync("Hello, Guest!");  
    }
}).RequireAuthorization();  

Pamiętaj, aby zarejestrować middlewar-y odpowiedzialne za uwierzytelnienie i autoryzację.

Krok 2: W sekcji odpowiedzialnej za konfiguracje kontenera DI, zarejestrujemy konfigurację uwierzytelnienia wykorzystującą Zitadel

builder.Services.AddAuthentication(options =>  
    {  
        options.DefaultScheme = "ZITADEL_BASIC";  
        options.DefaultChallengeScheme = "ZITADEL_BASIC";  
    }).AddZitadelIntrospection("ZITADEL_BASIC", o =>  
    {  
        o.Authority = "http://localhost:8080/";  
        o.ClientId = "269432505394855939@zitadel";  
        o.ClientSecret = "R4XBPcRUY0cd3T2hcnvUn6dxVYXRUydbYcibH8qwkRuO7IdOWZPBoBX1oTlGiREX";  
    });
builder.Services.AddAuthorization(); 

Oczywiście ClientId i ClientSecret nie powinny być zahardkodowane, w tym przypadku jest to zrobione czysto dla celów demonstracyjnych.

Gotowe! Dosłownie tyle wystarczy aby zintegrować ten CIAM z naszą aplikacją!

Proste prawda?

Ale to nie wszystko! Wraz z wykorzystaniem biblioteki Zitadel.NET otrzymujemy mega ułatwienie jeśli chodzi o lokalny development.

Bonus - Local Development

Mając “szczęście” pracować z systemem typu IAM / CIAM etc. wiesz jak bardzo może to utrudniać lokalny development.Dla nie wtajemniczonych - bardzo. Generowanie tokenów ze środowiska testowego jest narażone na błędy / downtime etc. Bardzo często spotykałem się z tym, że programiści po prostu komentowali atrybut [Authorize] na endpoincie, na którym obecnie pracowali. Od czasu do czasu napotykała nas sytuacja, w której znajdowaliśmy taki kwiatek podczas code review:

//[Authorize]
public async Task<IActionResult> SomeAction()

Ratunkiem jest biblioteka Zitadel, która pozwala na dodanie "fejkowego" uwierzytelnienia:

builder.Services.AddAuthentication(options =>  
    {  
        options.DefaultScheme = "ZITADEL_FAKE";  
        options.DefaultChallengeScheme = "ZITADEL_FAKE";  
    }).AddZitadelFake("ZITADEL_FAKE", o =>  
    {
	    o.FakeZitadelId = "1337";
	    o.AdditionalClaims = new List<Claim>() {
		    new(ClaimTypes.Name, "Kamil"),
		    new(ClaimTypes.Email, "test@test.local")
		};
	});
builder.Services.AddAuthorization();  

W takim wypadku wystarczy wysłać żądanie z nagłówkiem Authorize: Bearer 1337 abyśmy mogli się cieszyć z wykorzystania naszej fejkowej tożsamości!

Podsumowanie

Zitadel to jest naprawdę ciekawa opcja do rozważenia kiedy następnym razem będziecie potrzebowali jakiegoś systemu typu IAM / CIAM. Zitadel dostarcza bardzo fajnie zrobiony UI, jest wydajne jak i ma fajną bibliotekę dla świata .NET!

Jak zwykle mam nadzieję że się podobało!

Do Następnego! Cześć 👋

Referencje

By Bd90 | 19-06-2024 | .NET