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 framework működése
Egy OAuth authorizációs szerver több különböző, felhasználói interfésszel rendelkező vagy REST-szerű endpointot biztosít az authorizációs folyamatok elvégzésére. Bizonyos esetekben egyetlen authorizáció több ilyen endpoint meghívását igényli, ami attól függ, milyen "grant flow" mentén történik az authorizáció. Először is nézzük meg a kérdéses endpointokat és azok funkcióját.
Endpointok
Az authorize
endpoint
Tipikusan a GET /oauth/authorize
útvonalon elhelyezkedő endpoint, melynek grafikus felhasználói interfésze van. Ez az endpoint mindössze információt szolgáltat a bejelentkezett felhasználó kilétéről, illetve az authorizációt indító alkalmazás által, a felhasználó nevében kért jogosultságokról. Amennyiben egy 3rd party rendszeren jelentkezünk be ilyen módon, tipikusan azt fogjuk látni, hogy az adott 3rd party hozzáférést kér az email címünkhöz, nevünkhöz, felhasználói profilképünkhöz, továbbá gombok lesznek itt az engedélyezésre és az elutasításra. Ez a 3 tényező nagyon fontos, a specifikáció szerint minden authorizer rendszer, amely az OAuth 2.0 specifikációnak meg akar felelni, köteles a fentebb említett információkat megjeleníteni és a felhasználónak lehetőséget biztosítani a beleegyezésre vagy elutasításra. Utóbbi természetesen a folyamat megszakítását eredményezi.
A kezdeményező alkalmazás köteles bizonyos információkat elküldeni magáról, amelyek a következők:
- A saját kliens azonosítója (
client_id
): az authorizációs szerver ez alapján ismeri fel a regisztrált alkalmazást. - Az authorizáció módja (
response_type
): tipikusan acode
típust szokás használni, ez egy teljes "Authorization Code Grant Flow" authorizációs folyamatot indít, melynek eredménye egy "authorization code" lesz, amit a szerver visszaküld a kezdeményező alkalmazásnak sikeres authorizáció esetén. - Az authorizációs válasz módja (
response_mode
): Opcionális paraméter, alapértelmezetten query string-ben (query
) adja át az authorization code-ot a kezdeményező alkalmazásnak a szerver, de ez megváltoztató például űrlap alapúra (form_post
). - A visszairányítási útvonal (
redirect_uri
): az az URL, ahova az authorizációs szerver visszaküldi a böngészőt sikeres authorizáció esetén. A kliens alkalmazás regisztrációja során ez is egy kötelezően megadandó paraméter, így elkerülhető, hogy nem regisztrált alkalmazások próbáljanak az adott authorizáló rendszertől hozzáférést kérni (mivel még ha egy regisztrált alkalmazás kliens azonosítóját használják, akkor sem tudják az authorizációs szervert más helyre visszairányítani). - Igényelt jogosultságok (
scope
): a kezdeményező alkalmazás által igényelt felhasználói jogosultságok. - Végül egy véletlen karaktersorozatot is át kell adnia a kliensnek a
state
paraméterben: ez a további kommunikáció során a CSRF token szerepét tölti be.
Engedélyezés esetén az authorizációs szerver kiállítja a korábban említett "authorization code"-ot, melyet visszaküld a kezdeményező alkalmazásnak, az eredeti state
érték társaságában. Ez egy tipikusan nagyon rövid lejáratú (max 1-2 perc), véletlen, az authorizációs szerver által átmenetileg tárolt karaktersorozat, mellyel a kliens a következő lépésben egy hozzáférési tokent igényelhet. A kód a felhasználása után azonnal érvényét veszíti és kizárólag a hozzáférési token igénylésére alkalmas.
Egy kérés-válasz pár tehát nagyjából így fog kinézni:
GET /oauth/authorize
?response_type=code
&client_id=clnt1
&redirect_uri=http://localhost:9988/callback
&state=ab45-...-99cf
HTTP/1.1 302 Found
Location: http://localhost:9988/callback
?code=7766-...-eb41
&state=ab45-...-99cf
A token
endpoint
A második legfontosabb endpointja minden OAuth authorizációs szervernek, ezen az endpointon (tipikusan a POST /oauth/token
útvonalon) történik a hozzáférési token igénylése. Ez a lépés, illetve az endpoint számára biztosítandó paraméterek, nagyban függenek az éppen használt grant flow-tól. Az általában szükséges paraméterek a következők (fontos megjegyezni, hogy minden alább felsorolt paramétert form paraméterként kell küldeni):
- A használt grant flow azonosítója (
grant_type
): a framework (és az adott authorizációs szerver) által támogatott grant flow-k egyike. Értékei lehetnek:authorization_code
,client_credentials
,password
,refresh_token
, stb. A grant flow típus egyben meg fogja határozni azt is, hogy milyen további paramétereket vagyunk kötelesek megadni az alapokon túl. - A kezdeményező alkalmazás kliens azonosítója (
client_id
): ugyanaz, mint azauthorize
endpoint esetében. - A kezdeményező alkalmazás kliens "jelszava" (
client_secret
): token igénylésnél a kliens minden esetben köteles megbízható módon azonosítani magát. Ez a legtöbb esetben a kliens azonosító és jelszó párral lehetséges, kivétel csupán a PKCE kiegészítéssel működő Authorization Code Grant Flow esetén van - erre még később visszatérünk röviden. - Igényelt jogosultságok (
scope
): ugyanaz, mint azauthorize
endpoint esetében. Amennyibenauthorization_code
grant flow-val authorizálunk, és azauthorize
endpointon megadtunk egy scope értéket, ugyanazt kell itt is használnunk. - "Hallgatóság", azaz
audience
: ez sok esetben opcionális, de érdemes megadni, mivel leszűkíti a token felhasználhatóságát. Lényegében annak a cél alkalmazásnak egy saját speciális, URI-szerű azonosítója, amely a Resource Server szerepét tölti be a kapcsolat során.
A további paraméterek grant flow függően jelennek meg a token kérésben:
- Authorization code (
code
): amennyiben Authorization Code Grant Flow-val authorizálunk, azauthorize
lépésben kapott authorization code értékét el kell küldenünk a token endpointnak. Ezzel fogja az authorizációs szerver azonosítani a folyamatban levő authorizációs kérést, tehát ezen a ponton már ismerni fogja a rendszer a felhasználót is, akinek a nevében a kezdeményező alkalmazás authorizál. - Szintén Authorization Code Grant Flow esetén meg kell adni az eredeti visszairányítási útvonalat (
redirect_uri
). Ez egy extra biztonsági megszorítás, lényegében ugyanazt az azonosítást szolgálja, mint azauthorize
endpoint esetében a kliens azonosítóval párosítva. - Resource Owner (Password) Grant Flow esetén a felhasználó saját felhasználónevére (
username
) és jelszavára (password
) is szükség lesz.
A token kérés eredménye a hozzáférési token, opcionálisan egy úgynevezett ID token társaságában (ez utóbbi csak felhasználói információkat tartalmaz, hozzáférésre nem használható), illetve szintén opcionálisan egy refresh tokennel együtt, amely az automatikus megújítást teszi lehetővé a token lejárata esetén. A válasz továbbá tartalmazza a token típusát (ami a legtöbb esetben Bearer
, tehát az Authorization
headerben használható Bearer
tokenként), valamit a hátralevő lejárati időt másodpercekben.
A token endpoint esetében egy tipikus kérés-válasz pár az alábbiakhoz lesz hasonló:
POST /oauth/token
Authorization: Basic <client ID és jelszó>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=clnt1
&client_secret=secret1234
&audience=target.client.1
&scope=read%3Aall%20write%3Aall
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "jwt-vagy-opaque-token-string",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:all write:all"
}
A két fenti endpoint a legfontosabb az authorizációs folyamatok szempontjából, de még van néhány további, amiket fontos megemlíteni.
A userinfo
endpoint
Egy korábban kiállított access token segítségével, a kezdeményező alkalmazás lekérdezhet bizonyos információkat a token által authorizált felhasználóról. Erre főképp azért lehet szükség, hogy a felhasználó számára meg tudjuk jeleníteni a nevét, email címét, vagy bármely más személyes adatát az alkalmazás felületén. Nincs szabványosított útvonala, de tipikusan a /oauth/userinfo
vagy szimplán a /userinfo
útvonalra kerül. Az endpointot maga a kiállított access token védi, így egy tipikus kérés-válasz pár nagyjából ilyen lesz:
GET /oauth/userinfo
Authorization: Bearer jwt-vagy-opaque-token-string
HTTP/1.1 200 OK
Content-Type: application/json
{
"sub": "1",
"name": "Some User",
"email": "some-user@dev.local"
}
A fenti paraméterek például kifejezetten fontosak Spring integráció esetén, mivel a sub
paraméterbe a felhasználó adatbázisbeli ID-ja kerül, míg a name
és az email
értelemszerűen a felhasználó nevét és email címét tartalmazza. Ezen paramétereket egyrészt az integráció által kezelt hozzáférési tokenek és felhasználói munkafolyamatok összerendelésére, másrészt pedig a felhasználó számára történő visszajelzések céljából használja.
Az introspect
endpoint
Célja hasonló a userinfo
endpointhoz, de ez az endpoint magáról a tokenről tud információt biztosítani a kezdeményező alkalmazás számára. Legfontosabb funkciója a token érvényességének visszajelzése. Azonosítania itt is a kliensnek kell magát, a token egy form paraméter lesz. Szabványosított útvonalról itt sem beszélhetünk, az /oauth/introspect
vagy /oauth/token_info
endpointok tipikusan jó választások. JWT tokenek esetén erre az endpointra alapvetően nincs szükség, a token maga tartalmaz minden fontos információt, beleértve a lejáratát is. Egy tipikus kérés-válasz pár nagyjából ilyesmi lesz:
POST /oauth/introspect
Authorization: Basic <base64-kódolt-kliens-azonosító-és-jelszó>
token=opaque-token-string
HTTP/1.1 200 OK
Content-Type: application/json
{
"active": true,
"scope": "read:all write:all",
"client_id": "clnt1",
"username": "some-user@dev.local",
"exp": 1659019776
}
A "well-known" endpointok
Publikusan elérhető metainformációs endpointok. A két legfontosabb az automatikus server discovery-t lehetővé tevő /.well-known/oauth-authorization-server
endpoint és amennyiben a szerver aszimmetrikusan aláírt JWT tokeneket használ, a /.well-known/jwks
endpoint, ahol a publikus RSA kulcs érhető el, JWK formátumban. Előbbi útvonala Spring integráció esetén nagyon fontos, az integráció mindenképp itt keresi a szerver metainformációkat, utóbbi endpoint azonban szintén tetszőleges útvonalra tehető. Mindkettő publikusan elérhető, tehát nem igényelnek authentikált kérést.
A kérdés-válaszok tipikusan így néznek ki:
GET /.well-known/oauth-authorization-server
HTTP/1.1 200 OK
Content-Type: application/json
{
"authorization_endpoint": "http://localhost:8081/oauth/authorize",
"token_endpoint": "http://localhost:8081/oauth/token",
"jwks_uri": "http://localhost:8081/.well-known/jwks",
"token_introspection_endpoint": "http://localhost:8081/oauth/introspect",
"userinfo_endpoint": "http://localhost:8081/oauth/userinfo",
"grant_types_supported": [
"authorization_code",
"client_credentials",
"password"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"response_types_supported": [
"code"
],
}
Teljesen az adott OAuth authorizer rendszertől függ, hogy ez az endpoint milyen választ fog adni, a fenti példától akár jelentősen el is térhet, több vagy kevesebb paramétere is lehet. Saját authorizer implementáció esetén, amennyiben Spring OAuth támogatással konfigurált Resource Server-eket és klienseket akarunk használni, a fenti példa már (tesztelten) elegendő - az integráció így már sikeresen konfigurálta magát és elkezdte kiállítani a tokeneket.
A publikus kulcsok elérése, folytatva a fenti példát, az alábbi kérés-válasz páron keresztül lesz lehetséges:
GET /.well-known/jwks
HTTP/1.1 200 OK
Content-Type: application/json
{
"keys": [
{
"kty": "RSA",
"e": "<kulcs-exponenciális-érték>",
"use": "sig",
"kid": "<kulcs-azonosító>",
"alg": "RS256",
"n": "<kulcs-modulus-érték>"
}
]
}
A Resource Server-ek a fenti endpointot fogják keresni a hozzáférési token aláírásának ellenőrzése céljából. Enélkül az aláírás ellenőrzése nem lehetséges, így minden kérést HTTP 403 státusszal fog díjazni a Resource Server.
Authorizálási folyamat
Általánosságban az authorizálás az alábbi módon történik:
Felhasználói interakciót igénylő authorizálásnál (ez tipikusan az Authorization Code Grant Flow-t jelenti) a felhasználó előbb az alkalmazás feljogosítását lehetővé tevő felületre kerül - ahogy korábban említettem, ez az authorize
endpoint szerepe lesz. Amennyiben a felhasználó nincs ezen az Authorization Server-en bejelentkezve, úgy a beléptetést is elvégzi a rendszer - tipikusan felhasználónév és jelszó segítségével. Fontos megjegyezni, hogy a felhasználói fiók lokális erre az authorizerre - egy példával élve, tegyük fel StackOverFlow-n szeretnénk bejelentkezni a Google fiókunkkal. A StackOverFlow rendszerében a fiókunk nem létezik, az a Google rendszerében van jelen, tehát a Google Authorization Server-ére lokális. Az Authorization Code Grant Flow alapú beléptetésnél sem változik a helyzet, bár a belépés után létrejön a StackOverFlow-nál egy "fantom" fiók - valójában ennek semmi köze a Google fiókunkhoz, azon túl, hogy a Google fiókunkban megadott nevünk, email címünk, profilképünk, stb. fog majd ott is megjelenni, viszont a jelszavunk nem kerül át a StackOverFlow-hoz, és a továbbiakban is ha újra be akarunk lépni, megint a Google fiókunkkal tehetjük meg. Ez egy nagyon fontos alapvetése az OAuth-nak: a fiókot egy "központi" rendszer kezeli, az abban létező fiók adatai nem kerülnek át 3rd party rendszerekhez, a beléptetés sem közvetlenül náluk történik.
Sikeres authorizálás esetén az Authorization Server visszairányítja a felhasználót a kezdeményező alkalmazásra, ami a háttérben kicseréli a kapott authorization code-ot egy hozzáférési tokenre a token
endpoint meghívásával. Client Credentials és Resource Owner (Password) Grant Flow esetén csak ez a lépés történik meg, illetve itt nincs authorization code sem (mivel nincs authorize
endpoint hívás, tehát nem állít ki a szerver authorization code-ot). Bármely grant flow-t is tekintjük, a végeredmény mindig egy hozzáférési token lesz, mely segítségével a továbbiakban történik majd a védett kommunikáció a kliens alkalmazás és minden olyan Resource Server között, melyhez a token hozzáférést biztosít - mindaddig míg az le nem jár.
Hogyan válasszuk meg a grant flowt?
Pár egyszerű "tény" figyelembe vételével nagyon könnyen megállapítható, melyik grant flow szükséges számunkra.
- Amennyiben az alkalmazásunkban felhasználói fiókokat szeretnénk kezelni, de a felhasználói authentikálást és authorizálást külső rendszerre szeretnénk bízni (Google, Facebook, GitHub, stb.), az Authorization Code Grant Flow lesz a megfelelő választás.
- Amennyiben két service közötti háttérfolyamatokat szeretnénk biztosítani tokenekkel, úgy a Client Credentials Grant Flow-ra lesz szükségünk.
- A Resource Owner (Password) Grant Flow lényegében a fenti kettő keveréke, annyi különbséggel, hogy a felhasználó jelszavát is a 3rd party rendszer kell, hogy elküldje az Authorization Server-nek: ez - ha csak nem egy nagyon szigorúan zárt rendszerben dolgozunk, egy belső üzemeltetésű Authorization Server-rel - egy nagyon rossz modell és bár része az OAuth specifikációnak, de használata nem ajánlott.
- Tipikusan okos eszközök esetén válik hasznossá a Device Grant Flow.
Mi a helyzet a JavaScript frontend alkalmazásokkal?
Amennyiben a kezdeményező (kliens) alkalmazás egy tisztán kliens oldali JavaScript alkalmazás (tehát nincs a szerveren futó backendje), úgy van egy "kisebb" problémánk. Mint azt korábban említettem, a kliens alkalmazás minden esetben köteles azonosítani magát (és Authorization Code valamint Resource Owner Grant Flow-k esetén még ezen felül történik a felhasználói azonosítása). A tisztán JS alkalmazások esetén azonban ez azt jelentené, hogy a kliens alkalmazás saját jelszavát (a Client Secret-et) ki kell lógatni a böngészőben futó (így publikusan is olvasható) forráskódba. Természetesen ez egy elfogadhatatlan biztonsági réssel lenne egyenértékű. Erre nyújt megoldást az Authorization Code Grant Flow PKCE (Proof Key for Code Exchange) kiegészítése.
Az Authorization Code Grant Flow némiképp megváltozik ebben az esetben:
- Az első lépésben (
authorize
endpoint) a kliens köteles generálni egy kriptográfiailag véletlenszerű karaktersorozatot, majd ezt átmenetileg (kliens oldalon) eltárolnia. Ez lesz acode verifier
. - Még ugyanitt, a kliens a
code_verifier
értéket elhasheli (a specifikáció szerint SHA256 a javasolt, indokolt esetben lehet "plain text" is, de az nem javasolt), majd Base64 kódolja - az így keletkező érték lesz acode_challenge
. - Az authorizációs kérésben elküldi az Authorization Server-nek a
code_challenge
értéket, illetve a használt hashelési metódust, acode_challenge_method
mezőben, melynek értékeS256
(SHA256 esetén) vagyplain
(plain text esetén) lesz. - A hozzáférési token kérés során (
token
endpoint) a kliens nem küldi el aClient Secret
-et, helyette az eredetileg generáltcode_verifier
értéket fogja. A korábban elhashelt értéket és a hashelési metódust már ismeri az Authorization Server, így a kapottcode_verifier
értéket újrahashelve és a tároltcode_challenge
értékkel összevetve a szerver meg tudja állapítani, hogy valóban még mindig az eredeti kezdeményező klienssel beszélget, tehát a hozzáférési token kiállítható.
Konklúzió
A mai cikkben megnéztük, milyen endpointokkal fog alkalmazásunk kommunikálni, ha OAuth authorizációs rendszerrel integráljuk a sajátunkat (illetve ugyanez egy ízelítőnek is megfelelt, ha saját Authorization Server-t írnánk), illetve hogy néz ki az authorizációs folyamat. Azonban még mindig bőven van miről beszélni, így a következő cikkekben a JWT tokenek szerkezetéről, aláírásáról és verifikálásáról lesz szó, valamint később megnézzük, hogyan lehet beállítani a kliens alkalmazásokat és a Resource Server-eket a Spring OAuth támogatásával, továbbá megnézzük, nagyjából hogyan néz ki egy saját építésű OAuth Authorization Server.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.