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 II - Session meghosszabbítás
A session meghosszabbítása
Nyilván a legfontosabb kérdés, hogy miért van erre szükség, milyen előnyünk származhat belőle. Természetesen ez a funkció is csak a felhasználó kényelmét szolgálja - és ezzel a kijelentéssel le is zárhatnám akár a bejegyzést is, de persze nem fogom. Akár standard session alapú, akár egy a Leaflethez hasonló REST interfészt biztosító és így egyszersmind állapotmentes rendszer alatt dolgozunk, a felhasználó munkafolyamata biztonsági okokból véges. Előbb-utóbb a session lejár, és így a felhasználó kijelentkeztetésre kerül. Egy REST alapú rendszernél, ahol tokenekkel történik az azonosítás, ez általában még szigorúbb: a tokent tipikusan rövid lejárati idejűre szokás beállítani, ezzel is minimalizálva a token esetleges ellopásával járó illetéktelen behatolási kísérletek esélyét. Persze, a felhasználó nem szeret fél óránként újra bejelentkezni, ezért egy aktívan használt sessiont, ahonnan nem észlelhető furcsa felhasználói aktivitás (például váltakozó forrás IP cím), érdemes lehet meghosszabbítani.
Stratégiák
A Leaflet-ben implementált session meghosszabbítás tervezése során két egymással éles ellentétben álló stratégia közül dönthettem. Az egyik - melyet végül elvetettem - az volt, hogy a meghosszabbítást a backend rendszerre bízom. Kézenfekvő megoldásnak tűnt, hiszen a session store segítségével már tudtam követni a tokeneket, a lejárati idő ellenőrzése a token kinyitása nélkül is lehetséges volt. A meghosszabbítás így történhetett volna oly módon is, hogy a rendszer egy megadott idő-küszöbön belül, a kérés feldolgozása során kiállít egy új tokent és visszaküldi azt a kliensnek. A tokenek kliens felé történő visszakommunikálása már egyébként is adott volt addigra a rendszerben, így teljesen megvalósíthatónak látszott a koncepció. Ami miatt végül azonban elvetettem ezt a megközelítést, az pont az volt, hogy a kliens így nem feltétlenül számít rá, hogy ő új tokent fog kapni. Ha a kliens pedig nem kezdi el használni az új tokent, hanem megpróbálkozok a korábbival, az sikertelen kérést eredményezhet. Mindemellett pedig úgy éreztem, az új token automatikus kiállítása egy felesleges felelősséget róhat a backend rendszerre - ha szükségesnek “érzi”, igényeljen a kliens új tokent.
Így döntöttem végül a másik lehetséges stratégia mellett, azaz hogy a kliens igényeljen tokent a szükséges pillanatban. A Leaflet admin rendszere már implementálja ezt a megvalósítást, méghozzá az alábbi módon.
A megvalósítás
Az admin rendszer egy érvényes session alatt küldi kéréseit a backend felé - tehát nyilván aktív tokennel rendelkezik, mely sem szándékosan (kijelentkezéssel) a felhasználó által, sem automatikusan (lejárati idő, token lopás miatt) a backend által nem lett még visszavonva. Minden sikeres kérés után az alábbi filter megvizsgálja az éppen aktív token lejárati idejét. Amennyiben pedig a megadott thresholdon belül lejár a token, elindítja a meghosszabbítási folyamatot, melynek első lépéseként új tokent igényel a backendtől.
// a SessionExtensionFilter osztály tartalma
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (enabled) {
// szükségünk van az aktív Authentication objektumra, mivel a token ott van tárolva
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// ellenőrizzük, hogy a token lejár-e a közeljövőben
if (isExpiringSoon(authentication)) {
try {
// ha igen, elkezdjük a meghosszabbítási folyamatot
userFacade.renewToken(authentication);
} catch (CommunicationFailureException e) {
LOGGER.error("Leaflet unreachable - failed to renew user session.", e);
}
}
}
filterChain.doFilter(request, response);
}
private boolean isExpiringSoon(Authentication authentication) {
boolean expiringSoon = false;
if (authentication instanceof JWTTokenAuthentication) {
Date expirationDate = ((AuthenticationUserDetailsModel) authentication
.getDetails()).getExpiration();
long difference = expirationDate.getTime() - System.currentTimeMillis();
// a lejárati időküszöb az alkalmazás konfigurációjában szabályozható
// a küszöbérték jelenleg 30 perc, tehát ha a token legfeljebb 30 percen
// belül lejár, akkor új tokent fog igényelni az alkalmazás
expiringSoon = TimeUnit.MILLISECONDS.toMinutes(difference) <= threshold;
}
return expiringSoon;
}
A backenden azonban ez egy trükkös lépés, mivel úgy kell új tokent kiállítani, hogy a felhasználó jelszavával jelenleg nem rendelkezik a rendszer - hiszen csak a tokennel tudjuk azonosítani a felhasználót, az pedig nem tartalmazza - nem is tartalmazhatja - a jelszót. A teljes bejelentkezési folyamatot tehát ez esetben nem játszhatjuk végig, a tokenben azonban jelen van a felhasználó azonosítója (email címe), mellyel a felhasználó újra megkereshető, és azzal számára új token generálható. A meghosszabbítást végző endpoint a backenden így védett kell, hogy legyen, mivel az egyetlen biztos pontunk a felhasználó kilétét illetően ugyanaz a token, amivel addig azonosította magát. Nyilván emiatt a session meghosszabbítása visszavont, lejárt tokennel lehetetlen, legalábbis semmilyen biztonsági kapaszkodónk nem maradna.
// a folyamat backend oldali feldolgozó kódja
@Override
// nagyon fontos, hogy az endpoint védett legyen!
// az alábbi custom PreAuthorize annotáció megköveteli,
// hogy a felhasználó aktív tokennel hívja az endpointot
@PermitAuthenticated
public String extendSession(LoginContextVO loginContextVO) {
// 1. újrageneráljuk a tokent
JWTAuthenticationAnswerModel authenticationAnswerModel =
jwtComponent.generateToken(retrieveAuthenticatedUserDetails());
// 2. visszavonjuk a korábbit
revokeToken();
// 3. eltároljuk az újat a Session Store-ban
storeToken(loginContextVO, authenticationAnswerModel);
return authenticationAnswerModel.getToken();
}
Szintén fontos lépés ekkor a régi token visszavonása, mely rögtön az új token kiállítása után megtörténik. Mivel a Security Context-ben ebben a pillanatban még a régi token szerepel, a session store megkerülése és/vagy megerőszakolása nélkül egyszerűen revoke-olható a token. Záróakkordként a token még mentésre kerül a store-ban, majd a Leaflet visszatér a kigenerált új tokennel. Innentől a kliens feladata, hogy az új tokent eltárolja valamilyen formában és onnantól azzal küldje kéréseit. Mivel az admin rendszer jelenleg egy standard sessiont épít fel, mindössze a Security Context-ben kell kicserélni a tokent és a folyamat ezzel be is fejeződött. A felhasználó az egészből pedig semmit nem vesz észre, azonban a háttérben a munkafolyamata már (beállítástól függően) néhány órával tovább tart.
// a UserFacade::renewToken metódus implementációja
@Override
public void renewToken(Authentication authentication) throws CommunicationFailureException {
// a userBridgeService.renewToken() hívás meghívja a backend alkalmazást,
// amivel megigényli az új tokent
// ha sikeres a kérés, az authenticationUtility.replace(...) hívás
// kicseréli az aktív tokent az újonnan megigényeltre
// ekkor a régi token már érvénytelen
authenticationUtility.replace(authentication.getPrincipal().toString(),
userBridgeService.renewToken().getToken());
}
// az AuthenticationUtility::replace metódus implementációja
void replace(String username, String token) {
// a token cseréje elég egyszerű folyamat: a tokent újracsomagoljuk
// majd tároljuk a SecurityContext-ben
Authentication authentication = new JWTTokenAuthentication.Builder()
.withEmailAddress(username)
.withDetails(jwtTokenPayloadReader.readPayload(token))
.withToken(token)
.build();
store(authentication);
}
void store(Authentication authentication) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
A folyamat még akár tovább is biztosítható azzal, ha a token legutóbbi használati idejét is naplózza a rendszer. Ekkor a lejárati thresholdon túl inaktivitási thresholdot is figyelhetünk, mely mondjuk 1-2 óra inaktivitás után visszautasítja a meghosszabbítási kérelmet, vagy akár vissza is vonhatja a tokent, ezzel kényszerítve a klienst az újra-authentikálásra. Ez is egy a számtalan lehetőség közül, melyet a session store biztosít, azonban a sorozat következő (befejező) részében a jelszó helyreállításról lesz szó.
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.