Po co w ogóle OAuth2 i OpenID Connect w Spring Boot
Różnica między uwierzytelnianiem a autoryzacją w aplikacji
W aplikacjach biznesowych dwie rzeczy są kluczowe: potwierdzenie tożsamości użytkownika oraz kontrola, do czego ma dostęp. Uwierzytelnianie (authentication) odpowiada na pytanie „kim jesteś?”, a autoryzacja (authorization) – „do czego możesz wejść i co możesz zrobić?”. To rozdzielenie bywa w praktyce pomijane, co prowadzi do niejasnej i trudnej w utrzymaniu konfiguracji bezpieczeństwa.
OAuth2 koncentruje się głównie na autoryzacji – czyli na nadawaniu aplikacjom (klientom) kontrolowanego dostępu do zasobów w imieniu użytkownika. OpenID Connect (OIDC) natomiast buduje na szczycie OAuth2 warstwę tożsamości: dostarcza ustandaryzowany sposób uzyskania informacji o zalogowanej osobie i jej sesji. Połączenie OAuth2 + OIDC daje więc pełny obraz: z jednej strony wiadomo, kto jest zalogowany, z drugiej – jakie zasoby i operacje są dla niego dozwolone.
W tradycyjnych aplikacjach monolitycznych login/hasło w bazie i sesja HTTP często wystarczają. W systemach rozproszonych, z osobnymi frontendami, backendami i serwisami domenowymi, proste podejście zaczyna się kruszyć. Brakuje jednego źródła tożsamości, pojawiają się niezsynchronizowane sesje i trudności z obsługą single sign-on.
Dlaczego same loginy i hasła w bazie to za mało
Przechowywanie loginów i haseł bezpośrednio w bazie aplikacji bywa kuszące – pełna kontrola i prosty model programistyczny. W praktyce takie rozwiązanie zwykle szybko ujawnia swoje ograniczenia, szczególnie gdy aplikacja przestaje być pojedynczym monolitem. Pojawiają się problemy:
- każda nowa usługa musi implementować swój mechanizm logowania lub korzystać z centralnej bazy użytkowników,
- brak centralnego miejsca do zarządzania hasłami, blokadami kont, wymuszaniem polityki haseł czy wieloskładnikowego uwierzytelniania,
- trudność w integracji z zewnętrznymi dostawcami tożsamości (Google, Azure AD, korporacyjne IdP),
- kłopotliwe audytowanie dostępu: w wielu miejscach powstają własne tabele logów i reguły.
OAuth2 i OpenID Connect odsuwają aplikację od konieczności samodzielnego zarządzania hasłami. Aplikacja Spring Boot staje się po prostu klientem serwera autoryzacji/uwierzytelniania i polega na wystawianych przez niego tokenach dostępu i tokenach ID. Dzięki temu można:
- łatwiej wdrożyć SSO między różnymi aplikacjami,
- centralnie konfigurować MFA, polityki haseł, blokady kont,
- dokładać kolejne aplikacje bez powielania mechanizmów logowania.
Typowe scenariusze użycia w aplikacjach biznesowych
W praktyce Spring Boot jest często sercem architektury, do którego dochodzą różne fronty i integracje. OAuth2/OIDC porządkują te scenariusze:
- SPA + REST API – frontend w React/Angular/Vue, backend jako REST API. Użytkownik loguje się przez zewnętrznego dostawcę (Keycloak, Auth0, Azure AD), dostaje tokeny, a API weryfikuje je jako resource server.
- Panel administratora – klasyczna aplikacja server-side render (Thymeleaf, Freemarker) korzystająca z logowania przez zewnętrzny IdP, gdzie role i grupy przychodzą w tokenach i decydują o uprawnieniach.
- Aplikacje mobilne – klient mobilny też może korzystać z Authorization Code Flow z PKCE. Backend Spring Boot pełni rolę resource server, a także potrafi obsłużyć odświeżanie tokenów.
- Mikrousługi – wewnętrzna komunikacja serwis–serwis oparta na Client Credentials Flow lub wewnętrznych JWT wydawanych przez centralny serwer autoryzacji.
W każdej z tych konfiguracji można korzystać z tego samego serwera tożsamości, a Spring Boot dostarcza spójne API konfiguracji – zarówno po stronie klienta (login użytkownika), jak i resource server (weryfikacja tokenów).
Dlaczego nie opierać się wyłącznie na własnoręcznie pisanych mechanizmach
Samodzielne pisanie całej logiki bezpieczeństwa zwykle na początku sprawia wrażenie większej kontroli. Po paru miesiącach wychodzą jednak konsekwencje: brak aktualizacji bezpieczeństwa, trudności z obsługą nowych przeglądarek, edge-case’ów w cookies, brak standardowych integracji z MFA, brak gotowych narzędzi testujących. Standardy, takie jak OAuth2 i OpenID Connect, powstały po to, aby te problemy rozwiązywać raz, a porządnie.
Konsekwencje dla bezpieczeństwa są dość oczywiste: implementacje roll-your-own-auth często nie przewidują rzadkich, ale kosztownych klas ataków (np. CSRF przy logowaniu, wycieki tokenów w URL, błędy w przechowywaniu haseł). Używając Spring Security i sprawdzonych serwerów autoryzacji (Keycloak, Auth0, Azure AD, Okta), korzystasz z kodu, który jest publicznie analizowany i audytowany.
Równie istotna jest utrzymywalność. W przypadku OAuth2/OIDC:
- łatwo podmienić dostawcę tożsamości – aplikacja Spring Boot bazuje na abstrakcjach Spring Security, nie na szczegółach jednego IdP,
- inne zespoły w organizacji mogą korzystać z tego samego IdP w innych technologiach, zachowując spójny schemat tokenów i uprawnień,
- audyt logowań, blokad i błędów logowania odbywa się w jednym narzędziu (panel IdP), a nie w kilkunastu aplikacjach.
Standardy wprowadzają przewidywalność: jeśli system obsługuje Authorization Code Flow i OIDC Discovery, dowolny inny klient zgodny ze specyfikacją będzie wiedział, jak się z nim zintegrować. To zdejmuje z programisty dużą część zgadywania i eksperymentowania. Spring Boot wykorzystuje te standardy, aby sprowadzić konfigurację często do kilku wpisów w application.yml.
Podstawy OAuth2 bez magii – role, granty, przepływy
Główne podmioty w OAuth2 i ich odpowiedniki w Spring Boot
W modelu OAuth2 występuje kilka podmiotów o dość precyzyjnie zdefiniowanych rolach. Zrozumienie tego schematu bardzo upraszcza późniejszą konfigurację Spring Security.
- Resource Owner – zwykle użytkownik końcowy, który posiada dane (np. profil, dokumenty, zasoby API) i może przyznać aplikacji do nich dostęp.
- Client – aplikacja, która chce uzyskać dostęp do zasobów w imieniu użytkownika (np. SPA, aplikacja mobilna, backend server-side).
- Authorization Server – system, który uwierzytelnia użytkownika i wydaje tokeny (Keycloak, Auth0, Azure AD, serwer OAuth2).
- Resource Server – API lub serwis, który chroni swoje zasoby i weryfikuje tokeny wystawione przez Authorization Server.
W świecie Spring Boot te role mapują się w następujący sposób:
- Aplikacja webowa (login użytkownika) – Spring Boot z
spring-boot-starter-oauth2-clientto typowy OAuth2 Client. Użytkownik loguje się przez IdP, a Spring zarządza sesją po stronie aplikacji. - REST API chronione tokenem – Spring Boot z
spring-boot-starter-oauth2-resource-serverpełni rolę Resource Server, weryfikując JWT lub opaque token. - Serwer autoryzacji – najczęściej osobna instancja (Keycloak, Auth0, Azure AD). Można też użyć Spring Authorization Server, ale to odrębny komponent.
Bardzo często ten sam projekt Spring Boot może być jednocześnie klientem OAuth2 i resource serverem (np. aplikacja webowa, która ma też REST API). Konfiguracja Spring Security pozwala to ująć w jednej klasie lub kilku dedykowanych konfiguracjach.
Najważniejsze przepływy OAuth2 i kiedy ich używać
Specyfikacja OAuth2 opisuje kilka tzw. grant types (przepływów). Współcześnie w nowych projektach sens mają w zasadzie trzy z nich, z wyraźną preferencją jednego:
- Authorization Code Flow z PKCE – podstawowy, zalecany dla aplikacji webowych i SPA, również dla aplikacji mobilnych.
- Client Credentials Flow – do komunikacji serwer–serwer, bez udziału użytkownika.
- Refresh Token Flow – uzupełniający, służy do odnawiania access tokenu bez ponownego logowania użytkownika.
Authorization Code Flow przebiega w skrócie tak:
- Użytkownik wchodzi w aplikację (klienta). Aplikacja przekierowuje go na /authorize serwera autoryzacji.
- Użytkownik loguje się i, jeśli trzeba, wyraża zgodę na zakresy (scopes).
- Serwer autoryzacji odsyła użytkownika z powrotem do aplikacji z krótkotrwałym authorization code.
- Aplikacja serwerowa wymienia ten code na access token (i opcjonalnie refresh token, ID token) po stronie backendu, uwierzytelniając się client_secretem.
PKCE (Proof Key for Code Exchange) dodaje zabezpieczenie przed kradzieżą code’a przez złośliwe aplikacje. W Spring Security jest używane automatycznie w kliencie OIDC dla publicznych klientów.
Client Credentials Flow jest prostszy – klient (np. mikroserwis) identyfikuje się tylko swoim client_id i client_secret, a serwer autoryzacji wystawia token, który nie reprezentuje konkretnego użytkownika, lecz samą aplikację. Token ma zwykle inne scope’y (np. service.read, service.write) i nie zawiera claimów dotyczących użytkownika.
Resource Owner Password Credentials (tzw. password grant) jest dziś co do zasady odradzany. Zakłada on, że aplikacja klienta bierze od użytkownika login i hasło i wysyła je do serwera autoryzacji. Łamie to separację: klient dostaje pełne dane logowania, zamiast przekierować użytkownika do zaufanego IdP. W nowych projektach lepiej go unikać, a w istniejących stopniowo wygaszać na rzecz Authorization Code Flow.
Zakresy (scopes) a role (authorities) w Spring Security
Scope w OAuth2 to deklaracja zakresu dostępu, o jaki prosi klient. To nie jest równoznaczne z rolą użytkownika, choć bywa do niej zbliżone. Z perspektywy serwera autoryzacji scope’y to etykiety, które mówią: „ten token daje prawo do X, Y, Z”. Serwer resource server może następnie na ich podstawie decyzjonować dostęp.
W Spring Security ważne jest odróżnienie scope’ów od authorities (uprawnień). Domyślnie, jeśli używany jest spring-boot-starter-oauth2-resource-server z JWT, scope’y są mapowane do authorities z prefixem SCOPE_. Przykładowo:
- scope
api.read→ authoritySCOPE_api.read, - scope
profile→ authoritySCOPE_profile.
Równolegle mogą istnieć role aplikacyjne, takie jak ROLE_ADMIN, ROLE_USER, często dostarczane w osobnym cliamie (np. roles, realm_access, groups) przez IdP. To właśnie na rolach zwykle opiera się logikę @PreAuthorize("hasRole('ADMIN')"). W dobrze zaprojektowanym systemie:
- scope’y opisują „jakie API lub funkcje” są dostępne,
- role/authorities opisują „kim jest użytkownik w systemie”.
W Spring Boot można zdefiniować własne mapowanie claimów w tokenie do authorities, tworząc customowy JwtGrantedAuthoritiesConverter albo własny JwtAuthenticationConverter. Dzięki temu łatwo połączyć world scope’ów OAuth2 z rolami biznesowymi.

OpenID Connect – warstwa tożsamości na bazie OAuth2
Dlaczego sam OAuth2 nie wystarcza do logowania użytkownika
OAuth2 sam w sobie nie odpowiada precyzyjnie na pytanie, kim jest użytkownik. Określa jedynie, jak przyznać aplikacji dostęp do zasobów. Nic nie mówi o tym, jakie dane o użytkowniku powinny się znaleźć w tokenie ani jak je interpretować. W jednym systemie sub może być loginem, w innym wewnętrznym ID, w jeszcze innym – adresem e-mail. Brakuje standardu.
OpenID Connect (OIDC) rozwiązuje ten problem. Definiuje on sposób, w jaki serwer autoryzacji ma zwracać informacje o użytkowniku oraz o samej sesji uwierzytelnienia. OIDC dodaje do OAuth2 m.in. ID Token i UserInfo Endpoint, a także standaryzuje claimy i sposób odkrywania konfiguracji serwera (OpenID Provider Configuration, tzw. discovery).
Dlatego w praktyce do logowania użytkowników w aplikacjach Spring Boot stosuje się OpenID Connect (czyli OAuth2 + warstwa tożsamości), a nie „goły” OAuth2. Logowanie przez Google, Microsoft, Keycloak, Auth0 – wszystko to są w istocie implementacje OIDC.
Kluczowe elementy OpenID Connect
OpenID Connect wprowadza kilka fundamentalnych elementów, które trzeba rozumieć, aby konfiguracja Spring Boot miała sens:
- ID Token – token JWT zawierający informacje o użytkowniku i sesji uwierzytelnienia.
- UserInfo Endpoint – endpoint HTTP, z którego można pobrać dodatkowe dane o użytkowniku na podstawie access tokena.
Standardowe claimy w ID Token i ich znaczenie dla Spring Boot
ID Token zawiera zbiór ustandaryzowanych claimów. Część z nich jest kluczowa dla poprawnej integracji ze Spring Security, inne przydają się głównie w logice biznesowej lub przy debugowaniu.
Najczęściej spotykane claimy, na które realnie patrzy aplikacja:
iss(issuer) – adres serwera tożsamości. Resource server i klient w Springu używają go, aby upewnić się, że token pochodzi z oczekiwanego źródła.sub(subject) – stabilny identyfikator użytkownika w ramach danego providera. To zwykle najlepszy klucz techniczny do mapowania konta w systemie.aud(audience) – wskazuje odbiorcę tokenu (klienta lub API). Pozwala odsiać tokeny wystawione dla innej aplikacji.exp,iat,nbf– znaczniki czasu (wygaśnięcia, wystawienia, najwcześniejszego użycia). Spring automatycznie weryfikujeexpi, w zależności od konfiguracji biblioteki JWT, pozostałe.auth_time– czas faktycznego uwierzytelnienia użytkownika. Przydaje się przy wymuszaniu silniejszego uwierzytelniania (np. ponowne MFA).nonce– zabezpieczenie przed atakami replay w przeglądarkowych przepływach OIDC. Spring dopasowuje wartość nonce do tej przechowywanej w sesji.
Do tego dochodzą claimy „profilowe” z zakresu profile, email, phone czy address:
name,given_name,family_name,preferred_username,email,email_verified,locale,zoneinfoi inne.
W Spring Security claimy z ID Token są mapowane do obiektu OidcUser. Z perspektywy kontrolera można je odczytać np. przez:
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal OidcUser user) {
return user.getClaims();
}Kluczowe jest rozróżnienie: ID Token służy głównie do identyfikacji użytkownika po stronie klienta (aplikacji), natomiast access token jest używany przez resource server (API) do autoryzacji dostępu do zasobów.
UserInfo Endpoint a dane użytkownika w Spring Security
Nie każdy provider wkłada komplet informacji o użytkowniku do ID Tokenu. Zdarza się, że część dostępna jest tylko pod UserInfo Endpoint. Klient OIDC w Spring Boot może w takim przypadku automatycznie wykonać dodatkowe wywołanie HTTP po zalogowaniu.
W konfiguracji rejestracji klienta można sterować tym, czy i kiedy Spring ma pobierać dane z UserInfo:
- tylko ID Token – gdy wszystkie potrzebne dane są w JWT, dodatkowe zapytanie jest zbędne,
- tylko UserInfo – w specyficznych integracjach, gdy ID Token jest bardzo „chudy”,
- hybryda – dane częściowo w ID Token, częściowo z UserInfo.
Domyślne zachowanie zależy od providera (np. wbudowany support dla Google) i od konfiguracji user-info-uri / user-name-attribute. Z punktu widzenia aplikacji OidcUser scala claimy z ID Tokenu i odpowiedzi UserInfo w jeden widok. Przy debugowaniu nieporozumień („czemu nie widzę e-maila użytkownika?”) dobrze jest sprawdzić, czy zakres email został rzeczywiście przyznany i czy serwer wystawia ten claim w ID Tokenie, czy tylko pod UserInfo.
Przegląd Spring Security OAuth2 / OIDC – z czym pracujemy
Kluczowe startery i moduły Spring Boot
Spring Boot oferuje dwa podstawowe startery, które pokrywają większość scenariuszy OAuth2/OIDC:
spring-boot-starter-oauth2-client– integracja Spring Security z serwerem OIDC/OAuth2 po stronie klienta (logowanie użytkownika, single sign-on, wywoływanie zewnętrznych API).spring-boot-starter-oauth2-resource-server– ochrona REST API po stronie resource servera (walidacja tokenów JWT lub opaque, mapowanie scope’ów na authorities).
Oba startery opierają się na tym samym rdzeniu – spring-security-oauth2-core oraz modułach specyficznych dla klienta i resource servera. Konfiguracja odbywa się głównie w application.yml oraz w klasie konfiguracyjnej z SecurityFilterChain.
Osobnym komponentem jest Spring Authorization Server. To biblioteka do budowy własnego serwera autoryzacji / providera OIDC. Nie jest częścią starterów klienta ani resource servera i zwykle działa w osobnej aplikacji (inne jar, inny proces).
Mechanizmy logowania: oauth2Login() i oauth2Client()
W konfiguracji HTTP Spring Security stosuje się dwa powiązane, ale różne „moduły”: oauth2Login() i oauth2Client().
oauth2Login()– obsługuje uwierzytelnienie użytkownika w aplikacji webowej. Dodaje filtry, które:- przekierowują na stronę logowania IdP,
- odbierają kod autoryzacyjny, wymieniają go na tokeny,
- tworzą sesję Spring Security z wypełnionym
Principal(np.OidcUser).
oauth2Client()– odpowiada za korzystanie z tokenów przez aplikację jako klient zewnętrznych API. Pozwala np. wstrzyknąćOAuth2AuthorizedClientManageri automatycznie dołączać tokeny wWebClientlubRestTemplate.
Typowy scenariusz: użytkownik loguje się do aplikacji webowej dzięki oauth2Login(), a aplikacja następnie wywołuje w jego imieniu inne API, korzystając z oauth2Client() (delegacja uprawnień, tzw. „on-behalf-of”). W prostych systemach drugi element można pominąć, jeśli aplikacja nie wywołuje innych usług.
Jak Spring trzyma informacje o zalogowanym użytkowniku
Po udanym logowaniu przez OIDC Spring tworzy instancję Authentication, która staje się częścią Security Contextu oraz sesji HTTP. W przypadku OIDC najczęściej jest to OAuth2AuthenticationToken, a reprezentacją użytkownika – OidcUser.
Domyślny user details zawiera:
- listę authorities (zwykle scope’y + ewentualne role z tokenu),
- zbiór claimów z ID Tokenu / UserInfo,
- atrybut
name, który może zostać użyty jako „wyświetlana” nazwa użytkownika.
W kontrolerach można korzystać z adnotacji:
@GetMapping("/hello")
public String hello(@AuthenticationPrincipal OidcUser user) {
return "Hello " + user.getFullName();
}Jeśli wymagane jest inne mapowanie claimów do authorities, można przygotować własny GrantedAuthoritiesMapper lub OidcUserService i wstrzyknąć go do konfiguracji oauth2Login(). To typowy punkt, w którym mapuje się np. grupy z Azure AD na role ROLE_ADMIN / ROLE_USER.

Konfiguracja klienta OAuth2 / OIDC w Spring Boot krok po kroku
Definicja klienta OIDC w application.yml
Fundamentem konfiguracji klienta OIDC w Spring Boot jest sekcja spring.security.oauth2.client. W prostym przypadku wystarczy wskazać providera (np. Keycloak) i jeden lub kilka rejestrów klientów.
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://auth.example.com/realms/demo
registration:
demo-app:
provider: keycloak
client-id: demo-app
client-secret: <sekret-z-keycloak>
scope:
- openid
- profile
- email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"Kilka uwag praktycznych do powyższej konfiguracji:
issuer-uri– Spring wykorzysta OpenID Connect Discovery (/.well-known/openid-configuration), aby pobrać adresy endpointów i klucze publiczne do weryfikacji podpisu JWT.client-id/client-secret– dane rejestracji z serwera autoryzacji. Sekret warto trzymać w bezpiecznym mechanizmie (Vault, zmienne środowiskowe), a nie na stałe w repozytorium.redirect-uri– musi być zgodna z adresem skonfigurowanym w IdP. Placeholdery{baseUrl}i{registrationId}są rozwijane przez Springa.- scope – zawsze obejmuje
openid(bo to OIDC), pozostałe zależą od tego, jakie dane i API są potrzebne.
Jeśli korzysta się z popularnych providerów (Google, GitHub), Spring Boot ma prekonfigurowane aliasy; wówczas wystarczy część parametrów. Przy własnym IdP (Keycloak, Auth0, wewnętrzny serwer) najpewniejszym rozwiązaniem jest wskazanie issuer-uri.
Podstawowa konfiguracja SecurityFilterChain dla klienta
Gdy konfiguracja w application.yml jest gotowa, kolejnym krokiem jest włączenie oauth2Login() w konfiguracji HTTP.
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults()); // logowanie przez OIDC
return http.build();
}
}Ten minimalny wariant realizuje scenariusz:
- żądanie do zasobu chronionego → przekierowanie na IdP,
- logowanie użytkownika w IdP → powrót na
/login/oauth2/code/demo-app, - odebranie kodu, wymiana na tokeny → utworzenie sesji i przekierowanie na stronę, z której wyszło żądanie.
Jeżeli w aplikacji występuje własna strona logowania (np. z wyborem providera), można ją wskazać przez:
http
.oauth2Login(oauth2 -> oauth2
.loginPage("/login") // własny endpoint logowania
);Dostęp do tokenów w kodzie aplikacji
Niekiedy aplikacja potrzebuje access tokenu, aby zadziałać jako klient innego API (np. backend wykorzystuje token użytkownika do wywołania mikroserwisu). Spring udostępnia tokeny przez OAuth2AuthorizedClientService i adnotację @RegisteredOAuth2AuthorizedClient.
@GetMapping("/call-api")
public String callApi(@RegisteredOAuth2AuthorizedClient("demo-app")
OAuth2AuthorizedClient authorizedClient) {
String accessToken = authorizedClient.getAccessToken().getTokenValue();
// ... użycie tokenu w wywołaniu innego API
return "ok";
}W przypadku użycia WebClient istnieje integracja, która automatycznie dołącza token do nagłówka Authorization:
@Bean
WebClient webClient(ClientRegistrationRepository clients,
OAuth2AuthorizedClientRepository authzRepo) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authzRepo);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.apply(oauth2.oauth2Configuration())
.build();
}Tak skonfigurowany WebClient pobierze token bieżącego użytkownika z sesji i doda go przy każdym wywołaniu.
Przykład: integracja z Keycloak jako IdP OIDC
Typowy scenariusz z Keycloak wygląda następująco:
- W Keycloak tworzy się realm (np.
demo), - dodaje się klienta typu confidential z poprawnym redirect URI,
- generuje się
client-secret, - odczytuje się adres discovery (issuer) – np.
https://auth.example.com/realms/demo.
Następnie w Spring Boot ustawia się issuer-uri i parametry rejestracji. Po stronie Keycloak można zdecydować, czy role aplikacyjne mają trafiać do claimu realm_access, resource_access czy np. groups. Na tej podstawie w Spring Boot buduje się customowy konwerter authorities, który będzie z tych claimów wyciągał ROLE_*.
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter scopesConverter = new JwtGrantedAuthoritiesConverter();
scopesConverter.setAuthorityPrefix("SCOPE_");
return new JwtAuthenticationConverter() {
@Override
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
Collection<GrantedAuthority> authorities = new ArrayList();
authorities.addAll(scopesConverter.convert(jwt));
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null && realmAccess.containsKey("roles")) {
List<String> roles = (List<String>) realmAccess.get("roles");
roles.stream()
.map(role -> "ROLE_" + role)
.map(SimpleGrantedAuthority::new)
.
Najczęstsze problemy przy konfiguracji klienta OIDC
Przy pierwszym podejściu do integracji z IdP błędy konfiguracyjne są raczej regułą niż wyjątkiem. Dobrze znać kilka typowych objawów i ich przyczyny.
- „
invalid_redirect_uri” – adres redirect URI zgłoszony przez aplikację nie pokrywa się z tym zapisanym w konfiguracji klienta na IdP. Niewielka różnica (HTTP vs HTTPS, brak ukośnika na końcu) wystarczy, aby logowanie się nie powiodło.
- „
invalid_scope” – aplikacja żąda scope’u, którego IdP nie zna lub który nie jest przypisany do danego klienta. Często dotyczy scope’ów do API (np. api.read), które trzeba osobno dodać.
- „
invalid_grant” przy wymianie kodu na token – kod autoryzacyjny został już użyty, wygasł albo redirect URI w żądaniu tokenu nie zgadza się z tym z fazy autoryzacji.
- Błąd 401/403 w aplikacji po poprawnym logowaniu – użytkownik jest uwierzytelniony, ale brakuje mu wymaganych authorities (scope’ów lub ról). Powód najczęściej leży w konfiguracji claimów po stronie IdP lub w mapowaniu authorities po stronie Springa.
Przy diagnozie bardzo pomaga podgląd pełnych treści tokenów (w środowisku deweloperskim). Można je zdekodować np. w narzędziu online albo logując surowy JWT i analizując nagłówek oraz payload. W środowiskach produkcyjnych taki logging zwykle jest niedopuszczalny, więc lepiej od razu zadbać o osobne narzędzia diagnostyczne na dewelopment.
Spring Boot jako Resource Server – ochrona REST API tokenami
Gdy backend ma pełnić rolę API, do którego odwołują się inne usługi (front-end SPA, mobilka, mikroserwisy), wygodniej jest użyć modelu „stateless” z JWT niż sesji HTTP. Spring Security oferuje do tego tryb Resource Server, w którym aplikacja przyjmuje i weryfikuje access tokeny wystawione przez zewnętrzny Authorization Server / IdP.
Rola Resource Servera w architekturze OAuth2
W modelu OAuth2 Resource Server:
- weryfikuje podpis i ważność access tokenu,
- wyciąga z tokenu claimy (sub, scope, role),
- na ich podstawie buduje
Authentication,
- decyduje o dostępie do endpointów, ale nie odpowiada za proces logowania.
Logowanie odbywa się „gdzieś indziej” – w aplikacji frontowej lub w dedykowanym Authorization Serverze. Resource Server zakłada, że otrzymuje już prawidłowy token, choć nadal musi go zweryfikować kryptograficznie.
Minimalna konfiguracja Resource Servera z JWT
Podstawowa konfiguracja w Spring Boot dla JWT sprowadza się do wskazania issuer-uri albo adresu jwk set (jwk-set-uri), z którego pobierane są klucze publiczne do weryfikacji podpisu.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/realms/demo
# lub alternatywnie:
# jwk-set-uri: https://auth.example.com/realms/demo/protocol/openid-connect/certs
Po stronie konfiguracji HTTP trzeba włączyć filtr Resource Servera:
@Configuration
@EnableMethodSecurity
public class ApiSecurityConfig {
@Bean
SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // REST API typowo korzysta z tokenów, nie z cookies
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
);
return http.build();
}
}
Taki wariant zakłada, że IdP wystawia access tokeny w formacie JWT i publikuje metadane OIDC (issuer-uri). Przy każdym żądaniu Resource Server odczyta nagłówek Authorization: Bearer <token>, zweryfikuje podpis i – przy pomyślnej weryfikacji – wpuści żądanie do dalszej części aplikacji.
Weryfikacja audience i issuer – zabezpieczenie przed „token reuse”
Dobrą praktyką jest kontrola nie tylko ważności i podpisu, ale także takich elementów jak:
iss (issuer) – musi pochodzić z oczekiwanego IdP,aud (audience) – powinna zawierać identyfikator danego API.
Dzięki temu ten sam token nie zostanie użyty przeciwko innemu serwerowi zasobów, dla którego nie był przeznaczony. W Spring Security można dodać własny JwtValidator, jeśli wymagania są bardziej złożone.
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder =
JwtDecoders.fromIssuerLocation("https://auth.example.com/realms/demo");
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(
"https://auth.example.com/realms/demo"
);
OAuth2TokenValidator<Jwt> audienceValidator =
jwt -> {
List<String> audiences = jwt.getAudience();
if (audiences != null && audiences.contains("demo-api")) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error("invalid_token",
"Invalid audience", null);
return OAuth2TokenValidatorResult.failure(error);
};
OAuth2TokenValidator<Jwt> validator =
new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
decoder.setJwtValidator(validator);
return decoder;
}
Tak skonfigurowany dekoder odrzuci tokeny, które formalnie są poprawne kryptograficznie, lecz mają inne aud niż oczekiwane demo-api.
Mapowanie scope’ów i claimów JWT na authorities
Domyślne mapowanie w Resource Serverze bazuje na scope’ach (przekształcanych na SCOPE_*). W wielu systemach istotniejsze są jednak role, grupy lub inne claimy. Do ich obsługi służy JwtAuthenticationConverter, w którym można rozszerzyć domyślną logikę.
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter scopesConverter = new JwtGrantedAuthoritiesConverter();
scopesConverter.setAuthorityPrefix("SCOPE_");
scopesConverter.setAuthoritiesClaimName("scope"); // lub "scp", w zależności od IdP
return new JwtAuthenticationConverter() {
@Override
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
Collection<GrantedAuthority> authorities = new ArrayList();
authorities.addAll(scopesConverter.convert(jwt));
Map<String, Object> realmAccess = jwt.getClaim("realm_access");
if (realmAccess != null && realmAccess.containsKey("roles")) {
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
roles.stream()
.map(role -> "ROLE_" + role)
.map(SimpleGrantedAuthority::new)
.forEach(authorities::add);
}
return authorities;
}
};
}
Konwerter trzeba następnie podpiąć do konfiguracji Resource Servera:
@Bean
SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
Po takim rozszerzeniu możliwe jest używanie @PreAuthorize("hasRole('ADMIN')") lub @PreAuthorize("hasAuthority('SCOPE_orders.read')") w zależności od potrzeb.
Autoryzacja na poziomie endpointów i metod
Przy REST API często wygodniejsza jest autoryzacja na poziomie metod niż pojedyncza, globalna konfiguracja wzorców URL. Spring Security daje kilka modeli:
@PreAuthorize / @PostAuthorize – elastyczne wyrażenia SpEL oparte na authorities, claimach lub danych metody,@Secured – prostszy model bazujący wyłącznie na rolach (prefiks ROLE_),@RolesAllowed – kompatybilne z JSR-250.
Przykładowo, można ograniczyć dostęp do zamówień użytkownika do tokenów, które mają zarówno odpowiedni scope, jak i rolę:
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping
@PreAuthorize("hasAuthority('SCOPE_orders.read')")
public List<Order> listOrders() {
// ...
}
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_orders.write') and hasRole('MANAGER')")
public Order createOrder(@RequestBody OrderRequest request) {
// ...
}
}
Taki sposób granuluje uprawnienia wystarczająco precyzyjnie, a jednocześnie pozostaje czytelny dla zespołu.
Łączenie roli klienta OIDC i Resource Servera w jednej aplikacji
W praktyce backend bywa jednocześnie:
- aplikacją serwującą UI i obsługującą logowanie użytkownika (rola OIDC client),
- oraz API zabezpieczonym tokenami (rola Resource Server).
Taka konfiguracja wymaga osobnych filtrów bezpieczeństwa lub przynajmniej jasnego rozróżnienia, które endpointy są obsługiwane w którym trybie. Spring Security od wersji 5.7 pozwala stosować wiele SecurityFilterChain z różnymi matcherami.
@Configuration
@EnableMethodSecurity
public class MultiHttpSecurityConfig {
@Bean
@Order(1)
SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain uiFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.logoutSuccessUrl("/"));
return http.build();
}
}
Żądania do /api/** będą obsługiwane jako „gołe” wywołania REST z nagłówkiem Authorization: Bearer, a pozostałe trafią do typowej konfiguracji aplikacji webowej z sesją i oauth2Login(). Taki układ bywa stosowany np. przy aplikacjach, w których UI korzysta z sesji, a mobilny klient używa czysto tokenów.
Integracja z Keycloak w trybie Resource Servera
Keycloak w naturalny sposób pasuje do roli Authorization Servera i dostawcy tokenów JWT. Schemat jest podobny jak przy kliencie OIDC, ale tym razem kluczowy jest sposób wystawiania access tokenów dla API.
Typowy przebieg w Keycloak:
- Tworzenie klienta reprezentującego API (np.
demo-api) z typem „bearer-only” lub „confidential” – w zależności od potrzeb. - Definiowanie scope’ów i roli przypisanych do API – np. role
user, admin w sekcji klienta. - Upewnienie się, że access token zawiera odpowiednie claimy – np. role w
realm_access lub resource_access, a także właściwy aud (id klienta API).
Po stronie Spring Boot wystarcza skonfigurowanie issuer-uri oraz konwertera authorities, który wyciągnie role z tokenu. Przykładowy kontroler z ograniczeniem „tylko administrator”:
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/stats")
@PreAuthorize("hasRole('admin')")
public Map<String, Object> stats() {
Map<String, Object> result = new HashMap();
result.put("status", "ok");
return result;
}
}
Przy takiej konfiguracji użytkownik musi mieć rolę admin w Keycloak (np. jako realm role), aby wywołać /admin/stats. Brak tej roli zakończy się błędem 403, mimo że token był poprawnie podpisany i nieprzeterminowany.
Rozróżnienie ról „użytkownik” i „serwis” w JWT
W wielu systemach ten sam Resource Server obsługuje zarówno wywołania w imieniu użytkownika (tokeny z grantem Authorization Code lub OIDC), jak i wywołania techniczne między serwisami (Client Credentials). Dobrze jest wtedy rozróżnić te dwa światy na poziomie tokenów i reguł autoryzacji.
Źródłem różnicy może być:
- claim
sub – dla użytkowników często zawiera identyfikator osoby, dla serwisów – identyfikator klienta technicznego, - claim
client_id – wskazujący na klienta, w którego imieniu jest wydany token, - specjalny scope (np.
service), nadawany wyłącznie aplikacjom technicznym.
Na poziomie adnotacji można później wymusić, aby niektóre operacje były dostępne wyłącznie dla serwisów:
@PreAuthorize("hasAuthority('SCOPE_internal') and #oauth2.hasClient('billing-service')")
@PostMapping("/internal/recalculate")
public void recalculateInternal(/* ... */) {
// ...
}
W aktualnych wersjach Spring Security nie ma już wbudowanego #oauth2 w SpEL, ale podobną logikę da się osiągnąć przez własny komponent udostępniony w kontekście wyrażeń lub przez dedykowany PermissionEvaluator. Kluczowe jest, aby Authentication niosła wystarczające informacje o kliencie (zazwyczaj w claimach JWT).
Najczęściej zadawane pytania (FAQ)
Jaka jest różnica między OAuth2 a OpenID Connect w Spring Boot?
OAuth2 to przede wszystkim standard autoryzacji: określa, jak aplikacja-klient może uzyskać dostęp do zasobów w imieniu użytkownika, korzystając z tokenów dostępu. OpenID Connect (OIDC) rozszerza OAuth2 o warstwę tożsamości – definiuje sposób, w jaki klient dostaje ustandaryzowaną informację o zalogowanym użytkowniku (token ID, endpoint userinfo).
W Spring Boot OAuth2 zapewnia kontrolę dostępu do API (resource server), natomiast OIDC umożliwia wygodne pobieranie danych użytkownika (np. e‑mail, identyfikator, grupy) po stronie klienta. W praktyce w nowych projektach korzysta się zwykle z obu standardów naraz: OAuth2 odpowiada za uprawnienia, a OIDC za to, kto faktycznie jest zalogowany.
Dlaczego nie warto trzymać loginów i haseł tylko w bazie aplikacji Spring Boot?
Model z lokalną tabelą użytkowników i sesją HTTP sprawdza się wyłącznie w prostych, monolitycznych aplikacjach. Gdy pojawia się osobny frontend, kilka usług backendowych lub potrzeba SSO, zaczynają się problemy: duplikacja mechanizmów logowania, brak centralnej polityki haseł i MFA, trudniejsze audytowanie dostępu.
Wykorzystanie OAuth2/OIDC przenosi odpowiedzialność za uwierzytelnianie na wyspecjalizowany serwer autoryzacji. Spring Boot staje się klientem, który ufa wydawanym tokenom. Dzięki temu można centralnie zarządzać hasłami, blokadami, wieloskładnikowym uwierzytelnianiem i logami bezpieczeństwa, a kolejne aplikacje dopinać bez ponownego wymyślania logowania.
Jak skonfigurować OAuth2 i OpenID Connect w Spring Boot – klient vs resource server?
W uproszczeniu są dwa podstawowe scenariusze. Gdy aplikacja Spring Boot obsługuje logowanie użytkownika (np. panel admina, backend dla SPA), działa jako klient OAuth2. Wtedy korzysta się ze startera spring-boot-starter-oauth2-client, a w application.yml konfiguruje się dane dostawcy: client-id, client-secret, adresy endpointów itp.
Jeśli aplikacja udostępnia zabezpieczone API i ma jedynie weryfikować przychodzące tokeny, pełni rolę resource server. W tym celu używa się startera spring-boot-starter-oauth2-resource-server i wskazuje adres kluczy publicznych (JWKS) lub endpoint introspekcji. W wielu systemach jedna aplikacja łączy obie role: przyjmuje logowanie użytkownika i jednocześnie wystawia REST API zabezpieczone tym samym IdP.
Którego przepływu OAuth2 użyć w SPA, aplikacji mobilnej i mikrousługach?
Dla SPA (React, Angular, Vue) i aplikacji mobilnych standardem jest Authorization Code Flow z PKCE. Zapewnia on, że tokeny nie „wypływają” w adresach URL, a dodatkowy kod weryfikacyjny (PKCE) utrudnia przechwycenie autoryzacji. Backend Spring Boot zwykle pełni wtedy rolę resource servera i sprawdza ważność tokenów.
W komunikacji serwis–serwis (mikrousługi) stosuje się Client Credentials Flow. Tu nie ma użytkownika końcowego, a tożsamość ma konkretny serwis. Token otrzymany w tym przepływie jest następnie dołączany do wywołań między usługami i weryfikowany po stronie chronionych API.
Czy da się używać jednego IdP dla wielu aplikacji Spring Boot (SSO)?
Tak, to jeden z głównych powodów wdrażania OAuth2/OIDC. Wystarczy, że wszystkie aplikacje Spring Boot zostaną skonfigurowane jako klienci tego samego dostawcy tożsamości (IdP), np. Keycloak, Auth0, Azure AD. Każda ma własne client-id, ale współdzielą ten sam serwer autoryzacji i ten sam mechanizm sesji użytkownika.
Efektem jest single sign-on: użytkownik loguje się raz w IdP, a przechodząc między aplikacjami, nie musi ponownie podawać hasła, ponieważ Spring Security korzysta z tego samego dostawcy i rozpoznaje istniejącą sesję. Przy większej organizacji ułatwia to także centralne zarządzanie rolami i politykami bezpieczeństwa.
Czy warto pisać własny mechanizm logowania zamiast używać OAuth2/OpenID Connect?
Co do zasady nie. Własny mechanizm daje poczucie pełnej kontroli, ale bardzo szybko pojawiają się kwestie, które trudno poprawnie zaimplementować samodzielnie: obsługa rzadkich scenariuszy w przeglądarkach, poprawne zarządzanie cookies, zabezpieczenia przed CSRF, bezpieczne składowanie haseł czy integracja z MFA i IdP firm trzecich.
Standardy OAuth2/OIDC oraz biblioteki takie jak Spring Security są rozwijane, testowane i audytowane przez dużą społeczność. W praktyce lepiej skupić się na poprawnej konfiguracji sprawdzonych komponentów (np. Spring Authorization Server, Keycloak, Auth0, Azure AD), niż utrzymywać własny, trudny do przeglądu i aktualizacji kod bezpieczeństwa.
Jaką rolę pełni Spring Security przy integracji z OAuth2 i OIDC?
Spring Security jest warstwą, która spina Spring Boot z zewnętrznym serwerem autoryzacji. Po stronie klienta zarządza przekierowaniami do IdP, obsługą callbacków, sesją użytkownika i mapowaniem atrybutów z tokenów na obiekt użytkownika i jego uprawnienia. Po stronie resource servera odpowiada za walidację tokenów (np. JWT), sprawdzanie ich podpisu, dat ważności i wymaganych scope’ów.
Dzięki temu konfiguracja sprowadza się zwykle do kilku właściwości i klasy konfiguracyjnej, zamiast pisania całego przepływu ręcznie. W razie zmiany IdP lub dodania nowego przepływu OAuth2 można to przeprowadzić na poziomie konfiguracji, a nie przebudowy logiki bezpieczeństwa w całej aplikacji.






