Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
OAuth 2 authorizáció - A kliensek konfigurálása
Először is tisztázzuk, mi is lenne a cél. Miután már rendelkezünk egy OAuth authorization server hozzáféréssel, ahhoz hogy azt használatba vehessük, a klienseket (minden olyan alkalmazást, amit OAuth authorizációval akarunk védeni), regisztrálnunk kell a serveren. Ez a folyamat teljes egészében attól függ, hogy milyen OAuth authorization servert használunk, bár a lényeg nagyjából ugyanaz lesz mindenhol. A kliens az alábbiak egyike lesz:
- Authorization Code Grant Flow alapú UI (frontend) alkalmazás: a regisztrált alkalmazás elindíthat egy Auth Code authorizációs folyamatot, tehát a már korábban említett két-lépcsős authorizálást.
- Client Credentials Grant Flow alapú service alkalmazás: az authorizációs folyamatban nem vesz részt egy valós személy, effektíve két service alkalmazás közötti "privát" csatornát definiálunk.
- Resource Server alkalmazás: egy olyan alkalmazás, amihez hozzáférhetünk egy OAuth authorization server által kiállított tokennel és az valamilyen adatokat tud számunkra biztosítani. Gyakorlatilag az előző két típus túloldalán, a fogadó oldalon álló alkalmazásról beszélünk.
A regisztrációs folyamat lehet manuális, tehát egy adminisztrátor felhasználó kézzel rögzíti a kliens alkalmazás adatait és típusát, illetve sok esetben a CI/CD pipeline-ok integrált lépéseként történik meg, egy konfigurációs file tartalma alapján. Mivel a korábbi cikkeimben a saját authorizációs serveremet mutattam be, azon keresztül fogom most részletezni a lépéseket, de például egy Auth0-val is hasonló paramétereket kell majd megadnunk. A folyamat eredménye egy client ID és secret pár lesz, melyet a kliens alkalmazás használhat önmaga azonosítására. Resource Server regisztráció esetén valószínűleg meg kell adnunk egy audience értéket is, mely ugyan nem kötelező, de az OAuth specifikáció mindenképp javasolja használatát. Ezzel ugyanis a kiállított access tokenre tehetünk egy megszorítást, melyet így már csak olyan alkalmazások fogadhatnak el, amik önmagukat az adott audience-ként azonosítják. Tehát ha a token az XYZ Resource Server részére van kiállítva, ugyanazt a tokent ABC Resource Server nem fogadja el. Az audience érték egyébként a specifikáció szerint egy URL, ami nem kell valós domainre mutasson (tehát pl. a https://my.awesome.service.localhost egy a specifikáció által érvényesnek tekintett audience), a gyakorlatban azonban bármilyen érték megengedett.
Teljesen authorization server provider függő, de akár megadhatunk további megszorításokat is, például megadhatjuk, melyik kliens milyen scope-okat kérhet egy másik alkalmazás elérésére. Tehát például meghatározhatjuk, hogy "A" kliens "B" Resource Servert írási és olvasási kérésekkel is hívhatja, "C" kliens ugyanazt a "B" Resource Servert azonban csak olvasási kérésekkel. Így a jogosultságok nagyon szépen kezelhetővé válnak és biztosak lehetünk abban, hogy adott kliens biztosan csak a számára engedélyezett műveleteket hajtja végre a túlsó félen. Ennyit az elméletről, nézzük a gyakorlatot.
A Resource Server beállítása
Kezdjük a "nehezebb" részével, mivel a Resource Serveren több lépést is meg kell tennünk, hogy megfelelő módon működjön az integráció az Authorization Serverrel:
- Adjuk hozzá a projekthez a szükséges függéseket.
- Be kell állítani, mely endpoint milyen scope-ot vár: ez egy viszonylag hosszadalmas folyamat lehet, ha sok endpointtal rendelkezik az alkalmazás, legalábbis a megfelelő jogosultságok megállapítása tekintetében.
- Engedélyezni kell az alkalmazásban a Resource Serverként való működést: ez effektíve az access token kérésenkénti feldolgozását illetve az OAuth Authorization Serverrel való verifikálását takarja.
Spring esetén az alábbi függésekre lesz szükségünk:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- az alábbi csak akkor kell, ha JWT tokeneket használunk -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
A Spring (pontosabban a Spring Boot) egészen rugalmasan áll ehhez a témához, igazából kétféle módon is beállíthatjuk a scope-okat az endpointokra. Az egyik opció, mellyel egyúttal a Resource Server módot is engedélyezzük, egy SecurityFilterChain
bean létrehozása:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests()
// ez az endpoint public lesz
.regexMatchers(HttpMethod.GET, "/translations\\?packs=.+$")
.permitAll()
// erre az endpointra a read:translations scope-ot várjuk ...
.mvcMatchers(HttpMethod.GET, "/translations/**")
.hasAuthority("SCOPE_read:translations")
// ... erre pedig a write:translations scope-ot
.mvcMatchers("/translations/**")
.hasAuthority("SCOPE_write:translations")
.and()
// végül bekapcsoljuk a Resource Server módot (vagy ::opaqueToken ha nem JWT access tokent használunk)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// ... további beállítások
.build();
}
}
Nagyon fontos megjegyezni, hogy a Spring Security a scope-ok meghatározására a .hasAuthority
matchert használja, ami mindenképp SCOPE_
prefixszel várja a scope neveket. Enélkül ROLE_
prefixet rendel hozzájuk automatikusan, ami OAuth környezetben nem jellemzően használt (és nem is fog ebben az esetben működni).
A másik lehetőség Spring Boot esetében a @PreAuthorize
annotációk használata. Egy konfigurációs osztályon előbb el kell helyeznünk az @EnableGlobalMethodSecurity(prePostEnabled = true)
annotációt, majd bármilyen Spring-managed bean osztály metódusain elhelyezhetünk egy-egy @PreAuthorize
annotációt, például:
@PreAuthorize("isAuthenticated() && hasAuthority('SCOPE_write:entries')")
Ez gyakorlatilag kiváltja az endpoint szintű authorizálást, valamivel jobban szemcsézett authorizációs követelményeket támasztva, bár vitathatatlan hátránya, hogy a security konfiguráció így nem lesz központozott.
Az utolsó lépésként az application.yml
-ben meg kell adnunk az alábbi pár sort és a service ezzel készen áll a Resource Serverként való működésre:
spring:
security:
oauth2:
resourceserver:
# JWT access tokenek esetén, opaque tokenekkel picit másabb paraméterekre lesz szükség
jwt:
# az Authorization Server címe, egyben pontosan ugyanez az érték kell szerepeljen a kiállított token issuer fieldjében
issuer-uri: http://my-authorization-server.localhost
# a Resource Server audience értéke, tehát ahogy a service önmagát azonosítja
# ez egy opcionális érték, de ha a token tartalmaz audience értéket, kötelező megadni itt is
audiences: https://my.awesome.service.localhost
Hasonló módon tudunk beállítani egy Node.js-ben írt alkalmazást mint Resource Servert. A példában egy Express alapú, TypeScriptben írt alkalmazás konfigurációját láthatjuk majd. Először is, szükségünk lesz az express-oauth2-jwt-bearer
nevű függésre. Az authorizáció bekapcsolására egy middleware áll rendelkezésünkre, mely az issuert és az audiencet várja paraméterül:
auth({
issuerBaseURL: "http://my-authorization-server.localhost",
audience: "https://my.awesome.service.localhost"
})
Az elvárt scope-ot pedig a requiredScope
függvény segítségével adhatjuk meg, ami szintén egy middlewaret ad, paramétere pedig "tisztán" a scope, például write:entries
. Fontos megjegyezni, hogy az auth
middlewaret globális middlewareként bekötve minden endpoint védetté válik, függetlenül attól, hogy használjuk-e a requiredScope
-ot vagy sem, így ha vannak publikus endpointjaink is, az auth
-ot kénytelenek leszünk endpointonként bekötni. Ez egy elég bosszantó hiányossága ennek a librarynek, amit egyébként maga az Auth0 publikált.
A Client Credentials kliensek beállítása
Most, hogy már van, ami fogadja az access tokent, ideje nekiállni beállítani a klienseket - kezdésképpen egy Client Credentials Grant Flow kommunikációt nézünk meg. Ha Springet használunk, viszonylag egyszerű a dolgunk. Mivel a kliens egy tokent fog majd generálni egy service elérésére, a kliensnek effektíve be kell jelentkeznie az Authorization Serveren. Ezért meg kell adnunk a client ID és secret párt, a scope-ot amire generálni szeretnénk a tokent, illetve opcionálisan az audience-t. A konfiguráció nagyjából így fog kinézni:
spring:
security:
oauth2:
client:
registration:
my-awesome-service:
authorization-grant-type: client_credentials
client-id: my-client-id
client-secret: my-client-secret
provider: my-awesome-service-provider
scope:
- write:everything
- read:everything
provider:
my-awesome-service-provider:
token-uri: http://my-authorization-server.localhost/oauth/token?audience=https://my.awesome.service.localhost
issuer-uri: http://my-authorization-server.localhost
Bontsuk szét a fenti konfigurációt. Először is regisztrálunk egy OAuth klienst, aminek a grant type-ja client_credentials
. Megadjuk a kliens alkalmazás regisztrált azonosítóját és "jelszavát", illetve a scope-okat amire generáljuk a tokent. Providerre akkor lesz szükségünk, ha nem egy standard támogatott OAuth servicehez akarunk csatlakozni (pl. Facebook, GitHub, Google, stb.). A provider regisztrációban a token URI részeként megadjuk az audience-t, illetve beállítjuk az issuer címét, amit itt a Spring arra fog használni, hogy effektíve meg tudja hívni az Authorization Servert. Ezzel gyakorlatilag már készen áll a kliens alkalmazás a tokenek generáltatására és vele együtt az OAuth védett Resource Serverek meghívására. A hogyanra még visszatérünk.
Az Authorization Code kliensek beállítása
Nos ez viszonylag rövid magyarázat lesz: az authorization-grant-type
értékét állítsuk authorization_code
-ra és készen vagyunk, a többit a Spring intézi. Egy teljesen háttérben történő beszélgetés helyett azonban ilyenkor a Spring át fogja irányítani a felhasználót az Authorization Server "authorize" útvonalára, ahol a felhasználó engedélyezheti vagy elutasíthatja a hozzáférést. Nyilván ebből az okból fontos megjegyezni, hogy service-to-service kommunikációra nem használható az Authorization Code Grant Flow, továbbá JS front alkalmazás esetén az Authorization Code Grant Flow PKCE változatát kell használni, mivel a client secret ellenkező esetben megjelenne a publikusan elérhető forráskódban.
OAuth REST kliensek Spring Boot alatt
Az utolsó lépés, hogy ténylegesen elkezdjük hívni az OAuth védett Resource Servereket. A Spring már csak a WebClient-hez ad standard támogatást, ám ahhoz is némi manuális konfiguráció szükséges. Én egy másik utat választottam, azonban van egy kiváló Baeldung cikk a WebClient használatáról, a cikk végén mellékelem. Az általam választott megoldás egyébként a WebClient módszerén alapszik, csak bármilyen másfajta REST kliens implementációval működik. A lényege, hogy a Spring OAuth2AuthorizedClientManager
osztálya segítségével tudunk kérni egy OAuth2AuthorizedClient
objektumot, amiben lesz egy OAuth2AccessToken
. Ez az objektum tartalmazza magát az access tokent, ráadásul amennyiben már az lejárat közelében van, a Spring kér egy újat, mielőtt visszaadná azt a hívó félnek. Így mindig rendelkezésünkre áll egy érvényes access token, amit a REST kliens el tud helyezni az Authorization
headerben.
Az implementáció részletes leírása már némiképp túllépné a cikk eredeti célját, de alább mellékelek egy linket a forráskódra - illetve egy későbbi cikkben még lehet visszatérünk rá.
Egyedi REST kliens integráció Spring OAuth támogatással
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.