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ó - Tokenek
Alapvetően az OAuth specifikáció elég rugalmasan kezeli a tokenek fogalmát, annak ellenére, hogy alapvető építőköveknek számítanak annak működésében. Jelenleg két javaslat, illetve ajánlás létezik, melyek működésükben és használatukban is szignifikáns különbségeket mutatnak: az "opaque" és a JWT tokenek. Közös bennük, hogy legalább annyira szigorúan kell bánni velük biztonsági szempontból, mint bármilyen más azonosítási módszerrel (pl.: session azonosítók, cookie-k, stb.), mivel egy rossz kezekbe kerülő tokennel a támadó ugyanazokra a műveletekre képes, mint az általa azonosított felhasználó - amely akár a teljes rendszerre kiterjedő adminisztratív jogosultságokat is jelenthet. A legfontosabb biztonsági irányelv, melyet érdemes betartanunk, bármelyik token típust is használjuk, az a rövid lejárati idő, ami tipikusan maximum néhány óra, de felhasználási céltól függően akár mindössze néhány perc is lehet. Ugyan ezzel nem tudjuk megakadályozni, hogy egy ellopott tokent használjon a támadó, de a felhasználhatósági ideje minimalizálásával csökkenthetjük az általa okozható kár mennyiségét is. A tokenek követésével és bizonyos extra azonosítási adatok hozzárendelésével (például IP cím, eszköz azonosító, geo-lokáció, stb.) egészen hatékonyan lehet csökkenteni a kockázati faktort - ez azonban nem a mai cikkem témája, kanyarodjunk vissza a javasolt token típusokhoz.
Opaque hozzáférési tokenek
Az opaque tokenek nem tartalmaznak semmilyen azonosítási információt a felhasználóról, csupán véletlenszerű karaktersorozatok - a specifikáció szerint kriptográfiailag véletlen karaktersorozatok, ha egészen pontosak szeretnénk lenni, ami azt jelenti, hogy a tokent generáló rendszerben nulla annak esélye, hogy kétszer ugyanaz a karaktersorozat generálódjon. Ez a megkötés nyilvánvaló, hiszen ha két felhasználó ugyanazt a tokent kapja, a rendszer szempontjából az ugyanazt a jogosultságkört jelentené, illetve az egyik felhasználó valójában a másik felhasználó nevében tevékenykedne. De mint azt említettem, ezek a tokenek önmagukban nem tartalmaznak semmilyen információt a felhasználó kilétéről - akkor tehát hogyan azonosítják a felhasználót és annak jogosultságait? Nos, a megoldás amilyen egyszerű, épp annyira teszi körülményesebbé az opaque tokenek használatát. (A továbbiakban a korábbi cikkekben emlegetett "szereplőkre", mint a kliens alkalmazások, Resource és Authorization Serverek, valamint utóbbi endpointjaira fogok hivatkozni, itt most újra nem fogom részletezni a szerepeiket.) Az adott alkalmazások között megfelelő módon megtörténik a token kiállítása, majd a kliens beszélgetni kezd a token audience-ét képező Resource Serverrel. A Resource Server megkapja ezt a tokent és ekkor el kell döntenie, engedi-e a hozzáférést a kért erőforráshoz. Mivel a token nem tartalmaz erre vonatkozó információt, a Resource Server az Authorization Serverhez fordul a tokennel, lekéri annak adatait a /tokeninfo
endpointon. Ennek válasza tartalmazni fogja a token által biztosított scope-ot, a token lejárati idejét, az audience-t, és minden egyéb információt, ami fontos lehet a Resource Server számára a továbbiakban. A válasz alapján így a Resource Server már el tudja dönteni, engedélyezheti-e a hozzáférést.
Sajnos a fenti műveletet minden egyes kérésnél meg kell tenni, lévén állapotmentes alkalmazásokról beszélünk, így a token cache-elése semmilyen formában sem lehetséges, kliens és Resource Server oldalon sem. Ez nyilvánvaló overhead-et jelent a kérések feldolgozásában, hiszek számolni kell plusz egy kéréssel az Authorization Server felé, minden egyes kérés feldolgozása közben. Nyilván hálózaton belüli kommunikáció lehetőségének biztosításával, illetve az Authorization Server oldalon a megfelelő cache-eléssel ez az extra feldolgozási idő jelentős mértékben csökkenthető, azonban továbbra sem elhanyagolható tény. Viszont ennek a megoldásnak van egy nagy előnye is, mégpedig pont azért, mert minden alkalommal, amikor használjuk, ellenőriztetni kell az Authorization Serverrel: egy, bármilyen oknál fogva visszavont token azonnal érvényét veszíti, a visszavonás utáni következő kérésnél az Authorization Server jelezni fogja a Resource Server-nek, hogy a token érvénytelen, utasítsa el a kérést. Így ez a megoldás a tokenek visszavonhatóságát illetően lényegesen biztonságosabb, mint a JWT tokenek, ahol ez a folyamatos újraellenőrzés nem indokolt, bár semmi nem akadályozza meg, hogy megtegyük.
JWT hozzáférési tokenek
A másik lehetőségünk a JWT (JSON Web Token) tokenek használata. Szemben az opaque tokenekkel, ezek önmagukban tartalmazzák a felhasználó azonosítására és jogosultságaira vonatkozó információkat, illetve minden mást is (lejárat, audience, kiállító rendszer azonosítója, stb.). OpenID Connect alapokra építkező authorizációs rendszerek esetén (az OpenID Connect vagy röviden OIDC lényegében az OAuth kiterjesztése) külön ID és access token kiállítása a jellemző, előbbi a kliens számára ad információt a felhasználó kilétéről, de nem mond semmit a jogosultságairól, utóbbi éppen fordítva és a Resource Server számára hasznos. Az egyszerűség kedvéért most az OIDC-féle megkülönböztetést nem vesszük figyelembe, de az alapvetések ugyanazok akkor is, ha külön van ID és hozzáférési tokenünk.
Szóval a JWT maga is egy ipari standard, annak szerkezete jól definiált a specifikációjában, az OAuth nem változtat azon. Így tehát az általános szerkezete a szokásos:
- Kezdődik a header-rel, amely minimum a token típusát és aláírási algoritmusát tartalmazza, opcionális, az authorizáló rendszer számára szükséges extra paraméterek mellett (pl.: a tokent aláíró kulcs ID-ja);
- Ezután jön a payload, mely a token által tárolt azonosítási információkat tartalmazza, úgynevezett "claim"-ek formájában. Erre később még részletesen visszatérünk.
- Végül a token aláírása következik, mely amellett, hogy a token kiállítójának hitelességét bizonyítja, a token tartalmát is verifikálja, mivel bármilyen módosítás a header vagy a payload szekcióban az aláírását érvényességének megszűnésével jár.
- Mindhárom rész egyenként base64 kódolva látható egy elkészített tokenben, azokat pont szimbólummal összefűzve kapjuk a végleges tokent. A header és a payload base64 dekódolás után könnyedén olvasható, mivel eredetileg mindkettő egy-egy titkosítatlan JSON objektum. Emiatt nagyon fontos kiemelni, hogy semmilyen szenzitív adat, különösképpen jelszavak nem tárolhatóak JWT tokenben. Az aláírás természetesen base64 dekódolás után sem egy értelmes szöveg, az az aláíró algoritmustól függő formájú titkosított karaktersorozat, mely a header, a payload és egy privát kulcs kombinálásából készül.
Egy JWT token tehát nagyjából így fog kinézni:
eyJ0eXAiOiJKV1QiLCJra...bGciOiJSUzI1NiJ9.eyJzY29wZSI6InJlYWQ6Y29tbW...YmYiOjE2NjE5NjE0NzB9.IV5GoUU_0EXpCT...Vf7YIBbPik1g
| header | payload | aláírás
Dekódolás után láthatjuk a tartalmát is:
// header
{
"typ": "JWT",
"kid": "test-public-key",
"alg": "RS256"
}
// payload
{
"scope": "read:users write:users",
"sub": "test_application|uid=1234",
"usr": "test@dev.local",
"rol": "USER",
"name": "Test User",
"uid": 1234,
"aud": "application.testapp.test",
"exp": 1661965070,
"jti": "87c30b48-7878-4ddc-aeca-cb4e225732db",
"iat": 1661961470,
"iss": "http://localhost:9085",
"nbf": 1661961470
}
A token "claim"-jei alapvetően tetszőlegesek, ám bizonyos paramétereket a JWT szabvány és/vagy az OAuth specifikáció előír (vagy legalábbis javasol, de igazából nélkülük a token létjogosultsága kérdőjeleződik meg, szóval nem érdemes elhagyni őket) - ezek az alábbiak:
iss
: Issuer, azaz a tokent kiállító rendszer azonosítója, gyakorlatban az Authorization Server URL-je. Ez a claim értelemszerűen azt mutatja meg, mi volt a tokent kiállító és aláíró rendszer.sub
: Subject, a token tulajdonosának azonosítója. Ez tipikusan egy felhasználói azonosító szám, vagy Client Credentials authorizálás esetén a kliens alkalmazás azonosítója.aud
: Audience, annak az API-nak (Resource Server-nek) az azonosítója, amelyet hívni fogunk a tokennel. Tipikusan URI-kat szoktak használni ebben az értékben, például lehet a Resource Server saját címe, vagy egy tetszőleges, URL-szerű érték, ami az adott rendszeren belül egyértelműen azonosítja az adott Resource Server-t.exp
: Expiration, a token lejárati ideje másodperc alapú Unix timestamp formátumban. Fontos, hogy a kommunikációban érintett felek azonos időzóna szerint oldják fel ezt az értéket, így érdemes UTC-re normalizálni a lejárati időt (ez érvényes a következő két paraméterre is).nbf
: Not before, a token ezen időponttól kezdve használható. Segítségével szabályozható, hogy egy adott token mikortól használható fel, így például kiállítható időzített token is, mely csak két adott időpont között érvényes.iat
: Issued at, a token kiállításának időpontja.jti
: JWT ID: a kiállított token egyedi azonosítója. Lehetővé teszi extra tokenkövetés implementálását, illetve egyszer-használatos tokenek felhasználás utáni azonnali visszavonását, függetlenül a lejárati idejüktől.
A fenti paraméterekkel a legtöbb 3rd party illetve saját építésű OAuth authorizáló rendszer meg fog elégedni, de a tokenek kiegészíthetőek (és tipikusan ki is vannak egészítve) további paraméterekkel. Például a fenti paraméterek még semmit nem mondanak arról, hogy a token milyen műveletekhez ad jogosultságot, illetve a felhasználóról sem tudunk meg túl sokat. Tipikusan az alábbiak használhatóak ebből a célból:
scope
vagyscp
: A token által authorizált műveletek azonosítói. Az itt szereplő értékek mindig a célrendszer scope kiosztásától függenek, tehát, hogy az adott Resource Server milyen scope-okat támogat. Ha többet is meg akarunk adni, akkor szóközökkel elválasztva tehetjük meg. A scope-ok a műveletekkel együtt azt is megkötik, milyen adatokhoz férünk hozzá a tokennel. Ha külön van ID és hozzáférési tokenünk, a scope-ok mindenképp utóbbiban kell, hogy jelen legyenek.- Felhasználó azonosítását szolgáló claimek: tipikusan bármi lehet, pl.:
name
,email
,picture
,username
,role
stb. Fontos megjegyezni, hogy ezeket tipikusan inkább a kliens alkalmazásnak szánt ID tokenben érdemes elhelyezni, hiszen a legtöbb esetben a Resource Server-nek már nem lesz szüksége ezekre az adatokra, illetve adatbiztonsági szempontból is problémás lehet, ha a hozzáférési tokenben szerepelnek ezek az értékek. Természetesen mindez mindig a felhasználási céltól függ, teljes 1st party rendszer esetén pedig lehet, hogy ez nem is jelent különösebb problémát.
Token aláírás
A token aláírása egy kritikusan fontos lépés a JWT tokenek kiállítása során, mivel ez bizonyítja, hogy tényleg az arra jogosult rendszer állította ki a tokent, illetve hogy tényleg az abban állított adatokkal. Ha a Resource Server-ek nem várnák el az aláírást, bárki kézzel összerakhatna egy tetszőlegesen paraméterezett tokent, és még ha véletlenszerűen tippelget adott scope-okra is jó eséllyel találhat el akár kritikus jogosultságokat is, főképp úgy, hogy a 3rd party szolgáltatók scope kiosztása tipikusan publikus a külső kliensek integrálhatósága miatt. Így ez mindenképpen egy elengedhetetlen lépés. Opaque tokenek esetén nincs szükség ilyen jellegű aláírásra, mivel a tokent csak a kiállító rendszer tudja kezelni - ha pedig az nem rendelkezik róla semmilyen információval, akkor biztosan egy hamisított tokent használ a kliens.
Az aláírás tipikusan kétféleképpen történhet: szimmetrikus privát kulccsal vagy aszimmetrikus privát-publikus kulcspárral. Az előbbi esetén egyetlen privát kulcsunk, effektíve egy "jelszavunk" van, amit a kiállító rendszer ismer, és ezt használja az aláírás generálásához (a JWT szabvány erre a HMAC titkosítást javasolja). Hátránya, hogy akár a kliens, akár egy Resource Server ha verifikálni szeretné a tokent (amit köteles megtenni), akkor rendelkeznie kell ugyanezzel a kulccsal, tehát a privát aláíró kulcsot meg kell osztani minden érdekelt résztvevővel. Nyilván ez például 3rd party authorizálást használva egyszerűen nem lehetséges, gyakorlatilag nyílt titokká kellene változtatni a privát kulcsot. Ennek megfelelően az OAuth nem is javasolja ezt a megoldást, más esetekben is ritkán van létjogosultsága, bár még tisztán 1st party, saját authorizeres környezetben megoldható, de jobb más megoldás után nézni.
Sokáig keresgélni azonban szerencsére nem kell, mivel az OAuth specifikáció által javasolt aláírási módszer az aszimmetrikus RSA privát-publikus kulcsos titkosítás. Lényege, hogy a token aláírása kizárólag a privát kulccsal lehetséges, mellyel kizárólag az Authorization Server rendelkezik. A token verifikálása azonban a privát kulcs publikus párjával lehetséges, amely aláírásra nem alkalmas, így bárki rendelkezhet a publikus kulccsal, mivel azzal úgysem tud saját tokent készíteni. Így az Authorization Server-ek az előző cikkemben említett /.well-known/jwks
endpointon szokták publikálni a publikus kulcsot, melyet aztán a regisztrált Resource Server-ek le tudnak tölteni és azzal verifikálni a token aláírását. A "legkisebb" támogatott titkosítási algoritmus a 256 bites RSA, de indokolt esetben lehet feljebb is menni. Magát az aláírási műveletet mindenképp érdemes a tokeneket kezelő library-re bízni, amelyet többnyire csak fel kell paraméterezni a szükséges értékekkel.
A Token használata
A kiállított tokent (függetlenül attól, hogy opaque vagy JWT) az OAuth Resource Server-ek a HTTP kérés Authorization
headerjében fogják várni, Bearer
token formában. Amennyiben a token verifikálását rábízzuk az általunk használt frameworkre, az alábbi dolgok biztosan ellenőrizve lesznek minden hívás során (ha pedig manuálisan végezzük az ellenőrzést, ezekről ne feledkezzünk meg):
- A token aláírása: az ellenőrzéshez a Resource Servernek szüksége lesz a publikus kulcsra.
- A token kiállítója: a korábban említett
iss
claim értékének egyeznie kell a Resource Serveren beállított elvárt értékkel. - A token "célközönsége": az elvárt
aud
értéket szintén a Resource Server határozza meg. - A token érvényességi ideje: amennyiben az
exp
érték egy múltbéli időpontot jelez, a token már lejárt, így nem használható.
Érvénytelen aláírás vagy nem az elvárt értékeknek megfelelő claim-ek a kérés azonnali visszautasítását vonják maguk után, HTTP 403 Forbidden
hibakóddal jelezve. Természetesen az aláírás ellenőrzése nem vonatkozik az opaque tokenekre, illetve a claimek is másképp jelennek majd meg, de a szükséges ellenőrzések ugyanúgy elvégezhetőek, sőt, elvégzendőek. Ezeken felül pedig a Resource Server-en múlik, melyik scope milyen erőforráshoz ad hozzáférést az adott Resource Serveren - erről részletesebben a következő cikkben beszélünk majd.
Az Auth0 dokumentációja az OAuth tokenekről
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.