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
JWT alapú authentikálás III - Jelszó helyreállítása
A klasszikus jelszó-helyreállítási folyamat többnyire két jól elkülöníthető lépésből áll. Az első lépésben a rendszer bekéri a felhasználó valamilyen egyedi azonosítóját. Rendszertől függ, de többnyire a felhasználó regisztrált email címe lesz a kért egyedi azonosító - ennek persze kettős szerepe van. Egyrészt ez egy validálási lépés: a felhasználó rendelkezik egyáltalán fiókkal? Ha a megadott cím nem található az adatbázisban, az egyértelmű jel a rendszer és a felhasználó számára is: itt valami óriási félreértés történik. Ha azonban a cím létezik, a rendszer egy értesítést küld a megadott címre, mellyel folytatható a helyreállítási folyamat. A kapott hivatkozás többnyire egy temporális azonosítót tartalmaz, mellyel a felhasználó átmenetileg jogot kap jelszavának megváltoztatására. Ekkor már a helyreállítási folyamat második lépéséről beszélünk, amikor is a felhasználó egy űrlapon megadhatja új jelszavát, és végre újra bejelentkezhet.
Azonban itt el is értünk a folyamat legkevésbé sem biztonságos pontjához. Természetesen a folyamat minden esetben bízik annyira a felhasználóban, hogy nem hagyta bejelentkezve egy publikus helyen az email fiókját - ez nagyjából egy megkerülhetetlen probléma, a felhasználó figyelmetlensége minden authentikálást igénylő rendszer gyenge pontja. Az igazi problémát azonban sokkal inkább a kiküldött azonosító okozza, mely authentikálatlan felhasználó számára enged írási hozzáférést, ráadásul teszi ezt annak érdekében, hogy a felhasználó jelszót tudjon váltani a korábbi jelszavának ismerete hiányában. A felvetülő biztonsági probléma legalább annyira kacifántos, mint az előző mondatom. Az azonosító megfelelő követése és kezelése hiányában akár illetéktelen jelszó-csere is megtörténhet.
Ennek elkerülése érdekében a helyreállító azonosító fontos, hogy legyen
- követhető: A rendszernek pontosan tudnia kell, melyik felhasználónak osztotta ki a helyreállításhoz szükséges azonosítót.
- önmagától elévülő: Az azonosító kiosztása után azt a lehető leghamarabb “fel kell használni”, nem szabad sokáig nyitva tartani ezt a szándékos biztonsági rést.
- egyszer használható: Egy és csak egy alkalommal lehessen felhasználni az azonosítót.
A Leafletben található implementáció a fenti három kritériumot a JWT tokenek követési lehetőségével éri el.
A folyamat kezdetén, tehát az email cím megadása után, annak sikeres validálása esetén a felhasználó adataiból egy token kerül kigenerálásra. Ez a token ugyanúgy viselkedik, mint az összes többi, tehát ugyanúgy a session store-ba kerül, azonban két apró dologban különböznek. Egyrészt a lejárati idejük mindössze egy óra, tehát a kiállítástól számított egy órán belül fel kell őket használni. A másik különbség, hogy a tokenben tárolt felhasználói szerepkör a felhasználó eredeti szerepkörétől függetlenül minden esetben a RECLAIM
értéket veszi fel. Természetesen a token aláírása miatt a szerepkör ugyanúgy nem módosítható, mint bármely más a tokenben tárolt érték, tehát ezzel az egyébként érvényes tokennel a felhasználó a rendszernek csak azon végpontjait éri el, melyek elfogadják a RECLAIM
szerepkört. Ez gyakorlatilag csak egy esetben igaz, mégpedig a jelszóhelyreállítást feldolgozó végponton. Így tehát a felhasználó csak ezt a végpontot használhatja ezen token birtokában. A követés szempontjából egyébként fontos megemlíteni, hogy mivel a token az összes többi tokennel megegyező validáláson esik át, a token csak azon kliens és IP cím alól használható, ahol a jelszóhelyreállítás kezdeményezve lett.
@Override
public void demandPasswordReset(LoginContextVO loginContextVO) {
// a felhasználót megkeressük az adatbázisban
// ha a felhasználó nem található, ez a kérés kivételt dob
UserDetails userDetails = userDetailsService
.loadUserByUsername(loginContextVO.getUsername());
// itt lesz a felhasználó szerepköre kicserélve
UserDetails reclaimUserDetails = generateReclaimUserDetails(userDetails);
// majd legeneráljuk az 1 órás lejáratú tokent ...
JWTAuthenticationAnswerModel authenticationAnswerModel = jwtComponent
.generateToken(reclaimUserDetails, RECLAIM_TOKEN_EXPIRATION_IN_HOURS);
// ... és tároljuk azt a Session Store-ban
storeToken(loginContextVO, authenticationAnswerModel);
// majd értesítjük a felhasználót, illetve elküldjük neki a tokent
notificationService.passwordResetRequested(PasswordResetRequest.getBuilder()
.withParticipant(loginContextVO.getUsername())
.withUsername(((ExtendedUserDetails) reclaimUserDetails).getName())
.withElevated(isElevatedUser(userDetails))
.withExpiration(RECLAIM_TOKEN_EXPIRATION_IN_HOURS)
.withToken(authenticationAnswerModel.getToken())
.build());
}
A kiállított token nemes egyszerűséggel a helyreállítási folyamat kezdetét visszaigazoló emailben kerül a felhasználóhoz. A felhasználó eredeti szerepköre alapján a rendszer eldönti, melyik helyreállítási útvonalra irányítsa majd a felhasználót (ezzel tartva az azonos kliens megszorítást), és az útvonal végén foglal majd helyet a generált token. Az űrlap elküldésével a kliensnek a kérés Authorization headerjébe kell írnia a tokent, hogy a túloldalon (a backend alkalmazásban) megtörténhessen rajta a validálás. Amennyiben a token még nincs lejárva, és nincs érvénytelenítve sem, illetve a felhasználó is megfelelően írta be az új jelszavát (kétszer ugyanaz, klasszikus), az új jelszó hashelésre és eltárolásra kerül a token által azonosított felhasználó rekordjában. Ezután a token azonnal érvénytelenítésre kerül, így egy újabb jelszócsere azzal már nem lehetséges. Zárólépésként a felhasználó emailben kap értesítést a folyamat sikeres lezárultáról - amely remélhetőleg nem lepi majd meg.
@Override
@PermitReclaim
public Long confirmPasswordReset() {
// megkeressük a token által azonosított felhasználót az adatbázisban
ExtendedUserDetails userDetails = retrieveAuthenticatedUserDetails();
// értesítjük a folyamat befejezéséről
notificationService.successfulPasswordReset(PasswordResetSuccess.getBuilder()
.withParticipant(userDetails.getUsername())
.withUsername(userDetails.getName())
.build());
// érvénytelenítjük a tokent
revokeToken();
return userDetails.getId();
}
@Override
public void confirmPasswordReset(String password) throws EntityNotFoundException {
Long userID = authenticationService.confirmPasswordReset();
// és persze cseréljük a jelszót
userService.reclaimPassword(userID, passwordEncoder.encode(password));
}
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.