Bezpieczne API w praktyce: jak wdrożyć OAuth2 i OpenID Connect

1
21
2/5 - (1 vote)

Nawigacja:

Dlaczego samo API key to za mało

API key, Basic Auth, „gołe” JWT – szybkie porównanie

API key, Basic Auth i własne JWT kuszą prostotą. Sprawdzają się w małych, wewnętrznych projektach, ale skala i wymagania bezpieczeństwa szybko ujawniają ich ograniczenia.

API key to zazwyczaj losowy ciąg znaków przekazywany w nagłówku lub parametrze zapytania. Serwer porównuje go z wpisem w bazie i tyle. Brakuje tu rozróżnienia użytkownika od aplikacji, granularnych uprawnień, wygodnego mechanizmu wygaszania kluczy i audytu.

Basic Auth to login i hasło zakodowane w Base64. W praktyce jest to stały sekret przesyłany w każdym żądaniu. Przy braku TLS jest to jawna katastrofa, a nawet z TLS-em dochodzi ryzyko wycieków w logach, przeglądarce, narzędziach debugujących.

JWT bez standardu (własne „tokeny” JWT) daje pozory nowoczesności. Jeśli jednak nie jest oparte na spójnym protokole (OAuth2/OIDC), kończy się zlepkiem własnych claimów, nieprzewidywalnymi czasami życia i masą specjalnych przypadków w integracjach z zewnętrznymi systemami.

Główne zagrożenia prostych mechanizmów

W prostych rozwiązaniach uwierzytelniania najczęściej pojawiają się trzy problemy: brak kontroli zakresu uprawnień, trudne odwoływanie dostępu oraz brak audytu.

Stały wyciek klucza lub hasła oznacza pełny dostęp aż do momentu ręcznego unieważnienia. Nie ma możliwości ograniczenia uprawnień do jednego modułu czy funkcji – albo wszystko, albo nic.

Brak kontroli uprawnień utrudnia wdrożenie zasad least privilege. Jeśli każdy klucz ma pełny dostęp do API, każdy jego posiadacz jest potencjalnie krytycznym zagrożeniem. Scope’y i role z OAuth2/OIDC rozwiązują ten problem precyzyjniej.

Brak audytu uniemożliwia odpowiedź na pytania: kto wywołał dany endpoint, z jakiej aplikacji, w czyim imieniu. To blokuje reagowanie na incydenty i analizę nadużyć.

Przykład z praktyki: rosnąca liczba integracji

Typowy scenariusz: mała firma tworzy API na użytek jednej aplikacji frontendowej. Wystarcza prosty API key lub Basic Auth. Z czasem dochodzą:

  • druga aplikacja webowa,
  • aplikacja mobilna,
  • klient B2B chcący integrować się bezpośrednio z API,
  • skrypty automatyzujące (cron, integracje M2M).

W tym momencie w obiegu pojawia się kilkanaście lub kilkadziesiąt kluczy API, generowanych „na żądanie”, przechowywanych w różnych miejscach, często bez wygasania. Zmiana uprawnień jednego partnera wymaga ręcznych zmian, kasowania kluczy, testów regresyjnych.

OAuth2 i OpenID Connect porządkują tę przestrzeń. Rozdzielają tożsamość użytkownika od tożsamości aplikacji klienckiej, wprowadzają standardowy mechanizm scope’ów i uprawnień oraz możliwość audytu i centralnego zarządzania dostępem.

Kiedy trzeba przejść na OAuth2/OIDC

Prosty mechanizm kluczy można utrzymać w małych, wewnętrznych systemach. Pojawiają się jednak granice, po których przekroczeniu migracja do OAuth2/OIDC staje się koniecznością:

  • wdrożenie regulacji (np. RODO, wymogi bezpieczeństwa klientów korporacyjnych),
  • udostępnienie API podmiotom zewnętrznym,
  • wsparcie dla aplikacji mobilnych i SPA, gdzie nie można bezpiecznie przechowywać sekretów,
  • potrzeba SSO między kilkoma aplikacjami i systemami,
  • wymagania audytowe: kto, kiedy, z której aplikacji wywołał dany endpoint.

W tym punkcie secure by default API przestaje być luksusem, a staje się warunkiem prowadzenia biznesu. OAuth2 z OpenID Connect są obecnie jedynym szeroko akceptowanym standardem w tej roli.

Podstawy OAuth2 – role, pojęcia, przepływy

Role w OAuth2: kto jest kim

OAuth2 wprowadza cztery podstawowe role. Zrozumienie ich upraszcza projektowanie całej architektury.

Resource Owner – posiadacz zasobu, najczęściej użytkownik końcowy (człowiek), rzadziej konto systemowe. To on udziela zgody na dostęp aplikacji do swoich danych.

Client – aplikacja, która chce uzyskać dostęp do zasobu w imieniu użytkownika lub sama jako aplikacja (M2M). Może to być SPA, aplikacja mobilna, backend webowy, mikroserwis.

Authorization Server – serwer autoryzacji (IdP), który odpowiada za logowanie, wydawanie tokenów, walidację zgód. Przykłady: Keycloak, Auth0, Azure AD, Okta.

Resource Server – serwer API, który udostępnia zasoby chronione. Weryfikuje access tokeny i decyduje, czy przyznać dostęp do konkretnego endpointu.

Kluczowe pojęcia: grant, access token, refresh token, scope

OAuth2 opiera się na pojęciu authorization grant. To „dowód”, że użytkownik (resource owner) wyraził zgodę, aby dana aplikacja (client) mogła uzyskać dostęp. Najczęściej przyjmuje postać authorization code, który client wymienia na tokeny.

Access token to krótkotrwały token używany do wywoływania API. Zawiera informacje o tym, do czego i w jakim zakresie uprawnia posiadacza. Może mieć postać JWT lub nieprzezroczystego ciągu (opaque token).

Refresh token służy do uzyskiwania nowych access tokenów bez ponownego logowania użytkownika. Jest wrażliwym sekretem, który musi być przechowywany bardzo ostrożnie, najlepiej tylko po stronie backendu lub w bezpiecznym magazynie w aplikacji natywnej.

Scope definiuje zakres uprawnień. Przykłady: read:orders, write:orders, admin, openid profile email. API decyduje, który scope jest wymagany do konkretnego działania.

Przegląd przepływów OAuth2

OAuth2 definiuje kilka tzw. flows (grant types). Dla praktyka kluczowe są cztery:

  • Authorization Code – klasyczny przepływ z użyciem authorization code, rekomendowany w połączeniu z PKCE.
  • Authorization Code + PKCE – wariant z dodatkowymi zabezpieczeniami dla SPA i aplikacji mobilnych.
  • Client Credentials – przepływ dla komunikacji machine-to-machine, bez użytkownika.
  • Device Code – dla urządzeń bez pełnoprawnej przeglądarki (TV, IoT).

Stare przepływy, takie jak Implicit Flow, nie powinny być już używane. Zostały wypchnięte przez Authorization Code z PKCE, który spełnia te same cele znacznie bezpieczniej.

Dobór flow do typu aplikacji

Dobór właściwego przepływu to jedna z kluczowych decyzji projektowych. Można to uprościć do kilku reguł.

  • SPA (Single Page Application) – Authorization Code + PKCE, bez client secret po stronie przeglądarki.
  • Aplikacje mobilne – Authorization Code + PKCE, z wykorzystaniem biblioteki AppAuth lub podobnej; bez sekretnych kluczy w kodzie.
  • Aplikacje backendowe (server-side) – Authorization Code (z lub bez PKCE), client secret przechowywany na serwerze.
  • M2M / mikroserwisy – Client Credentials, z odpowiednio ograniczonymi scope’ami.

Device Code przydaje się głównie przy urządzeniach bez wygodnego interfejsu – bardziej niszowy, ale czasem niezbędny.

Podstawy OpenID Connect – to, czego brakuje w OAuth2

Różnica między autoryzacją a uwierzytelnianiem

OAuth2 sam w sobie rozwiązuje autoryzację – czyli przyznanie aplikacji dostępu do zasobu. Nie definiuje jednak, jak użytkownik się loguje ani jakie dane o nim są przekazywane do klienta.

OpenID Connect (OIDC) rozszerza OAuth2 o uwierzytelnianie. Umożliwia bezpieczne potwierdzenie tożsamości użytkownika i przekazanie aplikacji informacji o nim w standaryzowany sposób (przez ID Token i endpointy użytkownika).

Bez OIDC każda aplikacja musiałaby interpretować access token „po swojemu” lub opierać się na niestandardowych endpointach. OIDC wprowadza porządek, który ułatwia integrację i wymianę dostawcy tożsamości (IdP).

ID Token vs Access Token – co gdzie stosować

ID Token to JWT wydawany przez serwer OIDC, reprezentujący fakt uwierzytelnienia użytkownika. Zawiera claimy takie jak sub (identyfikator użytkownika), email, name, auth_time. ID Token jest przeznaczony dla klienta (aplikacji), a nie dla API.

Access Token służy do wywoływania API (Resource Server). API nie powinno polegać na ID Tokenie jako dowodzie autoryzacji. ID Token może w ogóle nie być wysyłany do API.

Mieszanie tych ról to częsty błąd. API powinno interpretować wyłącznie access token, podczas gdy frontend/backend aplikacji klienckiej może używać ID Tokena do wyświetlania informacji o zalogowanym użytkowniku i utrzymywania sesji.

Standardowe claims i mechanizm discovery

OIDC definiuje standardowe claims, dzięki którym aplikacje integrują się z różnymi IdP w podobny sposób. Najważniejsze to:

  • sub – stabilny identyfikator użytkownika w danym issuerze,
  • iss – adres issuer’a (serwera OIDC),
  • aud – audiencja (dla kogo przeznaczony jest token),
  • email, email_verified, name, family_name, given_name,
  • iat, exp, auth_time, nonce.

Mechanizm discovery opiera się na endpointcie /.well-known/openid-configuration. Pod standardowym URL-em (np. https://idp.example.com/.well-known/openid-configuration) klient znajdzie adresy wszystkich kluczowych endpointów: /authorize, /token, /jwks, /userinfo, a także obsługiwane flow, granty, algorytmy podpisu.

To pozwala dynamicznie konfigurować klientów i znacznie upraszcza integracje, szczególnie z dużymi dostawcami (Auth0, Azure AD, Google).

Single Sign-On i Single Logout

OpenID Connect umożliwia Single Sign-On (SSO). Użytkownik loguje się raz do IdP, po czym może uzyskiwać tokeny dla wielu aplikacji bez ponownego podawania hasła. Klient po prostu przekierowuje przeglądarkę na /authorize, a IdP rozpoznaje istniejącą sesję.

Single Logout (SLO) to trudniejszy element. OIDC definiuje mechanizmy front-channel i back-channel logout, ale implementacje są różne. W praktyce oznacza to:

  • wylogowanie użytkownika z IdP (sesja centralna),
  • wyczyszczenie lokalnych sesji w aplikacjach klienckich (cookies, pamięć tokenów),
  • opcjonalne wywołanie endpointów logout w backendach.

Spójne SSO/SLO wymaga przemyślanej integracji: gdzie trzymana jest sesja, czy klient ufa wyłącznie tokenom, czy także własnym cookies, jak wygląda propagacja wylogowania.

Projekt architektury: gdzie wpiąć OAuth2/OIDC w istniejące API

Rozdzielenie Authorization Server i Resource Server

Kluczowa zasada: serwer autoryzacji nie jest serwerem API. To dwa różne komponenty, często działające na innych domenach i pod innym zarządzaniem.

Authorization Server (IdP) zarządza logowaniem użytkowników, zgodami, tokenami. Może być wewnętrzny (Keycloak, własne wdrożenie) albo zewnętrzny (Auth0, Azure AD, Okta).

Resource Server (API) przyjmuje wyłącznie access tokeny, weryfikuje ich ważność i decyduje o dostępie. Nie powinien posiadać własnego mechanizmu logowania użytkownika – cała tożsamość przychodzi z zewnątrz przez token.

Taki podział pozwala centralnie zarządzać tożsamością i autoryzacją dla wielu API, jednocześnie utrzymując prostotę i niezależność samego API.

Centralny IdP a lokalne loginy – strategia migracji

W starszych systemach API często ma własną tabelę users, logowanie i sesje. Migracja na IdP i OIDC zwykle przebiega etapami:

  1. Wprowadzenie IdP jako źródła tożsamości dla nowych aplikacji (np. nowy panel admina).
  2. Mapowanie użytkowników – tworzenie powiązania między lokalnym user_id a sub z IdP.
  3. Stopniowe przenoszenie logowania – istniejący użytkownicy przy pierwszym logowaniu przez IdP są „linkowani” do konta lokalnego.
  4. Wyłączanie lokalnego logowania – po przejściu większości użytkowników dostęp zostaje tylko przez IdP.

Warstwa pośrednia: API Gateway lub reverse proxy

Często najprościej jest wpiąć OAuth2/OIDC w istniejący system na warstwie pośredniej, przed samym API.

API Gateway / reverse proxy (np. NGINX, Kong, Traefik, Envoy, Apigee) może przejąć obowiązki:

  • walidacji tokenów (podpis, iss, aud, data ważności),
  • odrzucania żądań bez odpowiednich scope’ów/claimów,
  • przekazywania do API tylko zaufanych nagłówków (np. X-User-Id, X-Scopes).

Backend dostaje już „oczyszczone” żądanie i nie musi znać szczegółów JWT. Wystarczy mu identyfikator użytkownika oraz lista uprawnień zaufana przez gateway.

To podejście jest wygodne przy stopniowej migracji – stare API może zachować swój model autoryzacji, a gateway będzie tłumaczył tokeny na lokalne role lub identyfikatory.

Mapowanie ról i uprawnień z IdP do domeny biznesowej

IdP zwykle operuje na scope’ach i prostych rolach (np. admin, user). API ma często bardziej rozbudowany model uprawnień: poziomy dostępu, działy, projekty, klienta B2B.

Przydatny jest prosty schemat:

  1. IdP wystawia claimy o wysokim poziomie ogólności: role globalne, organizacja, tenant.
  2. API lub gateway tłumaczy je na lokalne uprawnienia domenowe: dostęp do konkretnych zasobów, flagi funkcji.

Przykład: claim role=partner_manager z IdP mapuje się w API na możliwość odczytu i edycji klientów z danej organizacji. Sama logika ograniczenia do organizacji nadal siedzi w bazie i kodzie API, a nie w IdP.

Wielu dostawców tożsamości a jedno API

W systemach B2B pojawia się często potrzeba obsługi wielu IdP (np. własny Keycloak plus Azure AD klientów).

API z perspektywy bezpieczeństwa ma dwa zadania:

  • sprawdzić, czy token pochodzi od zaufanego issuera (lista dopuszczonych iss),
  • upewnić się, że audiencja i scope’y są zgodne z jego kontraktem.

Dalsze różnice (np. inne formaty grup, claimów) można wygładzić na poziomie tzw. broker’a tożsamości (centralny IdP, który „wchłania” innych) lub w gatewayu, który normalizuje tokeny / nagłówki do formatu zrozumiałego dla API.

Kursor myszy na ekranie z napisem o cyfrowym bezpieczeństwie API
Źródło: Pexels | Autor: Pixabay

Konfiguracja serwera autoryzacji (na przykładzie gotowego IdP)

Wybór IdP: self-hosted czy SaaS

Najpierw decyzja: gotowe SaaS (Auth0, Okta, Azure AD B2C, Cognito) czy self-hosted (Keycloak, Authentik, Ory Hydra + Kratos).

Praktyczne kryteria:

  • czy potrzebny jest SSO z istniejącym AD/LDAP,
  • jak ważna jest kontrola on-prem i brak danych w chmurze zewnętrznej,
  • czy zespół ma zasoby, by utrzymywać własny IdP (backup, HA, aktualizacje bezpieczeństwa).

Na start często łatwiej użyć SaaS, a przy większej skali lub wymaganiach prawnych przenieść się na własny IdP.

Podstawowa konfiguracja realm/tenant i klientów

W typowym IdP konfiguracja zaczyna się od utworzenia reiama/tenanta, czyli logicznej przestrzeni użytkowników i aplikacji.

W ramach tego kontekstu definiuje się:

  • klientów (applications) – każda aplikacja, która będzie używać OAuth2/OIDC,
  • typ klienta: public (SPA, mobilka) lub confidential (backend, M2M),
  • redirect URI – dozwolone adresy, na które IdP może przekierować użytkownika z kodem/autoryzacją,
  • grant types – Authorization Code, Client Credentials, Device Code itd.

Błąd, który pojawia się bardzo często: zbyt szerokie wzorce redirect URI, np. https://*.example.com/*. Lepiej wskazać kilka konkretnych adresów, niż otworzyć furtkę pod ataki z użyciem przechwycenia kodu autoryzacyjnego.

Konfiguracja scope’ów i claims

IdP pozwala zdefiniować scope’y i powiązane z nimi claims.

Typowe grupy:

  • scope’y OIDC: openid, profile, email,
  • scope’y API: np. orders.read, orders.write, admin,
  • scope’y organizacyjne: tenant:123, project:abc.

IdP można skonfigurować tak, by konkretne scope’y dodawały określone claimy w access tokenie lub ID Tokenie. Przykład: żądanie scope’a profile powoduje dodanie name, family_name, given_name do ID Tokena.

Przy API dobrze, aby scope’y miały bezpośrednie przełożenie na operacje. Łatwiej wtedy utrzymać spójność między polityką IdP a kodem uprawnień w serwisie.

Ustawienia PKCE, rotating refresh tokens, polityki sesji

Większość nowoczesnych IdP ma opcje wymuszenia bezpiecznych praktyk.

  • PKCE required – obowiązkowe stosowanie PKCE dla klientów publicznych. Chroni przed atakami na kody autoryzacyjne.
  • Rotating refresh tokens – każdy użyty refresh token jest unieważniany i wymieniany na nowy. Utrudnia nadużycie wykradzionych tokenów.
  • Maximum session age i Idle timeout – kontrola, jak długo sesja w IdP pozostaje ważna, zanim użytkownik będzie musiał się przelogować.
  • Token lifetimes – krótkie czasy życia access tokenów, dłuższe refresh tokenów.

Dobry kompromis dla większości systemów: access token 5–15 minut, refresh token od kilku godzin do kilku dni, zależnie od wrażliwości danych.

Implementacja klienta – Authorization Code + PKCE krok po kroku

Generowanie code verifier i code challenge

PKCE opiera się na dwóch elementach: code verifier (sekret znany tylko klientowi) i code challenge (jego skrót przekazywany do IdP).

W pseudo-kodzie:

// 1. Losowy verifier
verifier = base64url(randomBytes(32))

// 2. Challenge = SHA-256(verifier)
challenge = base64url(SHA256(verifier))

W przeglądarce verifier można trzymać w pamięci (np. w zmiennej JS lub sessionStorage) na czas trwania flow. Nie zapisuj go w localStorage na stałe, nie wysyłaj na serwer.

Przekierowanie użytkownika do /authorize

Kolejny krok to wysłanie użytkownika na endpoint /authorize IdP z odpowiednimi parametrami.

GET https://idp.example.com/oauth2/authorize?
  response_type=code&
  client_id=spa-client&
  redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
  scope=openid%20profile%20orders.read&
  code_challenge={challenge}&
  code_challenge_method=S256&
  state={losowy_csrf_token}&
  nonce={losowy_nonce_dla_oidc}

state chroni przed atakami CSRF – przy przekierowaniu z powrotem należy sprawdzić, że jest tym samym, który wysłano. nonce jest używany do związania ID Tokenu z konkretną próbą logowania.

Obsługa callbacku i wymiana code na tokeny

Po zalogowaniu użytkownika IdP przekieruje przeglądarkę z powrotem na redirect_uri z parametrami code i state.

  1. Odbierz code i state z adresu URL.
  2. Zweryfikuj, że state zgadza się z tym zapisanym przed przekierowaniem.
  3. Wyślij code do endpointu /token razem z code_verifier.

Przykładowe żądanie:

POST https://idp.example.com/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code={code_z_callbacku}&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
client_id=spa-client&
code_verifier={oryginalny_verifier}

IdP zwróci access token, opcjonalnie refresh token i ID Token. SPA powinna przechować access token w pamięci (np. w zmiennej aplikacji lub w sessionStorage, zależnie od modelu zagrożeń).

Gdzie przechowywać tokeny po stronie klienta

W SPA dostępne są trzy główne opcje:

  • pamięć w JS (np. w stanie aplikacji, Redux, Vuex) – brak przetrwania odświeżenia strony, ale minimalizuje ryzyko XSS przechwycenia tokenu w długim okresie,
  • sessionStorage – tokeny wygasają po zamknięciu karty, nadal narażone na XSS,
  • localStorage – wygodne, ale najwyższe ryzyko w razie XSS; bez dodatkowych zabezpieczeń lepiej unikać.

Dla aplikacji silnie narażonych na XSS dobrym wzorcem jest przeniesienie tokenów do backendu: SPA komunikuje się z własnym serwerem, a ten trzyma access/refresh tokeny w bezpiecznym magazynie, a do przeglądarki wysyła jedynie cookies HTTP-only identyfikujące sesję.

Obsługa błędów i anulowania

IdP w przypadku błędu (odmowa zgody, błąd konfiguracji) przekieruje na redirect_uri z parametrami error i error_description.

Klient powinien:

  • wyświetlić komunikat użytkownikowi wprost lub w formie przyjaznego błędu,
  • usunąć lokalne wartości state, code_verifier, aby nie zostawały „wiszące” sesje.

Jeśli użytkownik anuluje logowanie po stronie IdP, wynik będzie podobny – callback z błędem. Scenariusz anulowania warto pokryć testami E2E, bo łatwo go przeoczyć.

Weryfikacja i obsługa tokenów w API (Resource Server)

Pobieranie kluczy publicznych z JWKS

Jeśli access token jest JWT, podpis jest weryfikowany na podstawie kluczy publicznych IdP, dostępnych zwykle pod adresem z discovery: jwks_uri.

API lub gateway może:

  1. cyklicznie pobierać JWKS (JSON Web Key Set) i trzymać w cache,
  2. dla każdego tokenu odczytać kid z nagłówka JWT i dobrać odpowiedni klucz z JWKS,
  3. zweryfikować podpis i algorytm (np. RS256, ES256).

Brak weryfikacji algorytmu lub akceptowanie HS256 przy Public JWK to klasyczny wektor ataku. W konfiguracji biblioteki JWT trzeba jawnie wskazać dopuszczalne algorytmy.

Weryfikacja podstawowych claimów

Po podpisie trzeba sprawdzić semantykę tokenu.

  • iss – powinien pasować dokładnie do zaufanego issuer’a (np. https://idp.example.com/).
  • aud – musi zawierać identyfikator API (np. api://orders-service); nie wolno przyjmować tokenów wystawionych dla innych audiencji.
  • exp – access token po czasie wygaśnięcia jest bezwarunkowo odrzucany.
  • nbf i iat – można sprawdzać z uwzględnieniem niewielkiego „skew” czasowego.

Dodatkowo API może wymagać konkretnego scope’a lub roli, np. orders.read dla endpointu GET /orders. Lepiej mieć prostą funkcję pomocniczą, która sprawdza wymagane scope’y na poziomie middleware, niż rozrzucać te warunki po całym kodzie kontrolerów.

Opaque tokeny i introspection endpoint

Jeśli access token jest nieprzezroczystym ciągiem (opaque token), API nie zweryfikuje go samodzielnie. Potrzebny jest endpoint introspekcji (/introspect) w IdP.

Scenariusz:

  1. API otrzymuje token w nagłówku Authorization: Bearer {token}.
  2. Wysyła żądanie do IdP, uwierzytelniając się jako klient confidential (client credentials).
  3. Otrzymuje odpowiedź z informacją, czy token jest aktywny i jakie ma scope’y, identyfikator użytkownika itd.

Introspekcja zwiększa obciążenie IdP, ale ułatwia natychmiastowe unieważnianie tokenów (np. po zablokowaniu konta). Przy dużym ruchu można cache’ować odpowiedzi introspekcji przez kilka–kilkanaście sekund, o ile model bezpieczeństwa na to pozwala.

Propagacja tożsamości do logów i dalszych usług

API po zweryfikowaniu tokenu powinno znormalizować dane o użytkowniku do kilku nagłówków / pól wewnętrznych, np.:

  • X-User-Id – wartość claimu sub lub lokalnego identyfikatora użytkownika,
  • X-User-Scopes – lista scope’ów,
  • Spójne konteksty bezpieczeństwa w mikrousługach

    Przy wielu serwisach łatwo doprowadzić do chaosu: każdy mikroserwis inaczej interpretuje scope’y i role. To zaproszenie do luk uprawnień.

    Praktyczny wzorzec:

  • jedno miejsce definicji polityk – np. repozytorium „security-contracts” zawierające listę scope’ów, ról i mapowanie na operacje,
  • wspólna biblioteka dla serwisów, która:
    • parsuje token,
    • udostępnia gotowe helpery typu requireScope("orders.read"),
    • standaryzuje nazwy nagłówków korelacyjnych i identyfikatorów użytkownika.

Bez tego kończy się na if-ach rozrzuconych po kodzie i różnej interpretacji tego samego claimu w zależności od zespołu.

Delegacja uprawnień między usługami

Jeśli API A woła API B w imieniu użytkownika, trzeba zdecydować, co propagować dalej.

Popularne opcje:

  • Token użytkownika do downstream – najprostsze, ale B musi ufać temu samemu IdP i rozumieć scope’y. Dobre w jednym domenowym systemie.
  • Token serwisu z atrybutami użytkownika – A wymienia token użytkownika na token techniczny z ograniczonym zestawem claimów (pattern „token exchange”). Pozwala na ostrzejsze granice między domenami.

Drugi wariant bywa wygodniejszy przy integracjach międzyorganizacyjnych: serwis B nie musi znać pełnej struktury tożsamości zewnętrznego IdP, wystarczy mu minimalny zestaw pól przekazanych przez A.

Audyt i śledzenie żądań z Token ID

Logi bez powiązania z użytkownikiem i tokenem są mało użyteczne przy inspekcjach bezpieczeństwa.

Dobry zestaw pól w logach API:

  • user_id – z sub lub zmapowany identyfikator lokalny,
  • client_id – z claimu azp / client_id,
  • scopes – lista scope’ów,
  • session_id lub sid, jeśli IdP go wystawia,
  • trace_id – wspólny identyfikator korelacyjny dla całego łańcucha wywołań.

Przy incydencie typu „nieautoryzowany dostęp do zamówień” można po takim zestawie szybko zrekonstruować, kto, z jakiej aplikacji i w jakim kontekście wysłał żądanie.

Bezpieczne zarządzanie refresh tokenami i sesjami

Charakterystyka refresh tokenów

Refresh token ma długie życie i wysoki wpływ – jego przejęcie często równa się trwałemu przejęciu konta, dopóki nie zostanie unieważniony.

Z tego powodu:

  • traktuj refresh token jak hasło – nie pokazuj, nie loguj, nie przesyłaj między usługami bez powodu,
  • trzymaj go wyłącznie tam, gdzie możesz zastosować twarde zabezpieczenia (serwer, secure storage w mobilce, HTTP-only cookies).

Rotacja refresh tokenów

Rotacja zmniejsza skutki wycieku: użyty token jest wymieniany na nowy i stary traci ważność.

Model działania:

  1. Klient wysyła refresh token do IdP.
  2. IdP wydaje nowy access token i nowy refresh token, oznaczając stary jako zużyty.
  3. Próba użycia starego tokenu sygnalizuje potencjalny atak (concurrent usage).

Przy rotacji trzeba obsłużyć konflikt: jeśli aplikacja wyśle zużyty już token (np. z równoległej karty przeglądarki), powinna wymusić ponowne logowanie zamiast ślepo powtarzać żądanie.

Przechowywanie refresh tokenów po stronie backendu

W aplikacjach webowych bezpieczniej jest nie trzymać refresh tokenu w przeglądarce.

Wzorzec:

  • przeglądarka ma tylko ciasteczko sesyjne HTTP-only do własnego backendu,
  • backend przechowuje refresh token w swoim store (szyfrowana baza, HSM, KMS),
  • odświeżanie access tokenów odbywa się po stronie serwera, poza zasięgiem JS i XSS.

Przy takim podejściu przejęcie sesji wymaga naruszenia serwera lub kradzieży ciasteczka (co można ograniczyć m.in. przez SameSite, TLS, krótkie lifetimy).

Revocation endpoint i globalne wylogowanie

RFC 7009 definiuje endpoint revocation, który pozwala klientowi unieważnić własne tokeny.

Typowe zastosowania:

  • użytkownik klika „Wyloguj ze wszystkich urządzeń” – backend iteruje po znanych refresh tokenach i wysyła revoke,
  • panel administracyjny blokuje konto – system IAM oznacza konto jako nieaktywne, a job lub webhook wywołuje revocation dla aktywnych tokenów.

Przy JWT access tokenach „globalne wylogowanie” jest zawsze przybliżone: token pozostaje ważny do exp, chyba że użyjesz dodatkowego mechanizmu (lista odwołanych tokenów, introspekcja lub logout session index w bazie).

Modele sesji: IdP, aplikacja, przeglądarka

Logowanie użytkownika to w praktyce trzy sesje:

  • sesja w IdP (cookie logowania w domenie IdP),
  • sesja w aplikacji (sesja backendu lub „sesja” tokenowa),
  • sesja przeglądarki (zakres kart/okien, storage).

Warto ustalić jasną strategię:

  • co się dzieje po wylogowaniu z aplikacji – czy kończy też sesję w IdP, czy tylko lokalną,
  • czy zamknięcie przeglądarki kończy sesję (session cookies) czy nie (persistent cookies),
  • jak zachowuje się scenariusz wielu kart z tą samą aplikacją przy rotacji refresh tokenów.

Dobrą praktyką jest rozróżnienie: „Wyloguj z tej aplikacji” (kasuje tylko lokalną sesję) vs „Wyloguj ze wszystkich usług” (End Session w IdP).

Back-channel i front-channel logout

OpenID Connect definiuje mechanizmy wylogowania rozproszonego.

  • Front-channel logout – IdP ładuje w przeglądarce iframe’y / redirecty do aplikacji klienckich, sygnalizując im zakończenie sesji. Proste, ale zależne od przeglądarki.
  • Back-channel logout – IdP wysyła podpisaną wiadomość do endpointu backendowego klienta, ten unieważnia sesję po swojej stronie. Nie wymaga interakcji użytkownika ani przeglądarki.

W systemach o wyższym profilu ryzyka lepiej wdrożyć back-channel: mniej ruchomych części po stronie frontendu, mniejsze ryzyko, że klient przeoczy sygnał wylogowania.

Typowe scenariusze integracji – backend, SPA, mobilka, M2M

Klasyczna aplikacja backend-rendered (SSR)

Dla aplikacji serwujących HTML z serwera sensownym wyborem jest OIDC + cookies.

Przebieg:

  1. Użytkownik wchodzi na stronę, aplikacja wykrywa brak sesji.
  2. Przekierowanie do IdP (Authorization Code + PKCE lub bez PKCE, jeśli klient confidential).
  3. Po powrocie aplikacja wymienia code na tokeny i zapisuje identyfikator sesji w cookie HTTP-only.
  4. Access/refresh tokeny są przechowywane tylko po stronie serwera i używane do wołania downstream API.

Zaletą jest ograniczenie ekspozycji tokenów do warstwy serwerowej oraz prostsza ochrona przed XSS – kompromitacja frontu nie daje od razu dostępu do tokenów.

SPA w przeglądarce – public client

Nowoczesne SPA działają jako public clients: nie mają bezpiecznego miejsca na sekret, komunikują się bezpośrednio z IdP.

Bezpieczny wariant:

  • Authorization Code + PKCE,
  • brak implicit/hybrid flow,
  • tokeny trzymane w pamięci lub ewentualnie w sessionStorage,
  • krótkie lifetimy access tokenów, ostrożne użycie refresh tokenów (jeśli IdP w ogóle je dopuszcza do SPA).

W środowiskach z wysokim ryzykiem XSS zasadne jest dołożenie backendu dla SPA (tzw. BFF – Backend For Frontend) i przeniesienie całego zarządzania tokenami na serwer.

Backend For Frontend (BFF) dla SPA

BFF pośredniczy między przeglądarką, IdP i API biznesowymi.

Kluczowe elementy:

  • przeglądarka ma tylko ciasteczko sesyjne do BFF,
  • BFF realizuje cały OAuth2/OIDC z IdP jako confidential client,
  • komunikacja BFF → API z wykorzystaniem access tokenów, przechowywanych w serwerowym store,
  • CSRF dla żądań stanowiących zmianę (token synchronizowany w nagłówku vs cookie).

To podejście łączy wygodę SPA z profilem bezpieczeństwa zbliżonym do klasycznych aplikacji serwerowych.

Aplikacje mobilne – natywne

Mobilka jest również public clientem, ale ma lepsze opcje przechowywania sekretów niż przeglądarka.

Bezpieczna konfiguracja zwykle obejmuje:

  • Authorization Code + PKCE,
  • custom URI schemes lub App Links/Universal Links do obsługi redirect_uri,
  • przechowywanie refresh tokena w Keychain / Keystore lub odpowiedniku,
  • dodatkowe powiązanie aplikacji z IdP (np. OAuth 2.0 for Native Apps, App Attest/DeviceCheck na iOS, SafetyNet/Play Integrity na Androidzie – o ile IdP to wspiera).

Trzeba uważać na złośliwe aplikacje podszywające się pod te same schematy URI. App Links/Universal Links rozwiązują ten problem lepiej niż własne schematy.

Integracje serwer–serwer (M2M)

Usługi techniczne najczęściej używają Client Credentials Grant.

Wzorzec:

  1. Serwis A otrzymuje żądanie do wykonania operacji technicznej (bez użytkownika końcowego).
  2. A uwierzytelnia się do IdP (client_id + client_secret, certificate lub private_key_jwt).
  3. IdP wydaje access token z audiencją serwisu B.
  4. A woła B, przesyłając token w nagłówku Authorization.

Uprawnienia są opisane przez scope’y/role przypisane do klienta technicznego. Tokeny zwykle mają krótkie lifetimy, refresh tokeny często nie są potrzebne – łatwiej jest ponawiać pełne żądanie do /token.

M2M z kontekstem użytkownika

Czasem serwis techniczny potrzebuje działać „w imieniu” użytkownika, ale bez jego aktywnej sesji w przeglądarce. Przykład: batch generujący raport dla menedżera.

Dostępne są różne podejścia:

  • On-behalf-of / token exchange – serwis wymienia otrzymany token użytkownika na nowy, z audiencją i uprawnieniami dopasowanymi do zadania.
  • Delegacje długoterminowe – użytkownik raz nadaje aplikacji uprawnienia (consent), a ta otrzymuje własne refresh tokeny, których używa bez dalszej interakcji użytkownika.

Ten drugi wariant trzeba projektować bardzo ostrożnie: idzie w stronę „działań w tle” z szerokimi uprawnieniami. Scope’y powinny być maksymalnie wąskie, a logi – bardzo szczegółowe.

API gateway jako centralny strażnik

Przy większej liczbie usług wprowadza się gateway, który bierze na siebie część obowiązków OAuth2/OIDC.

Gateway może:

  • weryfikować podpis i podstawowe claimy tokenu,
  • odrzucać żądania z brakującym lub niepoprawnym tokenem,
  • udostępniać zaufane nagłówki z danymi z tokenu dla backendów (np. X-User-Id, X-Scopes),
  • egzekwować rate limiting per użytkownik/klient.

Wtedy serwisy biznesowe skupiają się na logice domenowej, a nie na szczegółach kryptografii. Trzeba tylko zadbać, by nie akceptowały „surowych” nagłówków od świata zewnętrznego – wszystko powinno przechodzić przez gateway.

Edge vs service-to-service – różne poziomy zaufania

Token używany na brzegu (użytkownik → gateway) nie zawsze jest odpowiedni dla komunikacji wewnętrznej między usługami.

Dwa poziomy bezpieczeństwa:

  • zaufanie zewnętrzne – token użytkownika z IdP, o dużej zmienności,
  • zaufanie wewnętrzne – wewnętrzne tokeny serwisów, często z innym issuerem i oddzielnym kluczem, krótkie (czasem kilkudziesięciosekundowe) lifetimy.

Gateway może mapować token zewnętrzny na token wewnętrzny (np. wystawiany przez wewnętrzny IdP), przenosząc tylko minimalny zestaw claimów potrzebnych usługom. To ogranicza efekt ewentualnego wycieku wewnętrznego tokenu poza sieć.

Najczęściej zadawane pytania (FAQ)

Dlaczego API key i Basic Auth są niewystarczające do zabezpieczenia API?

API key i Basic Auth działają jak stałe hasło do całego API. Jeśli klucz wycieknie, atakujący ma pełen dostęp, dopóki ktoś ręcznie go nie unieważni. Nie ma podziału na uprawnienia, brak kontekstu „kto” i „w czyim imieniu” wywołał endpoint.

Przy większej liczbie integracji rośnie chaos: dziesiątki kluczy, brak wygasania, ręczne zarządzanie dostępem. Trudno też spełnić wymagania audytowe, compliance (np. RODO) i zasady najmniejszych uprawnień.

Kiedy powinienem przejść z API key na OAuth2 i OpenID Connect?

Sygnalem do migracji są przede wszystkim: udostępnienie API podmiotom zewnętrznym, większa liczba aplikacji (web, mobile, B2B) oraz wymagania klientów korporacyjnych i działu bezpieczeństwa. Dochodzą też regulacje prawne i potrzeba jedno­krotnego logowania (SSO) między systemami.

Gdy zaczyna być potrzebna precyzyjna kontrola uprawnień (scope’y, role), centralne zarządzanie dostępem i możliwość audytu, proste klucze przestają wystarczać. Wtedy OAuth2/OIDC staje się praktycznie koniecznością.

Czym różni się OAuth2 od OpenID Connect w kontekście API?

OAuth2 rozwiązuje autoryzację, czyli przydzielanie dostępu do zasobów API na podstawie tokenów (access token). Nie mówi jednak, jak dokładnie użytkownik się loguje i jakie dane o nim dostaje aplikacja.

OpenID Connect rozszerza OAuth2 o uwierzytelnianie. Dostarcza ID Token oraz standardowe endpointy, które pozwalają potwierdzić tożsamość użytkownika i odczytać jego podstawowe dane. Dzięki temu aplikacje mogą korzystać z jednego IdP w spójny sposób.

Jaki flow OAuth2 wybrać dla SPA, aplikacji mobilnej i API M2M?

Praktyczny podział wygląda tak:

  • SPA (frontend w przeglądarce) – Authorization Code + PKCE, bez client secret w kodzie JS.
  • Aplikacje mobilne – Authorization Code + PKCE, z użyciem dedykowanej biblioteki (np. AppAuth).
  • Backend web (server-side) – Authorization Code (opcjonalnie z PKCE), client secret na serwerze.
  • Integracje M2M / mikroserwisy – Client Credentials z ograniczonymi scope’ami.

Przepływy typu Implicit są przestarzałe i nie powinny być już używane w nowych wdrożeniach.

Co to jest access token i refresh token i jak bezpiecznie ich używać?

Access token to krótko żyjący token używany do wywoływania API. Określa, do jakich zasobów i w jakim zakresie (scope) ma dostęp klient. Może być JWT lub nieprzezroczystym ciągiem, który weryfikuje serwer autoryzacji.

Refresh token służy do odświeżania access tokenów bez ponownego logowania użytkownika. Jest wrażliwym sekretem – powinien być przechowywany tylko po stronie backendu lub w bezpiecznym magazynie w aplikacji natywnej, nigdy w lokalnym storage SPA czy publicznym repozytorium.

Czym różni się ID Token od Access Token i kiedy którego używać?

ID Token to JWT potwierdzający uwierzytelnienie użytkownika przez dostawcę tożsamości. Zawiera claimy o użytkowniku (np. sub, email) i jest przeznaczony dla aplikacji klienckiej, aby wiedziała „kto się zalogował”.

Access Token służy do autoryzacji wywołań API na serwerze zasobów. API powinno ufać wyłącznie access tokenowi, a nie ID Tokenowi. ID Token można w ogóle nie wysyłać do API – wystarcza aplikacji frontendowej lub backendowi do identyfikacji użytkownika.

Jak dzięki OAuth2 i OIDC wdrożyć zasady najmniejszych uprawnień w API?

Podstawowym narzędziem są scope’y i role odzwierciedlające konkretne uprawnienia. API na poziomie endpointów wymusza odpowiedni scope, np. read:orders do odczytu zamówień, write:orders do modyfikacji, admin tylko dla operacji administracyjnych.

Dodatkowo można rozdzielić uprawnienia dla użytkowników i integracji M2M, wydając różne konfiguracje klientów OAuth2. Pozwala to dać partnerowi B2B dostęp tylko do wybranego fragmentu API, bez ingerencji w resztę systemu.

1 KOMENTARZ

  1. Ciekawy artykuł! Wdrożenie OAuth2 i OpenID Connect może być kluczowe dla zapewnienia bezpieczeństwa naszych API. Dzięki temu rozwiązaniu możemy skutecznie zarządzać autoryzacją i uwierzytelnieniem, eliminując wiele potencjalnych zagrożeń. Zdecydowanie warto sięgnąć po te narzędzia, by chronić nasze dane oraz naszych użytkowników przed atakami i nieautoryzowanym dostępem. Bardzo pomocny tekst dla osób, które dopiero zaczynają przygodę z bezpieczeństwem API!

Wymagane logowanie do dodawania komentarzy.