OAuth 2.0 Spring Boot és Spring Security keretrendszerekkel
Használt technológiák: Spring Boot 2, Spring Security 5, Keycloak 14
Frissítve: 2021. június 30.
Bejelentkezés és jogosultságkezelés témakörben az egyik legelterjedt szabvány az OAuth 2.0. Vállalati környezetben nem érdemes saját felhasználókezelést implementálni, hiszen erre már létező kész megoldások vannak, ebből az egyik a nyílt forráskódú Keycloak, amihez könnyen lehet egy Spring Boot alkalmazást integrálni.
Az OAuth egy nyílt szabány erőforrás-hozzáférés kezelésére, vagy ismertebb nevén authorizációra (authorization). Alapja, hogy elválik, hogy a felhasználó mit is akar igénybe venni, és az, hogy hol jelentkezik be. A legismertebb példa erre az, hogy mostanában ha igénybe akarunk venni egy online szolgáltatást, akkor nem feltétlen kell magunkat regisztrálnunk, hanem bejelentkezhetünk a Google vagy Facebook fiókunk használatával is. Ehhez az említett oldalon lehet bejelentkezni, ott megadni a jelszavunkat, majd folytathatjuk az online szolgáltatás oldalán a tevékenységünket. Nagy előnye, hogy egyrészt nem kell annyi jelszót megjegyeznünk, valamint az OAuth kialakítása miatt nem kell a jelszavunk megadni egy harmadik félnek.
Ha valaki most itt abbahagyná az olvasást, hogy az alkalmazásába nem akar sem Facebook, sem Google bejelentkezést, az is olvasson tovább. Ugyanis az OAuth szabvány használatával természetesen saját authorizációt is megvalósíthatunk, sőt, ez a javasolt mód. Ugyanis nagyon hamar eljuthatunk oda, hogy nem csak egy alkalmazást akarunk üzemeltetni, hanem kettőt, és nem szeretnénk, ha a felhasználóinknak több jelszót kelljen megjegyezniük. Ezt Single Sign-On-nak (SSO), egyszeri bejelentkezésnek nevezzük. És ez nem csak a felhasználóinknak jó, hanem a fejlesztőinknek is, hiszen ha kitaláljuk, hogy módosítani szeretnénk az autentikáción vagy authorizáción, például kétfaktoros autentikációt szeretnénk bevezetni, akkor elegendő egy helyen módosítani, nem kell módosítani a többi alkalmazáson. Erre jó példa, hogy a Google különböző szolgáltatásaiba sem kell külön bejelentkeznünk, mint pl. a GMail, YouTube vagy Maps.
OAuth 2.0
Az OAuth 2.0 annyi módosítást hozott, hogy a fejlesztőknek sokkal egyszerűbb dolga van a különböző mechanizmusok megvalósításakor, valamint különböző forgatókönyveket javasol különböző alkalmazásokhoz, úgymint desktop alkalmazásokhoz, klasszikus webes alkalmazásokhoz, csak böngészőben futó kliens oldali webes alkalmazásokhoz, vagy mobil kliens alkalmazásokhoz.
Az OAuth a következő szereplőket definiálja. (Innentől kezdve az érthetőség kedvéért kicsit pongyolább leszek, hiszen az OAuth mindent is támogat, és inkább csak a gyakoribb eseteket részletezném, nem akarom a kivételekkel elbonyolítani a leírást.)
- Resource owner: aki hozzáfér az erőforráshoz, a szolgáltatáshoz, humán esetben a felhasználó (de lehet alkalmazás is)
- Client: a szoftver, ami hozzá akar férni a felhasználó adataihoz
- Authorization Server: ahol a felhasználó adatai tárolva vannak, és ahol be tud lépni
- Resource Server: ahol a felhasználó igénybe veszi az erőforrásokat, a szolgáltatást
Vigyázzunk, nincs meghatározva, hogy ezek külön alkalmazások legyenek, ugyanaz az alkalmazás akár több szerepkört is betölthet.
Az elején meg kell adni a Grant Type-ot, ami megmondja, hogy a további lépések milyen forgatókönyv alapján kerüljenek végrehajtásra. A következő Grant Type-ok vannak:
- Authorization Code: klasszikus mód, ahogy egy webes alkalmazásba lépünk Facebook vagy a Google segítségével
- Implicit: mobil alkalmazások, vagy csak böngészőben futó alkalmazások használják
- Resource Owner Password Credentials: ezt olyan megbízható alkalmazások használják, melyek maguk kérik be a jelszót
- Client Credentials: ebben az esetben nem a felhasználó kerül azonosításra, hanem az alkalmazás önmaga
Fontos fogalom még a token, mely a belépés tényét igazoló információ darabka. A token visszavonható, vagy akár le is járhat.
Most nézzük meg, hogy hogy működik az Authorization Code típus, mely webes alkalmazásoknál javasolt:
- A felhasználó elmegy az alkalmazás oldalára
- Az átirányít a Authorization Serverre (pl. Google vagy Facebook), megadva a saját azonosítóját (client id), hogy hozzá szeretne férni a felhasználó adataihoz
- Az Authorization Serveren a felhasználó bejelentkezik
- Az Authorization Serveren a felhasználó jogosultságot ad az alkalmazásnak, hogy hozzáférjen a felhasználó adataihoz
- Az Authorization Server visszairányítja a felhasználót az alkalmazás oldalára, url paraméterként átadva neki egy úgynevezett authorization code-ot
- Az alkalmazás megkapja az authorization code-ot, és ezt, valamint a saját azonosítóját (client id), a saját, alkalmazáshoz tartozó “jelszavát” (client secret) felhasználva lekéri az Authorization Servertől a felhasználóhoz tartozó tokent
- Az alkalmazás visszakapja a tokent, mely hordozza a felhasználó adatait
Figyeljük meg, hogy a token lekérése csakis védett csatornán mehet, hiszen aki hozzáfér a client secret-hez, az az adott alkalmazás nevében lesz képes eljárni.
Azt is láthatjuk, hogy ahhoz, hogy ez működni tudjon, az Authorization Serveren fel kell venni az alkalmazást, és ott el lesz tárolva annak azonosítója (client id), neve, címe, “jelszava” (client secret) és az alapértelmezett url (Redirect URI or Callback URL), ahova vissza kell irányítani a felhasználót. A bejelentkezésnél ezt az alkalmazás felül is bírálhatja url paraméterben, és akkor máshova fogja a felhasználót visszadobni az Authorization Server.
A következő Grant Type az Implicit, amit pl. mobil alkalmazásoknál használunk. Itt nincs titkos csatorna az alkalmazás és a Authorization Server között, ezért az alkalmazás “jelszava” hozzáférhetővé válna, ezért mást kellett kitalálni.
Ez is átirányítás alapon működik, a különbség csupán annyi, hogy itt a bejelentkezés után a mobil alkalmazás azonnal a tokent kapja meg, ami alapján hozzáfér a felhasználó adataihoz.
A Resource Owner Password Credentials Grant Type csak speciális, megbízható alkalmazások esetén használható. Ebben az esetben ugyanis az alkalmazás maga kéri be a felhasználónevet és a jelszót, ezt továbbítja az Authentication Servernek, ami erre visszaadja a tokent, ami a felhasználó adatait hordozza. Újra kiemelném, itt az alkalmazás maga kéri be a jelszót, semmi sem akadályozza meg, hogy azt el is mentse. Ez publikus weben nem elképzelhető, hiszen a Google jelszavunkat sosem adnánk meg más alkalmazásnak (ugye?), de egy intranetes védett környezetben akár ez is használható.
A Client Credentials Grant Type-nál ahogy említettem az alkalmazás önmagát azonosítja, és önmagával kapcsolatos adminisztrációs feladatokat végezhet az Authorization Serveren.
Beszélni kell még magáról a tokenről is. Ez lehet JSON Web Token (JWT), melyről az előző posztban esett már szó, JSON formátumban önmaga tárolja a felhasználó adatait, ami azért hasznos, mert a bejelentkezés tényét nem kell az alkalmazásban tárolni (pl. sessionben, cache-ben), sem az Authentication Servertől lekérni. Ezáltal állapotmentes alkalmazásokat tudunk készíteni, mely egyszerűsíti a telepítést, üzemeltetést, skálázhatóságot.
Igen ám, de hogyan bizonyosodjunk meg arról, hogy a JWT tokent nem egy támadó állította össze, és küldte be. Erre való a JSON Web Signature (JWS) specifikáció, mely kriptográfiai mechanizmusokat ír le a token integritásának megőrzéséhez. Ez gyakorlatban annyit jelent, hogy az Authorization Server a tokent elektronikusan aláírja. A resource servernek az aláírást ellenőriznie kell, amikor azt megkapja. Ehhez az ellenőrzéshez azonban szükség van az Authorization Server publikus kulcsára, pontosabban az ezt is tartalmazó tanúsítványára. Itt jön a képbe egy újabb szabvány, a JSON Web Key (JWK), mely egy (megintcsak) JSON formátum arra, hogy hogyan lehet az alkalmazás számára a tanúsítványt átadni.
A token alapesetben nem feltétlen titkosított, hiszen a felhasználó a saját adatait ismerheti, viszont más nem férhet hozzá. Hamisítani nem lehet, az aláírás miatt. Azonban ha mégis szükség van a token titkosítására, akkor arra a JSON Web Encryption (JWE) használható.
Spring Security támogatás
És most nézzük meg, hogyan működik ez a gyakorlatban. Egy Spring Boot alkalmazást készítsünk, melyben Spring Security lesz felelős az OAuth 2.0 használatáért. Szándékosan nem Google vagy Facebook integrációt szeretnék bemutatni, hanem saját Authorization Servert szeretnék használni. Az alkalmazás kizárólag REST API interfésszel rendelkezik, nem akarom a példát elbonyolítani felhasználói felület implementálásával. Az alkalmazás letölthető GitHubról.
A Spring berkein belül az OAuth 2.0 támogatás elég megosztott, ugyanis támogatja a
Spring Security OAuth 2.3+ projekt,
valamint kisebb részek megtalálhatóak a Spring Cloud Security 1.2+ és a Spring Boot 1.5.x
projektekben is. Azonban 2018 januárjában bejelentették, hogy a teljes OAuth 2.0 támogatás
a Spring Security 5 projekt keretében lesz megvalósítva. Az 5.2-es verzió már
elég közel került a
teljes megvalósításhoz, azonban a terv fontos vonatkozásban változott, méghozzá a
Spring Security nem fog tartalmazni Authorization Servert. A döntés oka, hogy egy
teljes Authorization Server egy termék, azonban a Spring Security megpróbál egy
keretrendszer maradni, mely a létező library-kat integrálja egy egységes keretbe.
Ezzel együtt a Spring Security OAuth projekt fejlesztését is leállították.
A neten sok példa található ennek használatával, érdemes odafigyelni. Van egy
Migration Guide
is a kettő közötti különbségről. Ha a függőségek között spring-security-oauth2
projektet találunk, vagy @EnableResourceServer
és @EnableAuthorizationServer
annotációkat, biztosak lehetünk benne, hogy a régi Spring Security OAuth
megoldáshoz van dolgunk.
Ezen kívül a Keycloakhoz külön Spring Boot Starter projekt van, ezért még könnyebben integrálható.
És most nézzük a példa projektet! Az implementálásához a következő lépésekre lesz szükségünk:
- Fel kell telepíteni egy Authorization Servert. Ehhez a Keycloakot fogom használni, és azért, hogy ne kelljen a telepítéssel fáradozni, Docker konténerben fogom futtatni.
- Létre kell hozni egy Keycloakon belül egy realmet, mely felhasználók, szerepkörök és csoportok halmaza.
- Létre kell hozni egy klienst, amihez meg kell adni annak azonosítóját, és hogy milyen url-en érhető el
- Létre kell hozni a szerepköröket
- Létre kell hozni egy felhasználót, beállítani a jelszavát, valamint hozzáadni a szerepkört
- Létre kell hozni egy Spring Boot projektet, és felvenni a megfelelő függőségeket
- Létre kell hozni egy REST webszolgáltatást, melyet utána le kell védeni, hogy csak a megfelelő szerepkörrel lehessen elérni
- Beállítani az Authorization Server elérését
Az alkalmazás teszteléséhez a következő lépéseket tesszük:
- Felhasználónév/jelszó megadásával az Authorization Servertől egy tokent kérünk le
- Meghívjuk a webszolgáltatást, a token megadásával, melyet az alkalmazás ellenőriz úgy, hogy a háttérben lekéri az Authorization Server tanúsítványát
Keycloak
Először indítsuk egy a Keycloak szervert Docker konténerben a következő módon:
docker run -e KEYCLOAK_USER=root -e KEYCLOAK_PASSWORD=root -p 8081:8080 --name keycloak jboss/keycloak
A KEYCLOAK_USER
és KEYCLOAK_PASSWORD
környezeti változókkal megadjuk az alapértelmezett
felhasználónevet és jelszót. A -p
kapcsolóval a saját gépen a 8081
-es porton elérhetővé
tesszük a Keycloak webes felületét. (A saját alkalmazásunk fog a 8080
-as porton futni.)
Valamint a keycloak
nevet adjuk a konténernek.
Ekkor a http://localhost:8081
címen az Administration Console linkre kattintva
az előbb megadott felhasználónévvel és jelszóval be tudunk jelentkezni.
A további műveleteket megtekinthetjük itt is screenshotokkal illusztrálva.
Itt válasszuk ki az Add Realm gombot, és töltsük ki a nevet:
Name: JTechLogRealm
Majd vegyük fel a még el nem készült alkalmazásunkat kliensként a Clients menüpontban
a Create
gomb megnyomásával:
Client ID: jtechlog-app
Root URL: http://localhost:8080
Ezzel mondjuk meg, hogy az alkalmazásunk azonosítója (client id) jtechlog-app
, és
elérhető a http://localhost:8080
címen.
Majd a Roles menüpontban az Add Role gomb megnyomásával vegyünk fel egy új szerepkört:
Role Name: jtechlog_user
Végül a Users menüpont alatt vegyünk fel egy felhasználót az Add user gomb megnyomásával:
Username: johndoe
Aa Email Verified legyen On értéken, hogy be lehessen a felhasználóval jelentkezni.
Mentsük el, majd a Credentials fülön adjunk meg egy új jelszót:
Password: johndoe
Password Confirmation: johndoe
A Temporary értéke legyen Off, hogy ne kelljen jelszót módosítani.
Végül a Role Mappings fülön adjuk hozzá az Assigned Roles közé vegyük fel a
jtechlog_user
szerepkört.
Ha ezzel kész is vagyunk, akkor a http://localhost:8081/auth/realms/JTechLogRealm/.well-known/openid-configuration
címen egy JSON-t kapunk, ami tartalmazza a legfontosabb információkat.
Számunkra érdekes lehet a token_endpoint
, amin tokent lehet igényelni, de
kelleni fog a jwks_uri
is, amely címen a tanúsítvány érhető el.
Tokent kérni tehát úgy lehet, hogy a token_endpoint
címre postot küldünk,
a törzsben átadva a grant_type
password
legyen, valamint a client_id
értékét, és a felhasználónevet és jelszót, rendre username
és password
nevekkel.
curl -s --data "grant_type=password&client_id=jtechlog-app&username=johndoe&password=johndoe" http://localhost:8081/auth/realms/JTechLogRealm/protocol/openid-connect/token | jq
A jq egy parancssori eszköz JSON feldolgozására, most itt egyszerűen csak formázunk vele.
Ekkor egy hasonló JSON jön vissza:
{
"access_token": "eyJ...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJ...",
"token_type": "bearer",
"not-before-policy": 0,
"session_state": "ee91d614-f350-4067-b67f-01424a10625e",
"scope": "email profile"
}
Itt természetesen az access_token
és refresh_token
értéke sokkal hosszabb.
Mivel a token nem kódolt, átmásolhatjuk a https://jwt.io/ oldalra, amivel
megtekinthetjük annak tartalmát.
{
"jti": "4b826032-ff77-4ee6-becc-bd4fe5114623",
"exp": 1582149546,
"nbf": 0,
"iat": 1582149246,
"iss": "http://localhost:8081/auth/realms/JTechLogRealm",
"aud": "account",
"sub": "0e25515b-b915-46a9-aad4-12554c309cd5",
"typ": "Bearer",
"azp": "jtechlog-app",
"auth_time": 0,
"session_state": "ee91d614-f350-4067-b67f-01424a10625e",
"acr": "1",
"allowed-origins": [
"http://localhost:8080"
],
"realm_access": {
"roles": [
"jtechlog_user",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"email_verified": false,
"preferred_username": "johndoe"
}
Itt látható pár érdekesség. Először a sub
(Subject) tartalmazza általában a felhasználónevet,
itt ez a Keycloak által kiadott egyedi azonosító, és a felhasználónév a preferred_username
mezőben szerepel. A iat
(Issued at) a kiadás, az exp
(Expiration time) a lejárat dátuma. Az iss
-ben (Issuer) a kiadó,
a azp
-ben (Authorized party) a client id van. A szerepkörök a realm_access
mezőben vannak.
Ezt a kérést természetesen Postmanben is lefuttathatjuk, ha az Authorization fülre kattintunk, ott kiválasztjuk az OAuth 2.0 TYPE-ot, majd megnyomjuk a Get New Access token gombot. Itt töltsük ki a következő mezőket:
Grant Type: Password Credentials
Access Token URL: http://localhost:8081/auth/realms/JTechLogRealm/protocol/openid-connect/token
Username: johndoe
Password: johndoe
Client ID: jtechlog-app
Scope: profile
(A scope-ot nem kitöltve üres stringet küld a Postman, melyet a szerver nem tud értelmezni.)
Majd nyomjuk meg a Request Token gombot, és meg is kapjuk a tokent.
A tanúsítvány a http://localhost:8081/auth/realms/JTechLogRealm/protocol/openid-connect/certs
címen érhető el. Itt egy JSON-t kapunk vissza, melynek x5c
mezője rejti a tanúsítványláncot,
ahol a tanúsítványok a X.509 szabvány formátumban vannak.
Alkalmazás
A következő feladat az alkalmazás elkészítése. Üres Spring Boot projekttel induljunk,
majd vegyük fel a pom.xml
fájlba a <dependencyManagement>
tagbe a következőt:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>14.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Majd a függőségek közé:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
Ahhoz, hogy az alkalmazás még ellenőrizni tudja a tokent, be kell állítani
az application.properties
állományban a Keycloak szerverhez kapcsolódás
tulajdonságait:
keycloak.auth-server-url=http://localhost:8081/auth
keycloak.realm=JTechLogRealm
keycloak.resource=jtechlog-app
keycloak.public-client=true
keycloak.security-constraints[0].authRoles[0]=jtechlog_user
keycloak.security-constraints[0].securityCollections[0].patterns[0]=/*
keycloak.principal-attribute=preferred_username
Az alkalmazást indíthatjuk az mvn spring-boot:run
paranccsal is parancssorból.
Alapesetben ekkor minden url védett, ezért ha meg akarunk hívni egy webszolgáltatást, akkor a következő üzenetet kapjuk:
$curl -s -v http://localhost:8080/api/hello
> GET /api/hello HTTP/1.1
> Host: localhost:8080
>
< HTTP/1.1 401
< WWW-Authenticate: Bearer
A -s
(silent) kapcsolóval nem kérjük a letöltést jelző indikátort, a -v
(verbose) kapcsolóval pedig
nem csak a válasz törzsét, hanem a kérést és a válasz fejlécét is kiíratjuk.
Látható, hogy 401
Unauthorized hibával elutasította a kérést, és az
autentikáció módja Bearer, azaz hordozó tokent kell átadni a kérés
fejlécében.
Ezt a következőképpen tudjuk átadni:
$curl -s -H "Authorization: bearer eyJ..." http://localhost:8080/api/hello | jq
{
"message": "Hello JWT!"
}
Ahol a eyJ...
helyett a teljes token szerepel. Ezt ugyanígy tudjuk Postmanben is használni,
figyeljünk arra, hogy az Access Token beviteli mező értéke fel legyen töltve úgy, hogy az
Available Tokens legördülőből kiválasztunk egyet.
Amikor nem kapunk vissza semmit, akkor a válasz törzse üres, ekkor használjuk a curl
-v
kapcsolóját, hiszen a státuszkódot és a hibaüzenetet a fejlécben fogjuk megtalálni.
Vigyázzunk, mert a tanúsítvány alapesetben hamar lejár.
Ami még érdekes, hogy bármikor újraindíthatjuk az alkalmazásunkat, újra be tudunk a tokennel jelentkezni, mert az alkalmazásunk állapotmentes.
Nézzük meg, hogy lehet hozzáférni a JWT-ben tárolt adatokhoz. Ehhez a Spring Controllerben a
@RequestMapping
annotációval ellátott metódusban egy Principal
paramétert
kell deklarálni.
@GetMapping("/api/hello")
public HelloResponse sayHello(Principal principal) {
log.info("The name of the user: {}", principal.getName());
return new HelloResponse("Hello JWT!");
}