JwtAuthenticationService.java

package cf.maybelambda.httpvalidator.springboot.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;

import static java.util.Objects.nonNull;

/**
 * Service for handling JWT authentication, including token generation,
 * validation, and secret key management.
 */
@Service
public class JwtAuthenticationService {
    public static final String BEARER_PREFIX = "Bearer ";
    static final String SECRET_PROPERTY = "jwt.signing.secret";
    static Logger logger = LoggerFactory.getLogger(JwtAuthenticationService.class);

    @Autowired
    private Environment env;

    /**
     * Generates a new encoded signing key.
     *
     * @return Base64-encoded signing key
     */
    static String getNewEncodedSigningKey() {
        return Encoders.BASE64.encode(Jwts.SIG.HS512.key().build().getEncoded());
    }

    /**
     * Retrieves the Base64-encoded secret key from the environment properties.
     *
     * @return Secret key for signing JWT
     */
    private SecretKey getSecretKey() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(this.env.getProperty(SECRET_PROPERTY)));
    }

    /**
     * Generates a new signed JWT token that is valid for a specified number of hours.
     *
     * @param hours Number of hours the token is valid for
     * @return JWT token
     */
    public String getNewTokenValidFor(int hours) {
        return Jwts.builder()
            .issuer(this.getClass().getCanonicalName())
            .issuedAt(new Date())
            .expiration(Date.from(Instant.now().plus(Duration.ofHours(hours))))
            .signWith(this.getSecretKey())
            .compact();
    }

    /**
     * Extracts all claims from a JWT token.
     *
     * @param token JWT token
     * @return Jws containing the claims
     */
    private Jws<Claims> extractAllClaims(String token) {
        return Jwts.parser()
            .verifyWith(this.getSecretKey())
            .build()
            .parseSignedClaims(token);
    }

    /**
     * Validates a JWT token from the Authorization header of an incoming request.
     *
     * @param authorizationHeader Authorization header containing the JWT token
     * @return true if the token is valid, false otherwise or if the header is null or empty
     */
    public boolean isValidToken(String authorizationHeader) {
        boolean ans = false;
        if (nonNull(authorizationHeader) && authorizationHeader.startsWith(BEARER_PREFIX)) {
            String token = authorizationHeader.replace(BEARER_PREFIX, "");
            try {
                Jws<Claims> claims = this.extractAllClaims(token);
                ans = this.getClass().getCanonicalName().equals(claims.getPayload().getIssuer());
            } catch (JwtException e) {
                logger.warn("Invalid JWT received: {}", e.getMessage());
            }
        }

        return ans;
    }

    /**
     * Sets the environment. Used for testing purposes.
     *
     * @param env Environment
     */
    void setEnv(Environment env) { this.env = env; }

    /**
     * Sets the logger; used for testing purposes.
     *
     * @param logger The logger to set.
     */
    void setLogger(Logger logger) { JwtAuthenticationService.logger = logger; }
}