Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > OAuth 2 authorizáció - A framework működése

Szűrő megjelenítése

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 a code 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 az authorize 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 az authorize endpoint esetében. Amennyiben authorization_code grant flow-val authorizálunk, és az authorize 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, az authorize 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 az authorize 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:

alt

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:

  1. 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 a code verifier.
  2. 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 a code_challenge.
  3. 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, a code_challenge_method mezőben, melynek értéke S256 (SHA256 esetén) vagy plain (plain text esetén) lesz.
  4. A hozzáférési token kérés során (token endpoint) a kliens nem küldi el a Client Secret-et, helyette az eredetileg generált code_verifier értéket fogja. A korábban elhashelt értéket és a hashelési metódust már ismeri az Authorization Server, így a kapott code_verifier értéket újrahashelve és a tárolt code_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.

Kommentek

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.