JWT Validation Best Practices in Java: Complete Guide

Introduction to JWT Validation

JSON Web Tokens (JWT) are widely used for authentication and authorization. Proper validation is critical for security, as incorrect validation can lead to severe vulnerabilities like token forgery, privilege escalation, and unauthorized access.


System Architecture Overview

JWT Validation Architecture
├── Token Structure
│   ├── Header (alg, typ, kid)
│   ├── Payload (claims: sub, exp, iat, nbf, aud, iss)
│   └── Signature (HMAC, RSA, ECDSA, EdDSA)
├── Validation Pipeline
│   ├── Structural Validation
│   ├── Signature Verification
│   ├── Claims Validation
│   ├── Token Freshness
│   └── Revocation Check
├── Key Management
│   ├── Symmetric Keys (HMAC)
│   ├── Asymmetric Keys (RSA/EC)
│   ├── JWKS (JSON Web Key Set)
│   └── Key Rotation
└── Security Features
├── Algorithm Confusion Prevention
├── Replay Attack Protection
├── Token Binding
└── Audit Logging

Core Implementation

1. Maven Dependencies

<properties>
<jjwt.version>0.12.5</jjwt.version>
<nimbus.version>9.37.3</nimbus.version>
<bouncycastle.version>1.78</bouncycastle.version>
</properties>
<dependencies>
<!-- JJWT (Java JWT) - Recommended -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Nimbus JOSE + JWT (Alternative) -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus.version}</version>
</dependency>
<!-- Bouncy Castle for additional algorithms -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Spring Security (optional, for integration) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.2.0</version>
<scope>provided</scope>
</dependency>
<!-- Redis for token blacklisting -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.2.0</version>
</dependency>
<!-- HTTP Client for JWKS fetching -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- Cache for JWKS keys -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>

2. Comprehensive JWT Validator

package com.jwt.validation;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.security.Key;
import java.security.PublicKey;
import java.time.Clock;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
@Service
public class JWTValidator {
private static final Logger logger = LoggerFactory.getLogger(JWTValidator.class);
private final Clock clock = Clock.systemUTC();
private final ObjectMapper objectMapper = new ObjectMapper();
// Cache for parsed and validated tokens
private final Map<String, ValidatedToken> tokenCache = new ConcurrentHashMap<>();
// Validation metrics
private final ValidationMetrics metrics = new ValidationMetrics();
/**
* JWT Validation Result
*/
public static class ValidationResult {
private final boolean valid;
private final Claims claims;
private final String error;
private final ValidationError errorType;
public ValidationResult(boolean valid, Claims claims, String error, ValidationError errorType) {
this.valid = valid;
this.claims = claims;
this.error = error;
this.errorType = errorType;
}
public static ValidationResult success(Claims claims) {
return new ValidationResult(true, claims, null, null);
}
public static ValidationResult failure(String error, ValidationError errorType) {
return new ValidationResult(false, null, error, errorType);
}
// getters
}
/**
* Validation error types
*/
public enum ValidationError {
MALFORMED_TOKEN,
INVALID_SIGNATURE,
EXPIRED_TOKEN,
NOT_YET_VALID,
INVALID_ISSUER,
INVALID_AUDIENCE,
MISSING_CLAIM,
REVOKED_TOKEN,
ALGORITHM_MISMATCH,
KEY_NOT_FOUND,
INTERNAL_ERROR
}
/**
* Validated token with metadata
*/
public static class ValidatedToken {
private final String token;
private final Claims claims;
private final Instant validatedAt;
private final String keyId;
public ValidatedToken(String token, Claims claims, String keyId) {
this.token = token;
this.claims = claims;
this.validatedAt = Instant.now();
this.keyId = keyId;
}
// getters
}
/**
* JWT Validation Configuration
*/
public static class ValidationConfig {
private boolean requireExpiration = true;
private boolean requireIssuedAt = true;
private boolean requireNotBefore = false;
private boolean requireSubject = true;
private boolean requireId = false;
private Set<String> allowedIssuers = new HashSet<>();
private Set<String> allowedAudiences = new HashSet<>();
private long leewaySeconds = 0;
private boolean validateTokenType = true;
private boolean rejectTokensWithoutKid = false;
private Set<String> allowedAlgorithms = new HashSet<>(Arrays.asList("RS256", "ES256", "EdDSA"));
// Builder pattern
public static class Builder {
private ValidationConfig config = new ValidationConfig();
public Builder withRequiredExpiration(boolean required) {
config.requireExpiration = required;
return this;
}
public Builder withAllowedIssuer(String issuer) {
config.allowedIssuers.add(issuer);
return this;
}
public Builder withAllowedAudience(String audience) {
config.allowedAudiences.add(audience);
return this;
}
public Builder withLeewaySeconds(long seconds) {
config.leewaySeconds = seconds;
return this;
}
public Builder withAllowedAlgorithms(Set<String> algorithms) {
config.allowedAlgorithms = algorithms;
return this;
}
public ValidationConfig build() {
return config;
}
}
}
/**
* Core validation method with comprehensive checks
*/
public ValidationResult validateToken(String token, 
KeyProvider keyProvider,
ValidationConfig config) {
metrics.incrementTotalValidations();
try {
// Step 1: Basic structural validation
if (!isWellFormed(token)) {
metrics.incrementMalformedTokens();
return ValidationResult.failure("Malformed token structure", 
ValidationError.MALFORMED_TOKEN);
}
// Step 2: Parse header to check algorithm
JwsHeader header = parseHeader(token);
// Step 3: Algorithm validation (prevent algorithm confusion attacks)
ValidationResult algoCheck = validateAlgorithm(header, config);
if (!algoCheck.isValid()) {
return algoCheck;
}
// Step 4: Get signing key
Key signingKey = keyProvider.getKey(header);
if (signingKey == null) {
metrics.incrementKeyNotFound();
return ValidationResult.failure("Signing key not found", 
ValidationError.KEY_NOT_FOUND);
}
// Step 5: Parse and validate signature
Jws<Claims> jws;
try {
jws = Jwts.parser()
.verifyWith( signingKey instanceof SecretKey ? 
(SecretKey) signingKey : null)
.verifyWith( signingKey instanceof PublicKey ? 
(PublicKey) signingKey : null)
.build()
.parseSignedClaims(token);
} catch (SignatureException e) {
metrics.incrementInvalidSignatures();
return ValidationResult.failure("Invalid signature", 
ValidationError.INVALID_SIGNATURE);
} catch (ExpiredJwtException e) {
metrics.incrementExpiredTokens();
return ValidationResult.failure("Token expired", 
ValidationError.EXPIRED_TOKEN);
} catch (MalformedJwtException e) {
metrics.incrementMalformedTokens();
return ValidationResult.failure("Malformed token", 
ValidationError.MALFORMED_TOKEN);
} catch (UnsupportedJwtException e) {
return ValidationResult.failure("Unsupported token", 
ValidationError.MALFORMED_TOKEN);
} catch (IllegalArgumentException e) {
return ValidationResult.failure("Invalid token", 
ValidationError.MALFORMED_TOKEN);
}
Claims claims = jws.getPayload();
// Step 6: Claims validation
ValidationResult claimsCheck = validateClaims(claims, config);
if (!claimsCheck.isValid()) {
return claimsCheck;
}
// Step 7: Token type validation
if (config.validateTokenType) {
String typ = header.getType();
if (typ != null && !"JWT".equalsIgnoreCase(typ)) {
return ValidationResult.failure("Invalid token type: " + typ, 
ValidationError.MALFORMED_TOKEN);
}
}
// Step 8: Cache validated token
ValidatedToken validated = new ValidatedToken(token, claims, header.getKeyId());
tokenCache.put(claims.getId(), validated);
metrics.incrementValidTokens();
return ValidationResult.success(claims);
} catch (Exception e) {
logger.error("Unexpected error during token validation", e);
metrics.incrementInternalErrors();
return ValidationResult.failure("Internal validation error: " + e.getMessage(), 
ValidationError.INTERNAL_ERROR);
}
}
/**
* Validate token with caching
*/
public Optional<ValidatedToken> validateTokenCached(String token,
KeyProvider keyProvider,
ValidationConfig config) {
// Check cache first (if token has JTI)
try {
Claims claims = parseClaimsWithoutValidation(token);
String jti = claims.getId();
if (jti != null && tokenCache.containsKey(jti)) {
ValidatedToken cached = tokenCache.get(jti);
// Verify token hasn't expired
if (cached.getClaims().getExpiration() == null || 
cached.getClaims().getExpiration().after(new Date())) {
metrics.incrementCacheHits();
return Optional.of(cached);
}
}
} catch (Exception e) {
// Continue with full validation
}
// Full validation
ValidationResult result = validateToken(token, keyProvider, config);
if (result.isValid()) {
String jti = result.getClaims().getId();
if (jti != null) {
ValidatedToken validated = new ValidatedToken(token, result.getClaims(), 
parseHeader(token).getKeyId());
tokenCache.put(jti, validated);
}
return Optional.of(new ValidatedToken(token, result.getClaims(), 
parseHeader(token).getKeyId()));
}
return Optional.empty();
}
/**
* Check if token is revoked (blacklist)
*/
public boolean isTokenRevoked(String tokenId, TokenRevocationService revocationService) {
if (tokenId == null) return false;
return revocationService.isRevoked(tokenId);
}
/**
* Validate algorithm to prevent algorithm confusion attacks
*/
private ValidationResult validateAlgorithm(JwsHeader header, ValidationConfig config) {
String algorithm = header.getAlgorithm();
// Check if algorithm is allowed
if (!config.allowedAlgorithms.contains(algorithm)) {
return ValidationResult.failure(
"Algorithm '" + algorithm + "' not allowed. Allowed: " + config.allowedAlgorithms,
ValidationError.ALGORITHM_MISMATCH);
}
// Check for "none" algorithm (critical security check)
if ("none".equalsIgnoreCase(algorithm)) {
return ValidationResult.failure(
"'none' algorithm is not allowed",
ValidationError.ALGORITHM_MISMATCH);
}
// Check for key ID if required
if (config.rejectTokensWithoutKid && header.getKeyId() == null) {
return ValidationResult.failure(
"Token missing 'kid' header",
ValidationError.KEY_NOT_FOUND);
}
return ValidationResult.success(null);
}
/**
* Validate all claims
*/
private ValidationResult validateClaims(Claims claims, ValidationConfig config) {
Date now = new Date(clock.millis());
// Expiration check
if (config.requireExpiration && claims.getExpiration() == null) {
return ValidationResult.failure("Missing expiration claim", 
ValidationError.MISSING_CLAIM);
}
if (claims.getExpiration() != null) {
Date expWithLeeway = new Date(claims.getExpiration().getTime() + 
config.leewaySeconds * 1000);
if (expWithLeeway.before(now)) {
return ValidationResult.failure("Token expired", 
ValidationError.EXPIRED_TOKEN);
}
}
// Not Before check
if (config.requireNotBefore && claims.getNotBefore() == null) {
return ValidationResult.failure("Missing not-before claim", 
ValidationError.MISSING_CLAIM);
}
if (claims.getNotBefore() != null) {
Date nbfWithLeeway = new Date(claims.getNotBefore().getTime() - 
config.leewaySeconds * 1000);
if (nbfWithLeeway.after(now)) {
return ValidationResult.failure("Token not yet valid", 
ValidationError.NOT_YET_VALID);
}
}
// Issued At check
if (config.requireIssuedAt && claims.getIssuedAt() == null) {
return ValidationResult.failure("Missing issued-at claim", 
ValidationError.MISSING_CLAIM);
}
// Subject check
if (config.requireSubject && (claims.getSubject() == null || 
claims.getSubject().trim().isEmpty())) {
return ValidationResult.failure("Missing or empty subject claim", 
ValidationError.MISSING_CLAIM);
}
// Issuer check
if (!config.allowedIssuers.isEmpty()) {
String issuer = claims.getIssuer();
if (issuer == null || !config.allowedIssuers.contains(issuer)) {
return ValidationResult.failure(
"Invalid issuer: " + issuer, 
ValidationError.INVALID_ISSUER);
}
}
// Audience check
if (!config.allowedAudiences.isEmpty()) {
String audience = claims.getAudience();
if (audience == null || !config.allowedAudiences.contains(audience)) {
return ValidationResult.failure(
"Invalid audience: " + audience, 
ValidationError.INVALID_AUDIENCE);
}
}
// JWT ID check
if (config.requireId && claims.getId() == null) {
return ValidationResult.failure("Missing JWT ID claim", 
ValidationError.MISSING_CLAIM);
}
return ValidationResult.success(claims);
}
/**
* Basic structural validation without cryptographic verification
*/
public boolean isWellFormed(String token) {
if (token == null || token.trim().isEmpty()) {
return false;
}
String[] parts = token.split("\\.");
if (parts.length != 3) {
return false;
}
// Check each part is valid Base64URL
try {
for (String part : parts) {
if (part.isEmpty()) {
return false;
}
Base64.getUrlDecoder().decode(part);
}
} catch (IllegalArgumentException e) {
return false;
}
return true;
}
/**
* Parse header without validation
*/
private JwsHeader parseHeader(String token) {
String[] parts = token.split("\\.");
byte[] headerBytes = Base64.getUrlDecoder().decode(parts[0]);
return Jwts.parser().build().parseSignedClaims(token).getHeader();
}
/**
* Parse claims without signature validation (use only for cache lookup)
*/
private Claims parseClaimsWithoutValidation(String token) {
String[] parts = token.split("\\.");
byte[] claimsBytes = Base64.getUrlDecoder().decode(parts[1]);
String claimsJson = new String(claimsBytes);
return Jwts.claims(new String(claimsBytes));
}
/**
* Extract subject from token without full validation (for logging/metrics only)
*/
public Optional<String> extractSubject(String token) {
try {
Claims claims = parseClaimsWithoutValidation(token);
return Optional.ofNullable(claims.getSubject());
} catch (Exception e) {
return Optional.empty();
}
}
/**
* Key provider interface for flexible key management
*/
public interface KeyProvider {
Key getKey(JwsHeader header);
}
/**
* Symmetric key provider (HMAC)
*/
public static class SymmetricKeyProvider implements KeyProvider {
private final SecretKey secretKey;
public SymmetricKeyProvider(String base64Secret) {
byte[] keyBytes = Base64.getDecoder().decode(base64Secret);
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
}
public SymmetricKeyProvider(SecretKey secretKey) {
this.secretKey = secretKey;
}
@Override
public Key getKey(JwsHeader header) {
return secretKey;
}
}
/**
* JWKS (JSON Web Key Set) key provider
*/
public static class JwksKeyProvider implements KeyProvider {
private final JwksClient jwksClient;
private final com.github.benmanes.caffeine.cache.Cache<String, Key> keyCache;
public JwksKeyProvider(String jwksUrl) {
this.jwksClient = new JwksClient(jwksUrl);
this.keyCache = com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, java.util.concurrent.TimeUnit.HOURS)
.build();
}
@Override
public Key getKey(JwsHeader header) {
String kid = header.getKeyId();
if (kid == null) {
return null;
}
return keyCache.get(kid, k -> {
try {
return jwksClient.getKey(k);
} catch (Exception e) {
logger.error("Failed to fetch key from JWKS: {}", kid, e);
return null;
}
});
}
}
/**
* Validation metrics for monitoring
*/
public static class ValidationMetrics {
private final java.util.concurrent.atomic.AtomicLong totalValidations = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong validTokens = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong invalidSignatures = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong expiredTokens = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong malformedTokens = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong keyNotFound = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong cacheHits = new java.util.concurrent.atomic.AtomicLong();
private final java.util.concurrent.atomic.AtomicLong internalErrors = new java.util.concurrent.atomic.AtomicLong();
public void incrementTotalValidations() { totalValidations.incrementAndGet(); }
public void incrementValidTokens() { validTokens.incrementAndGet(); }
public void incrementInvalidSignatures() { invalidSignatures.incrementAndGet(); }
public void incrementExpiredTokens() { expiredTokens.incrementAndGet(); }
public void incrementMalformedTokens() { malformedTokens.incrementAndGet(); }
public void incrementKeyNotFound() { keyNotFound.incrementAndGet(); }
public void incrementCacheHits() { cacheHits.incrementAndGet(); }
public void incrementInternalErrors() { internalErrors.incrementAndGet(); }
public Map<String, Object> getSnapshot() {
Map<String, Object> snapshot = new HashMap<>();
snapshot.put("totalValidations", totalValidations.get());
snapshot.put("validTokens", validTokens.get());
snapshot.put("invalidSignatures", invalidSignatures.get());
snapshot.put("expiredTokens", expiredTokens.get());
snapshot.put("malformedTokens", malformedTokens.get());
snapshot.put("keyNotFound", keyNotFound.get());
snapshot.put("cacheHits", cacheHits.get());
snapshot.put("internalErrors", internalErrors.get());
snapshot.put("successRate", totalValidations.get() > 0 ? 
(double) validTokens.get() / totalValidations.get() : 0);
return snapshot;
}
}
}

3. Token Revocation Service

package com.jwt.validation;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service
public class TokenRevocationService {
private final RedisTemplate<String, String> redisTemplate;
private final Set<String> inMemoryBlacklist = ConcurrentHashMap.newKeySet();
private final boolean useRedis;
private static final String BLACKLIST_PREFIX = "jwt:blacklist:";
private static final Duration DEFAULT_BLACKLIST_DURATION = Duration.ofDays(7);
public TokenRevocationService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
this.useRedis = redisTemplate != null;
}
/**
* Revoke a specific token by its JTI
*/
public void revokeToken(String tokenId, Duration duration) {
if (useRedis) {
String key = BLACKLIST_PREFIX + tokenId;
redisTemplate.opsForValue().set(key, "revoked", duration);
} else {
inMemoryBlacklist.add(tokenId);
}
}
/**
* Revoke token with expiration from token itself
*/
public void revokeToken(String tokenId, Date expiration) {
if (expiration != null) {
Duration duration = Duration.between(
Instant.now(), 
expiration.toInstant()
);
revokeToken(tokenId, duration);
} else {
revokeToken(tokenId, DEFAULT_BLACKLIST_DURATION);
}
}
/**
* Check if token is revoked
*/
public boolean isRevoked(String tokenId) {
if (tokenId == null) return false;
if (useRedis) {
String key = BLACKLIST_PREFIX + tokenId;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} else {
return inMemoryBlacklist.contains(tokenId);
}
}
/**
* Revoke all tokens for a user (logout everywhere)
*/
public void revokeAllUserTokens(String userId, String issuer) {
String pattern = issuer + ":" + userId + ":*";
// Implementation would revoke all tokens matching pattern
}
/**
* Clean up expired blacklist entries
*/
public void cleanup() {
if (!useRedis) {
// In-memory cleanup would require tracking expiration
}
}
}

4. Token Binding Service

package com.jwt.validation;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Base64;
@Service
public class TokenBindingService {
private static final String HMAC_ALGORITHM = "HmacSHA256";
/**
* Create token binding hash from client properties
*/
public String createBindingHash(String clientId, 
String userAgent, 
String ipAddress,
byte[] bindingKey) throws Exception {
StringBuilder sb = new StringBuilder();
sb.append(clientId).append("|");
sb.append(userAgent).append("|");
sb.append(ipAddress);
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(bindingKey, HMAC_ALGORITHM);
mac.init(keySpec);
byte[] hash = mac.doFinal(sb.toString().getBytes());
return Base64.getEncoder().encodeToString(hash);
}
/**
* Verify token binding
*/
public boolean verifyBinding(String tokenBindingHash,
String clientId,
String userAgent,
String ipAddress,
byte[] bindingKey) throws Exception {
String computedHash = createBindingHash(clientId, userAgent, ipAddress, bindingKey);
return MessageDigest.isEqual(
tokenBindingHash.getBytes(),
computedHash.getBytes()
);
}
/**
* Extract client fingerprint from request
*/
public ClientFingerprint extractFingerprint(HttpServletRequest request) {
ClientFingerprint fingerprint = new ClientFingerprint();
fingerprint.setUserAgent(request.getHeader("User-Agent"));
fingerprint.setAcceptLanguage(request.getHeader("Accept-Language"));
fingerprint.setAcceptEncoding(request.getHeader("Accept-Encoding"));
fingerprint.setIpAddress(request.getRemoteAddr());
fingerprint.setForwardedFor(request.getHeader("X-Forwarded-For"));
// Add TLS session ID if available
if (request.isSecure()) {
fingerprint.setTlsSessionId(request.getAttribute("javax.servlet.request.ssl_session_id"));
}
return fingerprint;
}
public static class ClientFingerprint {
private String userAgent;
private String acceptLanguage;
private String acceptEncoding;
private String ipAddress;
private String forwardedFor;
private Object tlsSessionId;
// getters and setters
public String toNormalizedString() {
return String.format("%s|%s|%s|%s",
userAgent != null ? userAgent : "",
acceptLanguage != null ? acceptLanguage : "",
acceptEncoding != null ? acceptEncoding : "",
forwardedFor != null ? forwardedFor : ipAddress != null ? ipAddress : ""
);
}
}
}

5. JWT Issuance Service

package com.jwt.validation;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.security.Key;
import java.security.PrivateKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
@Service
public class JWTIssuanceService {
private final Key signingKey;
private final String issuer;
private final TokenRevocationService revocationService;
public JWTIssuanceService(Key signingKey, String issuer, 
TokenRevocationService revocationService) {
this.signingKey = signingKey;
this.issuer = issuer;
this.revocationService = revocationService;
}
/**
* Issue a new access token
*/
public String issueAccessToken(String subject, 
Map<String, Object> claims,
Duration validity) {
Instant now = Instant.now();
Instant expiry = now.plus(validity);
return Jwts.builder()
.subject(subject)
.issuer(issuer)
.issuedAt(Date.from(now))
.expiration(Date.from(expiry))
.id(UUID.randomUUID().toString())
.claims(claims)
.signWith(signingKey)
.compact();
}
/**
* Issue refresh token with longer validity
*/
public String issueRefreshToken(String subject, Duration validity) {
Instant now = Instant.now();
Instant expiry = now.plus(validity);
return Jwts.builder()
.subject(subject)
.issuer(issuer)
.issuedAt(Date.from(now))
.expiration(Date.from(expiry))
.id(UUID.randomUUID().toString())
.claim("token_type", "refresh")
.signWith(signingKey)
.compact();
}
/**
* Refresh access token using refresh token
*/
public Optional<String> refreshAccessToken(String refreshToken,
JWTValidator validator,
KeyProvider keyProvider,
JWTValidator.ValidationConfig config) {
JWTValidator.ValidationResult result = validator.validateToken(
refreshToken, keyProvider, config);
if (!result.isValid()) {
return Optional.empty();
}
Claims claims = result.getClaims();
// Verify it's a refresh token
if (!"refresh".equals(claims.get("token_type"))) {
return Optional.empty();
}
// Check if refresh token is revoked
if (validator.isTokenRevoked(claims.getId(), revocationService)) {
return Optional.empty();
}
// Issue new access token
String newAccessToken = issueAccessToken(
claims.getSubject(),
Map.of("scope", claims.get("scope", String.class)),
Duration.ofMinutes(15)
);
return Optional.of(newAccessToken);
}
/**
* Logout - revoke token
*/
public void logout(String token, JWTValidator validator, KeyProvider keyProvider) {
validator.extractSubject(token).ifPresent(subject -> {
validator.validateToken(token, keyProvider, new JWTValidator.ValidationConfig.Builder()
.withRequiredExpiration(true)
.build())
.getClaims()
.ifPresent(claims -> {
revocationService.revokeToken(claims.getId(), claims.getExpiration());
});
});
}
}

6. Spring Security Integration

package com.jwt.validation.security;
import com.jwt.validation.JWTValidator;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class JWTAuthenticationFilter extends OncePerRequestFilter {
private final JWTValidator jwtValidator;
private final JWTValidator.KeyProvider keyProvider;
private final JWTValidator.ValidationConfig validationConfig;
public JWTAuthenticationFilter(JWTValidator jwtValidator,
JWTValidator.KeyProvider keyProvider,
JWTValidator.ValidationConfig validationConfig) {
this.jwtValidator = jwtValidator;
this.keyProvider = keyProvider;
this.validationConfig = validationConfig;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null) {
try {
JWTValidator.ValidationResult result = jwtValidator.validateToken(
token, keyProvider, validationConfig);
if (result.isValid()) {
// Extract authorities from claims
List<String> roles = result.getClaims().get("roles", List.class);
List<SimpleGrantedAuthority> authorities = roles != null ?
roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList()) :
List.of();
UsernamePasswordAuthenticationToken auth = 
new UsernamePasswordAuthenticationToken(
result.getClaims().getSubject(),
null,
authorities
);
auth.setDetails(result.getClaims());
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception e) {
logger.error("JWT authentication failed", e);
}
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
String token = request.getParameter("access_token");
if (token != null) {
return token;
}
return null;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
return path.startsWith("/public") || 
path.startsWith("/health") ||
path.startsWith("/metrics");
}
}

7. JWKS Client

package com.jwt.validation;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
public class JwksClient {
private final String jwksUrl;
private final CloseableHttpClient httpClient;
private final ObjectMapper objectMapper;
private final com.github.benmanes.caffeine.cache.Cache<String, JsonNode> jwksCache;
public JwksClient(String jwksUrl) {
this.jwksUrl = jwksUrl;
this.httpClient = HttpClients.createDefault();
this.objectMapper = new ObjectMapper();
this.jwksCache = com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
.maximumSize(1)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
/**
* Get key by key ID
*/
public Key getKey(String kid) throws Exception {
JsonNode jwks = getJwks();
JsonNode keys = jwks.get("keys");
for (JsonNode key : keys) {
if (kid.equals(key.get("kid").asText())) {
return parseKey(key);
}
}
throw new IllegalArgumentException("Key not found: " + kid);
}
/**
* Fetch JWKS from endpoint
*/
private JsonNode fetchJwks() throws Exception {
HttpGet request = new HttpGet(jwksUrl);
return httpClient.execute(request, response -> {
if (response.getCode() != 200) {
throw new RuntimeException("Failed to fetch JWKS: " + response.getCode());
}
return objectMapper.readTree(response.getEntity().getContent());
});
}
/**
* Get JWKS with caching
*/
private JsonNode getJwks() throws Exception {
return jwksCache.get("jwks", k -> {
try {
return fetchJwks();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
/**
* Parse JWK to Java Key
*/
private Key parseKey(JsonNode jwk) throws Exception {
String kty = jwk.get("kty").asText();
switch (kty) {
case "RSA":
return parseRsaKey(jwk);
case "EC":
return parseEcKey(jwk);
case "oct":
return parseOctKey(jwk);
default:
throw new IllegalArgumentException("Unsupported key type: " + kty);
}
}
private Key parseRsaKey(JsonNode jwk) throws Exception {
BigInteger modulus = new BigInteger(1, 
Base64.getUrlDecoder().decode(jwk.get("n").asText()));
BigInteger exponent = new BigInteger(1, 
Base64.getUrlDecoder().decode(jwk.get("e").asText()));
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(spec);
}
private Key parseEcKey(JsonNode jwk) throws Exception {
// ECDSA key parsing
String crv = jwk.get("crv").asText();
byte[] x = Base64.getUrlDecoder().decode(jwk.get("x").asText());
byte[] y = Base64.getUrlDecoder().decode(jwk.get("y").asText());
// Implementation depends on curve
throw new UnsupportedOperationException("EC key parsing not implemented");
}
private Key parseOctKey(JsonNode jwk) {
// Symmetric key
byte[] keyBytes = Base64.getUrlDecoder().decode(jwk.get("k").asText());
return Keys.hmacShaKeyFor(keyBytes);
}
}

8. Testing and Validation

package com.jwt.validation.test;
import com.jwt.validation.JWTValidator;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.*;
import javax.crypto.SecretKey;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class JWTValidationTest {
private JWTValidator validator;
private SecretKey signingKey;
private JWTValidator.KeyProvider keyProvider;
private JWTValidator.ValidationConfig config;
@BeforeEach
void setUp() {
validator = new JWTValidator();
signingKey = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.RS256);
keyProvider = new JWTValidator.SymmetricKeyProvider(signingKey);
config = new JWTValidator.ValidationConfig.Builder()
.withRequiredExpiration(true)
.withAllowedIssuer("test-issuer")
.withAllowedAudience("test-audience")
.withAllowedAlgorithms(Set.of("RS256"))
.build();
}
@Test
@Order(1)
void testValidToken() {
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.audience().add("test-audience").and()
.issuedAt(new Date())
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.id(UUID.randomUUID().toString())
.claim("role", "admin")
.signWith(signingKey)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertTrue(result.isValid());
assertNotNull(result.getClaims());
assertEquals("user123", result.getClaims().getSubject());
}
@Test
@Order(2)
void testExpiredToken() {
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.expiration(Date.from(Instant.now().minus(1, ChronoUnit.HOURS)))
.signWith(signingKey)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.EXPIRED_TOKEN, 
result.getErrorType());
}
@Test
@Order(3)
void testInvalidSignature() {
SecretKey wrongKey = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.RS256);
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.signWith(wrongKey)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.INVALID_SIGNATURE, 
result.getErrorType());
}
@Test
@Order(4)
void testWrongAlgorithm() {
// Create token with different algorithm than allowed
SecretKey hmacKey = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.signWith(hmacKey, io.jsonwebtoken.SignatureAlgorithm.HS256)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.ALGORITHM_MISMATCH, 
result.getErrorType());
}
@Test
@Order(5)
void testWrongIssuer() {
String token = Jwts.builder()
.subject("user123")
.issuer("wrong-issuer")
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.signWith(signingKey)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.INVALID_ISSUER, 
result.getErrorType());
}
@Test
@Order(6)
void testMalformedToken() {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" + 
".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0" + 
".invalid-signature-part";
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.MALFORMED_TOKEN, 
result.getErrorType());
}
@Test
@Order(7)
void testNoneAlgorithm() {
// JWT with "none" algorithm (critical security test)
String header = Base64.getUrlEncoder().withoutPadding()
.encodeToString("{\"alg\":\"none\"}".getBytes());
String payload = Base64.getUrlEncoder().withoutPadding()
.encodeToString("{\"sub\":\"user123\"}".getBytes());
String token = header + "." + payload + ".";
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.ALGORITHM_MISMATCH, 
result.getErrorType());
}
@Test
@Order(8)
void testTokenWithoutExpiration() {
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.signWith(signingKey)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertFalse(result.isValid());
assertEquals(JWTValidator.ValidationError.MISSING_CLAIM, 
result.getErrorType());
}
@Test
@Order(9)
void testCachedValidation() {
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.id("test-jti-123")
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.signWith(signingKey)
.compact();
// First validation - should be cache miss
var result1 = validator.validateTokenCached(token, keyProvider, config);
assertTrue(result1.isPresent());
// Second validation - should be cache hit
var result2 = validator.validateTokenCached(token, keyProvider, config);
assertTrue(result2.isPresent());
}
@Test
@Order(10)
void testTokenBinding() throws Exception {
TokenBindingService bindingService = new TokenBindingService();
byte[] bindingKey = "test-binding-key".getBytes();
String clientId = "client-123";
String userAgent = "Mozilla/5.0";
String ipAddress = "192.168.1.100";
String bindingHash = bindingService.createBindingHash(
clientId, userAgent, ipAddress, bindingKey);
assertNotNull(bindingHash);
boolean verified = bindingService.verifyBinding(
bindingHash, clientId, userAgent, ipAddress, bindingKey);
assertTrue(verified);
// Wrong IP should fail
boolean wrongVerified = bindingService.verifyBinding(
bindingHash, clientId, userAgent, "192.168.1.101", bindingKey);
assertFalse(wrongVerified);
}
@Test
@Order(11)
void testRevocation() {
TokenRevocationService revocationService = new TokenRevocationService(null);
String tokenId = "test-token-123";
assertFalse(revocationService.isRevoked(tokenId));
revocationService.revokeToken(tokenId, java.time.Duration.ofHours(1));
assertTrue(revocationService.isRevoked(tokenId));
}
@Test
@Order(12)
void testValidationMetrics() {
// Generate some tokens with various issues
testExpiredToken();
testInvalidSignature();
testMalformedToken();
var metrics = validator.getMetrics().getSnapshot();
assertNotNull(metrics);
assertTrue((Long) metrics.get("totalValidations") > 0);
assertTrue((Long) metrics.get("invalidSignatures") > 0);
assertTrue((Long) metrics.get("expiredTokens") > 0);
assertTrue((Long) metrics.get("malformedTokens") > 0);
}
@Test
@Order(13)
void testAlgorithmConfusionPrevention() {
// Create RSA token but try to verify with HMAC
// This tests prevention of algorithm confusion attacks
// This would require RSA key generation and specific attack simulation
// For brevity, we're testing the algorithm whitelist instead
assertTrue(config.getAllowedAlgorithms().contains("RS256"));
assertFalse(config.getAllowedAlgorithms().contains("HS256"));
}
@Test
@Order(14)
void testPerformance() {
int iterations = 1000;
String token = Jwts.builder()
.subject("user123")
.issuer("test-issuer")
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.signWith(signingKey)
.compact();
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
validator.validateToken(token, keyProvider, config);
}
long duration = System.nanoTime() - start;
long avgTimeNs = duration / iterations;
System.out.printf("Average validation time: %d ns (%.2f ms)%n", 
avgTimeNs, avgTimeNs / 1_000_000.0);
assertTrue(avgTimeNs < 10_000_000, "Validation too slow: " + avgTimeNs + " ns");
}
@Test
@Order(15)
void testStress() {
// Stress test with many different tokens
for (int i = 0; i < 100; i++) {
String token = Jwts.builder()
.subject("user-" + i)
.issuer("test-issuer")
.expiration(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
.claim("index", i)
.signWith(signingKey)
.compact();
JWTValidator.ValidationResult result = validator.validateToken(
token, keyProvider, config);
assertTrue(result.isValid());
assertEquals("user-" + i, result.getClaims().getSubject());
}
}
}

Security Best Practices Checklist

1. Critical Security Checks

public class SecurityChecklist {
// ❌ NEVER do this
public void insecureValidation(String token) {
String[] parts = token.split("\\.");
String payload = new String(Base64.getDecoder().decode(parts[1]));
// No signature verification!
}
// ✅ ALWAYS do this
public void secureValidation(String token, Key key) {
Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token);
}
}

2. Algorithm Hardening

// Reject weak algorithms
public class AlgorithmHardening {
private static final Set<String> WEAK_ALGORITHMS = Set.of(
"none", "HS256", "HS384", "HS512", // HMAC with short keys
"RS256", "RS384", "RS512" // RSA with key size < 2048
);
public boolean isAlgorithmAllowed(String alg, Key key) {
if (WEAK_ALGORITHMS.contains(alg)) {
return false;
}
// Check key strength
if (key instanceof RSAPublicKey) {
int keySize = ((RSAPublicKey) key).getModulus().bitLength();
return keySize >= 2048;
}
return true;
}
}

3. Key Rotation

@Scheduled(cron = "0 0 2 * * 0") // Weekly
public void rotateKeys() {
// Generate new key
Key newKey = Keys.secretKeyFor(SignatureAlgorithm.RS256);
// Store with new key ID
keyStore.put(newKeyId, newKey);
// Keep old key for token validation until they expire
keyStore.put(oldKeyId + "_expiring", oldKey);
// Schedule old key removal
scheduledExecutor.schedule(() -> {
keyStore.remove(oldKeyId + "_expiring");
}, Duration.ofDays(7).toMillis(), TimeUnit.MILLISECONDS);
}

4. Audit Logging

@Aspect
@Component
public class ValidationAuditAspect {
@Around("@annotation(Audited)")
public Object auditValidation(ProceedingJoinPoint pjp) throws Throwable {
String token = (String) pjp.getArgs()[0];
String subject = extractSubject(token);
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - start;
auditLogger.log(new AuditEvent(
subject,
"VALIDATION_SUCCESS",
duration,
null
));
return result;
} catch (Exception e) {
auditLogger.log(new AuditEvent(
subject,
"VALIDATION_FAILURE",
System.currentTimeMillis() - start,
e.getMessage()
));
throw e;
}
}
}

5. Rate Limiting

@Component
public class ValidationRateLimiter {
private final RateLimiter rateLimiter = RateLimiter.create(100); // 100 per second
public boolean allowValidation(String token) {
return rateLimiter.tryAcquire();
}
}

Configuration Examples

application.yml

jwt:
validation:
allowed-algorithms: RS256,ES256,EdDSA
allowed-issuers: https://auth.example.com,https://accounts.google.com
required-claims: sub,exp,iat,iss
leeway-seconds: 5
enable-revocation: true
enable-caching: true
cache-size: 1000
cache-ttl: 300 # seconds
keys:
jwks-url: https://auth.example.com/.well-known/jwks.json
refresh-interval: 3600 # seconds
fallback-key: ${JWT_SECRET_KEY}

Conclusion

Key Best Practices Summary

  1. Always verify signatures - Never trust unsigned tokens
  2. Validate algorithm - Prevent algorithm confusion attacks
  3. Check expiration - Reject expired tokens
  4. Validate issuer and audience - Ensure token is for your application
  5. Use strong keys - RSA 2048+ or ECDSA with NIST P-256+
  6. Implement revocation - Blacklist compromised tokens
  7. Add token binding - Bind tokens to client characteristics
  8. Cache validation results - Improve performance
  9. Monitor metrics - Track validation failures
  10. Rotate keys regularly - Limit exposure of compromised keys

Common Vulnerabilities to Avoid

VulnerabilityMitigation
Algorithm confusionExplicitly allow only specific algorithms
Missing signature verificationAlways verify with proper key
Weak keysEnforce minimum key strength
Token replayUse short expiration and JTI tracking
Information disclosureDon't include sensitive data in payload

Performance Considerations

  • Cache validated tokens with JTI
  • Use efficient key lookup (kid indexing)
  • Implement JWKS with caching
  • Consider async validation for high load
  • Monitor validation latency

This implementation provides production-ready JWT validation with comprehensive security features suitable for enterprise applications.

Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)


Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/


Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/


Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/


Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/


Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/


Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/


Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/


Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/


Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper