Manually Validating a JWT Token in Java Without External Dependencies

JWT (JSON Web Token) is a widely used method for securely transmitting information between two parties. Most applications that deal with OAuth2 or OpenID Connect use JWT tokens for authentication and authorization.

In Java, validating a JWT token is often made easier with libraries like Spring Security or libraries that handle OAuth2. However, there are scenarios where you might need to validate a JWT manually, without relying on any external libraries or dependencies.

This blog post will walk you through the process of manually validating a JWT token in Java using only core Java classes. We'll cover the following steps:

  1. JWT structure and decoding

  2. Validating claims (issuer, audience, and expiration)

  3. Fetching the JWKS (JSON Web Key Set) and constructing the RSA public key

  4. Verifying the JWT signature

JWT Structure Overview

A JWT token consists of three parts:

  • Header: Metadata, such as the signing algorithm and token type.

  • Payload: The actual claims or data.

  • Signature: Ensures the token hasn't been tampered with.

Each part is Base64URL-encoded and concatenated with dots (.). Example of a JWT:

This JWT can be broken into:

  1. Header: { "alg": "RS256", "typ": "JWT" }

  2. Payload: { "sub": "1234567890", "name": "John Doe", "iss": "https://login.microsoftonline.com/tenant", "aud": "client_id", "exp": 1616547400 }

  3. Signature: The signed hash of the header and payload using the server's private key.

Step 1: Decoding the JWT

To manually validate a JWT, we first decode the header and payload.

String token = "YOUR_JWT_TOKEM";
String[] parts = token.split("\\.");
if (parts.length != 3) {
    throw new IllegalArgumentException("Invalid JWT token");
}

// Decode header and payload
String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]));
String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]));

System.out.println("Header: " + headerJson);
System.out.println("Payload: " + payloadJson);

After decoding, we parse the JSON to validate the token's claims (such as the issuer, audience, and expiration).

Step 2: Validating JWT Claims

We check the issuer (iss), audience (aud), and expiration time (exp) to ensure that the token is valid for our application.

JSONObject payload = new JSONObject(payloadJson);

// Validate expiration
long exp = payload.getLong("exp");
if (Instant.now().getEpochSecond() > exp) {
    throw new RuntimeException("Token is expired");
}

// Validate issuer
String iss = payload.getString("iss");
if (!iss.equals(EXPECTED_ISSUER)) {
    throw new RuntimeException("Invalid token issuer");
}

// Validate audience
String aud = payload.getString("aud");
if (!aud.equals(EXPECTED_AUDIENCE)) {
    throw new RuntimeException("Invalid token audience");
}

In this step, we ensure the token hasn’t expired and is issued by a trusted authority. The iss and aud values will come from your JWT provider’s documentation.

Step 3: Fetching the JWKS (JSON Web Key Set)

JWT tokens signed with RS256 are verified using a public key, which is typically part of a JWKS (JSON Web Key Set). The JWKS is a URL where the public keys are stored.

To validate the JWT’s signature, we need to retrieve the corresponding public key from the JWKS using the Key ID (kid) in the token's header.

JSONObject header = new JSONObject(headerJson);
String kid = header.getString("kid");

PublicKey publicKey = getPublicKey(kid);

Fetching the JWKS:

private static JSONObject fetchJwks(String jwksUrl) throws Exception {
    URL url = new URL(jwksUrl);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("GET");

    try (InputStreamReader reader = new InputStreamReader(connection.getInputStream())) {
        int length;
        char[] buffer = new char[4096];
        StringBuilder result = new StringBuilder();
        while ((length = reader.read(buffer)) != -1) {
            result.append(buffer, 0, length);
        }
        return new JSONObject(result.toString());
    }
}

Once the JWKS is retrieved, we extract the public key from the modulus and exponent of the kid.

Step 4: Constructing the RSA Public Key

The modulus and exponent are extracted from the JWKS response and used to create an RSA public key.

private static PublicKey constructPublicKey(byte[] modulus, byte[] exponent) throws Exception {
    BigInteger modBigInt = new BigInteger(1, modulus);
    BigInteger expBigInt = new BigInteger(1, exponent);

    RSAPublicKeySpec rsaPublicKeySpec = new RSAPublicKeySpec(modBigInt, expBigInt);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");

    return keyFactory.generatePublic(rsaPublicKeySpec);
}

Step 5: Verifying the JWT Signature

Once we have the public key, we verify the JWT signature to ensure the token hasn't been tampered with.

private static boolean verifySignature(String headerAndPayload, String signature, PublicKey publicKey) throws Exception {
    byte[] signatureBytes = Base64.getUrlDecoder().decode(signature);

    Signature sig = Signature.getInstance("SHA256withRSA");
    sig.initVerify(publicKey);
    sig.update(headerAndPayload.getBytes());

    return sig.verify(signatureBytes);
}

Here, the signature is verified using the RSA public key with the SHA256withRSA algorithm.

Putting It All Together

After decoding the token, validating claims, fetching the JWKS, constructing the public key, and verifying the signature, you can confidently validate the JWT manually.

public static void validateToken(String token) throws Exception {
    String[] parts = token.split("\\.");
    String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]));
    String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]));
    String signature = parts[2];

    JSONObject header = new JSONObject(headerJson);
    JSONObject payload = new JSONObject(payloadJson);

    // Validate claims
    validateClaims(payload);

    // Fetch public key and verify signature
    PublicKey publicKey = getPublicKey(header.getString("kid"));
    boolean isValidSignature = verifySignature(parts[0] + "." + parts[1], signature, publicKey);

    if (!isValidSignature) {
        throw new RuntimeException("Invalid JWT signature");
    }

    System.out.println("JWT is valid!");
}

Conclusion

Validating JWT tokens manually in Java provides more flexibility and control when you don't want to rely on external libraries. While this approach takes more effort, it can be helpful in situations where you need custom validation logic or you're working in an environment where adding extra dependencies is not possible.

By following the steps above, you can securely validate JWT tokens in your application without relying on Spring Security or OAuth2 libraries.