Refresh token Spring Security

Mali uvod, bavim se bekendom vec godinama, ali nisam imao prilike da se upustim u vode devopsa i securitija bas toliko koliko bih voleo. Kako god, odlucio sam da to prvo naucim i savladam na nekim mojim projektima, pa da se guram za taskove. Iskreno sramota me je sto kazem da radim bekend a da ne znam neke ovako elementarne stvari, ali sta je tu je. Imam volje da to promenim.

Da predjem na poentu. Savladao sam i znam da implementiram jwt token, sa nekim bazicnim dto-om. Dakle, sve tu radi perfektno – nasledio 'OncePerFilterRequest' i odradio metodu 'doFilterInterntal', lepo se proveravaju svi potrebni requestovi.

E sad, ja bi voleo sa ubacim neki refresh token koji ce da refresuje taj jwt token bez potrebe da korisnik unosi ponovni credentials. Imam nekoliko pitanja:

  • Da li taj refres token trebam da obradjujem na isti nacin kao jwt token, ali samo uz razliku da refresh token traje duze od jwt tokena, kome je standard trajanja svega do nekih 15-20 minuta?

  • Ako da, da li to znaci da u slucaju da dodje do bilo kakvog napada na sesije korisnika i ako napadac dodje do refresh tokena, da ce moci da generise nove jwt tokene sve dok taj refresh token ne istekne? Kako spreciti taj scenario?

  • Na nekim mestima sam procitao da je okej cuvati token u bazi, pa me zanima i misljenje iskusnijih kolega ovde u vezi sa tim. Ja koliko znam, a ne znam puno sto se tice ove teme priznajem, cela zamisao jwt tokena je da se autentifikacija i autorizacija vrsi nevezano od baze.

  • Da li da pravim endpoint u kontroleru koji ce da se gadja kada je jwt token istekao, a refresh token i dalje validan, da bi refresh token generisao novi jwt token, i da to na neki nacin postavim da se automatski desava? Da li je generalno uopste zamisao refresh tokena da samo generise novi jwt token i da ne ucestvuje u bilo kakvim drugim stvarima?

  • Sve ostalo sto vam padne na pamet a da smatrate da mi moze na bilo koji nacin pomoci, slobodno napisite. Realno mogao sam gpt (ili sta god drugo) da cimam za ovo (sto i jesam), ali jedno je kad dobijem takve generisane odgovore od strane AI-a, a drugo kad mi napise savet covek koji svaki dan maltene radi ovakve stvari na poslu.

Za one koje do sada nisam smorio sa ovim malo poduzim pitanjima, evo i mog koda sto se tice jwt implementacije, ako bi zbog bilo kakvih saveta trebao kod radi lakseg sporazumavanja, evo ga:

Glavna klasa:

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    // This method is called once per request. It checks if the request contains a valid JWT and authenticates the user based on it.
    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain)
        throws ServletException, IOException {

        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String username;

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        jwt = authHeader.substring(7);
        username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
            );

                authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Servis:

@Service
public class JwtService {

    private static final Logger logger = LoggerFactory.getLogger(JwtService.class);

    // Secret key used for signing the JWT.
    private static final String SECRET_KEY = "f9fad47fb71672a5799ae90b6475ce3ff18c234dcc2bd8a39791f996bbd3b3e1";

    // Extracts the username from the JWT.
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    // Extracts the username from the JWT for authenticated requests
    public String extractUsernameForAuthentication(String token) {
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
        }
        return extractClaim(token, Claims::getSubject);
    }

    // Extracts a specific claim from the JWT.
    public  T extractClaim(String token, Function claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    // Generates a JWT for a specific user.
    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    // Generates a JWT for a specific user with additional claims.
    public String generateToken(Map extraClaims, UserDetails userDetails) {
        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 15)) // 15 mins
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // Checks if a JWT is valid for a specific user.
    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    // Checks if a JWT has expired.
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    // Extracts the expiration date from the JWT.
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    // Extracts all claims from the JWT.
    private Claims extractAllClaims(String token) {
        try {
            return Jwts
                    .parserBuilder()
                    .setSigningKey(getSigningKey())
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
       } catch (ExpiredJwtException exception) {
            logger.error("JWT token is expired: {}", exception.getMessage());
            throw exception;
        } catch (MalformedJwtException exception) {
            logger.error("Invalid JWT token: {}", exception.getMessage());
            throw exception;
        } catch (UnsupportedJwtException exception) {
            logger.error("JWT token is unsupported: {}", exception.getMessage());
            throw exception;
        } catch (IllegalArgumentException exception) {
            logger.error("JWT claims string is empty: {}", exception.getMessage());
            throw exception;
        }
    }

    // Returns the signing key for the JWT.
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

View Reddit by svemirski_gospodinView Source

Mali uvod, bavim se bekendom vec godinama, ali nisam imao prilike da se upustim u vode devopsa i securitija bas toliko koliko bih voleo. Kako god, odlucio sam da to prvo naucim i savladam na…