JWT Bearer token z WordPress API w .NET

Jak zalogować aplikację konsolową w .NET do WordPressa przez JWT Authentication for WP REST API, zcache'ować token i użyć go w kolejnych żądaniach.

JAJarosław Armatys 8 min czytania
Wersja wideo tego wpisu — obejrzyj na YouTube

Skąd JWT w WordPressie

W poprzednim odcinku przygotowaliśmy WordPressa tak, żeby w ogóle dało się go odpytywać przez zewnętrzną aplikację — zainstalowaliśmy wtyczkę JWT Authentication for WP REST API, dołożyliśmy odpowiednie wpisy w wp-config.php i .htaccess, a w Postmanie sprawdziliśmy, że pod POST /wp-json/jwt-auth/v1/token faktycznie przychodzi odpowiedź z tokenem. Dzisiaj odtwarzamy ten sam flow z poziomu naszej konsolówki .NET — tak, żeby importer logował się jako zdefiniowany użytkownik WordPressa i mógł wystrzeliwać posty z ogłoszeniami jednym strzałem.

Model jest banalny: WordPress nie umie przyjąć od ciebie "goły" POST na /wp-json/wp/v2/posts bez autoryzacji, bo z automatu pozwalałoby to komukolwiek dodawać treści. Wtyczka JWT rozwiązuje to w stylu, który znasz z setek innych API — wysyłasz login i hasło na osobny endpoint, dostajesz w odpowiedzi token, a w kolejnych żądaniach przekazujesz go nagłówkiem Authorization: Bearer <token>. Serwer waliduje podpis tokena, czyta z niego ID użytkownika i traktuje zapytanie tak, jakby przyszło od zalogowanego admina.

Bearer token to w praktyce "karta wstępu na imprezę". Kto ma kartę, ten wchodzi — API nie pyta drugi raz o login i hasło, tylko weryfikuje podpis tokena. Dlatego ten token trzeba traktować jak hasło — nie commitować, nie logować, nie przekazywać dalej.

Zanim przejdziemy do kodu, jedna uwaga dla porządku. Ten sam Bearer flow, który tu pokazuję, działa praktycznie identycznie dla większości publicznych API — Spotify, Slack, GitHub, Azure DevOps. Różni się tylko adres endpointu logowania i format body. Jak nauczysz się klepać tę integrację raz, to kolejne robisz już z zamkniętymi oczami.

Pobieranie tokena

Zaczynamy od appsettings.json. Dodaję nową sekcję WordpressConfig, w której siedzą trzy wartości: Url (baza strony, bez / na końcu), Username i Password. To są dane, których absolutnie nie chcesz mieć w repozytorium — dlatego lądują w appsettings.Development.json (ignorowanym w .gitignore) albo w zmiennych środowiskowych przy deployu.

{
  "WordpressConfig": {
    "Url": "https://twoja-strona.pl",
    "Username": "importer-bot",
    "Password": "super-tajne-haslo"
  }
}

⚠️ Jeżeli logujesz się tym flow jako administrator, token daje ci pełną władzę nad WordPressem — od dodawania postów po instalowanie wtyczek. Zrób sobie osobnego użytkownika z rolą author albo editor, nadaj mu tylko tyle uprawnień, ile potrzebuje importer, i jego dane trzymaj w sekretach. Jeżeli dane admina kiedykolwiek wyciekną do publicznego repo, masz przejętą stronę tego samego dnia.

Do odwzorowania tej sekcji w kodzie używamy klasycznego pattern IOptions<T>, który omówiliśmy w odcinku o konfiguracji. Klasa WordpressOptions to zwykłe POCO z trzema propertami pasującymi nazwami do jsona. W Program.cs robimy services.Configure<WordpressOptions>(ctx.Configuration.GetSection("WordpressConfig")) i od tej chwili każdy serwis może wstrzyknąć sobie IOptions<WordpressOptions> przez konstruktor.

Samo pobranie tokena to dwa kroki. Budujemy body żądania jako obiekt z polami username i password, serializujemy do JSON-a (ważne — z małych liter, bo WordPress JWT rozpoznaje wyłącznie takie nazwy, a domyślna serializacja C# generuje PascalCase). Wysyłamy POST z nagłówkiem Content-Type: application/json. W odpowiedzi dostajemy obiekt z kilkoma polami: token, user_email, user_nicename, user_display_name. Interesuje nas tylko token.

var body = new { username = _options.Username, password = _options.Password };
var response = await _http.PostAsJsonAsync("wp-json/jwt-auth/v1/token", body);
response.EnsureSuccessStatusCode();

var auth = await response.Content.ReadFromJsonAsync<JwtResponse>();
var token = auth!.Token;

PostAsJsonAsync z System.Net.Http.Json załatwia za ciebie zarówno serializację (lowercase, dzięki domyślnej JsonSerializerOptions.Web), jak i ustawienie Content-Type. Gdybyś pisał to ręcznie przez StringContent, musiałbyś osobno przypilnować obie te rzeczy — to klasyczna pułapka, która powoduje 403 Forbidden bez sensownego komunikatu.

Przechowywanie i odświeżanie

Pierwszy odruch po zobaczeniu działającego żądania to wciągnąć to do metody i wołać za każdym razem, kiedy potrzebujesz tokena. Zły pomysł — token ważny jest przez 7 dni (domyślna konfiguracja wtyczki), a ty pobierasz go co request. Dwa problemy: niepotrzebny ruch sieciowy (a przy 55 tysiącach firm robi to różnicę) i obciążenie endpointu autoryzacji, który na WordPressie jest wolny.

Rozwiązanie: cache. W najprostszej wersji trzymasz token w prywatnym polu serwisu zarejestrowanego jako Singleton w DI, i metoda GetTokenAsync() zwraca go, jeżeli już jest, a pobiera tylko za pierwszym razem. W naszej konsolówce to wystarczy — cały proces importu mieści się w jednym uruchomieniu, które trwa kilka minut.

Jeżeli aplikacja ma chodzić długo (np. jako serwis w tle albo w webówce), zrób sobie dodatkowo walidację tokena przed każdym użyciem — wtyczka JWT ma endpoint /wp-json/jwt-auth/v1/token/validate. Jak rzuci błędem, czyścisz cache i pobierasz świeży token. To dwie linijki if, a oszczędza ci debugowania o 2 w nocy, kiedy token w cache'u przeterminował się zaraz po deployu.

Jest też opcja bardziej inżynierska — zdekodować payload JWT (middle segment tokenu, base64-encoded JSON), wyciągnąć pole exp i samemu pilnować czasu. Biblioteka System.IdentityModel.Tokens.Jwt robi to w jednej linijce. W naszym importerze byłoby to over-engineering, ale w serwisie produkcyjnym — jak najbardziej rozsądne.

Serwis IWordpressAuthService w .NET

Zamykamy całą logikę w dedykowanym serwisie. Interfejs ma jedną metodę — GetTokenAsync() — bo na zewnątrz nikogo nie obchodzi, czy token przyszedł z cache'a, czy z sieci. Wymóg dla konsumenta jest prosty: daj mi string, wrzucę go w nagłówek.

public interface IWordpressAuthService
{
    Task<string> GetTokenAsync();
}

public class WordpressAuthService : IWordpressAuthService
{
    private readonly HttpClient _http;
    private readonly WordpressOptions _options;
    private string? _cachedToken;

    public WordpressAuthService(HttpClient http, IOptions<WordpressOptions> options)
    {
        _http = http;
        _options = options.Value;
    }

    public async Task<string> GetTokenAsync()
    {
        if (!string.IsNullOrEmpty(_cachedToken)) return _cachedToken;

        var body = new { username = _options.Username, password = _options.Password };
        var response = await _http.PostAsJsonAsync("wp-json/jwt-auth/v1/token", body);
        response.EnsureSuccessStatusCode();

        var auth = await response.Content.ReadFromJsonAsync<JwtResponse>();
        _cachedToken = auth!.Token;
        return _cachedToken;
    }
}

public class JwtResponse
{
    [JsonPropertyName("token")]
    public string Token { get; set; } = default!;
}

Rejestracja w Program.cs leci przez AddHttpClient — tą samą drogą, którą ustawialiśmy klienta dla Google Places API:

builder.Services.AddHttpClient<IWordpressAuthService, WordpressAuthService>((sp, client) =>
{
    var options = sp.GetRequiredService<IOptions<WordpressOptions>>().Value;
    client.BaseAddress = new Uri(options.Url);
});

Dzięki temu HttpClient dostaje już ustawiony BaseAddress (czyli w serwisie możemy pisać relatywne ścieżki — wp-json/jwt-auth/v1/token zamiast pełnego URL-a), a DI dba o prawidłowy lifecycle klienta — bez pułapek z SocketException przy długo żyjących instancjach.

Gdy zawołasz GetTokenAsync() pierwszy raz, serwis pobierze token i zapamięta. Każde kolejne wywołanie to zwykły return z pola — zero ruchu sieciowego. W odcinku ósmym pokażę drugą połowę tego flow — jak zbudować WordpressPostService, który weźmie ten token, dołoży go nagłówkiem Authorization: Bearer <token> i wystrzeli POST /wp-json/wp/v2/posts z ogłoszeniem firmy. Po tym kroku masz komplet — aplikacja loguje się, autoryzuje i dodaje treści do strony bez dotykania WordPress admin-panela.

Co dalej?

W kolejnym odcinku bierzemy się za dodawanie samych ogłoszeń — POST do WordPress REST API z użyciem tokenu, który właśnie nauczyliśmy się pobierać. Pokażę, jak wrzucić tytuł, treść, status (publish vs draft), kategorie i niestandardowe pola — bo standardowe WordPressowe pola wystarczają dopiero po dorzuceniu Advanced Custom Fields do całości.

Jeżeli chcesz zobaczyć kod, który dzisiaj pisaliśmy, cała seria żyje na GitHubie: github.com/jarmatys/WPImporter. Najlepszy moment na klon — kolejne odcinki dodają coraz więcej ruchomych części i łatwiej się w nich pogubić, jeżeli nie masz działającego startu.

Masz pytanie o JWT, o bezpieczne przechowywanie haseł w .NET albo o to, jak ogarnąć refresh tokenów w dłużej żyjącej aplikacji? Zostaw komentarz pod filmem — temat autoryzacji wraca non-stop w komercyjnych projektach, chętnie rozwinę każdy wątek, który cię interesuje.

Do zobaczenia w odcinku ósmym. Trzymaj się.

Tagi:#course-dotnet-wordpress#wordpress#jwt#authentication#httpclient

Masz pytanie do tego wpisu?

Napisz do mnie — chętnie porozmawiam o szczegółach technicznych, albo o tym jak zastosować to u Ciebie w projekcie.