java-jwt builds tokens with JWT.create()…sign(algorithm) and verifies them with JWT.require(algorithm)…build().verify(token). The Algorithm object carries both the algorithm and the key, so a verifier only ever accepts that one algorithm: an RS256 token, an unsigned token and an HS256 token forged with the EC key's PEM text all failed against an ES256 verifier with AlgorithmMismatchException.
The protection holds only while the Algorithm is chosen from configuration. JWT.require(Algorithm.none()) accepts unsigned tokens, and Algorithm.HMAC256(pemBytes) verified the forged HS256 token — code that picks the Algorithm from the token's alg header, or loads keys as raw bytes, recreates the confusion bugs the API otherwise prevents. JWT.decode() parses without checking the signature or any claim; it returned a token that had expired an hour earlier.
The algorithm set is HMAC256/384/512, RSA256/384/512 and ECDSA256/384/512 — no PS256, no ES256K and no EdDSA — so tokens from issuers that moved to RSA-PSS or Ed25519 need Nimbus or JJWT. ES256 refuses a P-384 key at signing time.
ES256 signatures are the 64-byte raw r‖s, and a DER signature fails with the generic The Token's Signature resulted invalid when verified using the Algorithm: SHA256withECDSA; the JWT debugger shows the signature length. There is no default leeway and exp is optional: the sample adds withClaimPresence("exp") and acceptLeeway(30) along with the issuer check.