de.cidaas.jwt.JWTVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cidaas-interceptor-spring-security Show documentation
Show all versions of cidaas-interceptor-spring-security Show documentation
Interceptor for Cidaas Java Spring Clients
package de.cidaas.jwt;
import de.cidaas.jwt.algorithms.Algorithm;
import de.cidaas.jwt.exceptions.*;
import de.cidaas.jwt.impl.JWTParser;
import de.cidaas.jwt.impl.PublicClaims;
import de.cidaas.jwt.interfaces.Claim;
import de.cidaas.jwt.interfaces.Clock;
import de.cidaas.jwt.interfaces.DecodedJWT;
import de.cidaas.jwt.interfaces.Verification;
import java.util.*;
/**
* The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also it's signature matches.
*/
@SuppressWarnings("WeakerAccess")
public final class JWTVerifier implements de.cidaas.jwt.interfaces.JWTVerifier {
private final Algorithm algorithm;
final Map claims;
private final Clock clock;
private final JWTParser parser;
JWTVerifier(Algorithm algorithm, Map claims, Clock clock) {
this.algorithm = algorithm;
this.claims = Collections.unmodifiableMap(claims);
this.clock = clock;
this.parser = new JWTParser();
}
/**
* Initialize a JWTVerifier instance using the given Algorithm.
*
* @param algorithm the Algorithm to use on the JWT verification.
* @return a JWTVerifier.Verification instance to configure.
* @throws IllegalArgumentException if the provided algorithm is null.
*/
static Verification init(Algorithm algorithm) throws IllegalArgumentException {
return new BaseVerification(algorithm);
}
/**
* The Verification class holds the Claims required by a JWT to be valid.
*/
public static class BaseVerification implements Verification {
private final Algorithm algorithm;
private final Map claims;
private long defaultLeeway;
private boolean ignoreIssuedAt;
BaseVerification(Algorithm algorithm) throws IllegalArgumentException {
if (algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
}
this.algorithm = algorithm;
this.claims = new HashMap();
this.defaultLeeway = 0;
}
/**
* Require a specific Issuer ("iss") claim.
*
* @param issuer the required Issuer value. If multiple values are given, the claim must at least match one of them
* @return this same Verification instance.
*/
@Override
public Verification withIssuer(String... issuer) {
requireClaim(PublicClaims.ISSUER, issuer == null ? null : Arrays.asList(issuer));
return this;
}
/**
* Require a specific Subject ("sub") claim.
*
* @param subject the required Subject value
* @return this same Verification instance.
*/
@Override
public Verification withSubject(String subject) {
requireClaim(PublicClaims.SUBJECT, subject);
return this;
}
/**
* Require a specific Audience ("aud") claim.
*
* @param audience the required Audience value
* @return this same Verification instance.
*/
@Override
public Verification withAudience(String... audience) {
requireClaim(PublicClaims.AUDIENCE, audience == null ? null : Arrays.asList(audience));
return this;
}
/**
* Define the default window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid.
* Setting a specific leeway value on a given Claim will override this value for that Claim.
*
* @param leeway the window in seconds in which the Not Before, Issued At and Expires At Claims will still be valid.
* @return this same Verification instance.
* @throws IllegalArgumentException if leeway is negative.
*/
@Override
public Verification acceptLeeway(long leeway) throws IllegalArgumentException {
assertPositive(leeway);
this.defaultLeeway = leeway;
return this;
}
/**
* Set a specific leeway window in seconds in which the Expires At ("exp") Claim will still be valid.
* Expiration Date is always verified when the value is present. This method overrides the value set with acceptLeeway
*
* @param leeway the window in seconds in which the Expires At Claim will still be valid.
* @return this same Verification instance.
* @throws IllegalArgumentException if leeway is negative.
*/
@Override
public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException {
assertPositive(leeway);
requireClaim(PublicClaims.EXPIRES_AT, leeway);
return this;
}
/**
* Set a specific leeway window in seconds in which the Not Before ("nbf") Claim will still be valid.
* Not Before Date is always verified when the value is present. This method overrides the value set with acceptLeeway
*
* @param leeway the window in seconds in which the Not Before Claim will still be valid.
* @return this same Verification instance.
* @throws IllegalArgumentException if leeway is negative.
*/
@Override
public Verification acceptNotBefore(long leeway) throws IllegalArgumentException {
assertPositive(leeway);
requireClaim(PublicClaims.NOT_BEFORE, leeway);
return this;
}
/**
* Set a specific leeway window in seconds in which the Issued At ("iat") Claim will still be valid.
* Issued At Date is always verified when the value is present. This method overrides the value set with acceptLeeway
*
* @param leeway the window in seconds in which the Issued At Claim will still be valid.
* @return this same Verification instance.
* @throws IllegalArgumentException if leeway is negative.
*/
@Override
public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException {
assertPositive(leeway);
requireClaim(PublicClaims.ISSUED_AT, leeway);
return this;
}
/**
* Skip the Issued At ("iat") date verification. By default, the verification is performed.
*/
public Verification ignoreIssuedAt() {
this.ignoreIssuedAt = true;
return this;
}
/**
* Require a specific JWT Id ("jti") claim.
*
* @param jwtId the required Id value
* @return this same Verification instance.
*/
@Override
public Verification withJWTId(String jwtId) {
requireClaim(PublicClaims.JWT_ID, jwtId);
return this;
}
/**
* Require a specific Claim value.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withClaim(String name, Boolean value) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, value);
return this;
}
/**
* Require a specific Claim value.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withClaim(String name, Integer value) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, value);
return this;
}
/**
* Require a specific Claim value.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withClaim(String name, Long value) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, value);
return this;
}
/**
* Require a specific Claim value.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withClaim(String name, Double value) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, value);
return this;
}
/**
* Require a specific Claim value.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withClaim(String name, String value) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, value);
return this;
}
/**
* Require a specific Claim value.
*
* @param name the Claim's name.
* @param value the Claim's value.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withClaim(String name, Date value) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, value);
return this;
}
/**
* Require a specific Array Claim to contain at least the given items.
*
* @param name the Claim's name.
* @param items the items the Claim must contain.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withArrayClaim(String name, String... items) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, items);
return this;
}
/**
* Require a specific Array Claim to contain at least the given items.
*
* @param name the Claim's name.
* @param items the items the Claim must contain.
* @return this same Verification instance.
* @throws IllegalArgumentException if the name is null.
*/
@Override
public Verification withArrayClaim(String name, Integer... items) throws IllegalArgumentException {
assertNonNull(name);
requireClaim(name, items);
return this;
}
/**
* Creates a new and reusable instance of the JWTVerifier with the configuration already provided.
*
* @return a new JWTVerifier instance.
*/
@Override
public JWTVerifier build() {
return this.build(new ClockImpl());
}
/**
* Creates a new and reusable instance of the JWTVerifier with the configuration already provided.
* ONLY FOR TEST PURPOSES.
*
* @param clock the instance that will handle the current time.
* @return a new JWTVerifier instance with a custom Clock.
*/
public JWTVerifier build(Clock clock) {
addLeewayToDateClaims();
return new JWTVerifier(algorithm, claims, clock);
}
private void assertPositive(long leeway) {
if (leeway < 0) {
throw new IllegalArgumentException("Leeway value can't be negative.");
}
}
private void assertNonNull(String name) {
if (name == null) {
throw new IllegalArgumentException("The Custom Claim's name can't be null.");
}
}
private void addLeewayToDateClaims() {
if (!claims.containsKey(PublicClaims.EXPIRES_AT)) {
claims.put(PublicClaims.EXPIRES_AT, defaultLeeway);
}
if (!claims.containsKey(PublicClaims.NOT_BEFORE)) {
claims.put(PublicClaims.NOT_BEFORE, defaultLeeway);
}
if(ignoreIssuedAt) {
claims.remove(PublicClaims.ISSUED_AT);
return;
}
if (!claims.containsKey(PublicClaims.ISSUED_AT)) {
claims.put(PublicClaims.ISSUED_AT, defaultLeeway);
}
}
private void requireClaim(String name, Object value) {
if (value == null) {
claims.remove(name);
return;
}
claims.put(name, value);
}
}
/**
* Perform the verification against the given Token, using any previous configured options.
*
* @param token to verify.
* @return a verified and decoded JWT.
* @throws AlgorithmMismatchException if the algorithm stated in the token's header it's not equal to the one defined in the {@link JWTVerifier}.
* @throws SignatureVerificationException if the signature is invalid.
* @throws TokenExpiredException if the token has expired.
* @throws InvalidClaimException if a claim contained a different value than the expected one.
*/
@Override
public DecodedJWT verify(String token) throws JWTVerificationException {
DecodedJWT jwt = new JWTDecoder(parser, token);
return verify(jwt);
}
/**
* Perform the verification against the given decoded JWT, using any previous configured options.
*
* @param jwt to verify.
* @return a verified and decoded JWT.
* @throws AlgorithmMismatchException if the algorithm stated in the token's header it's not equal to the one defined in the {@link JWTVerifier}.
* @throws SignatureVerificationException if the signature is invalid.
* @throws TokenExpiredException if the token has expired.
* @throws InvalidClaimException if a claim contained a different value than the expected one.
*/
@Override
public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException {
verifyAlgorithm(jwt, algorithm);
algorithm.verify(jwt);
verifyClaims(jwt, claims);
return jwt;
}
private void verifyAlgorithm(DecodedJWT jwt, Algorithm expectedAlgorithm) throws AlgorithmMismatchException {
if (!expectedAlgorithm.getName().equals(jwt.getAlgorithm())) {
throw new AlgorithmMismatchException("The provided Algorithm doesn't match the one defined in the JWT's Header.");
}
}
private void verifyClaims(DecodedJWT jwt, Map claims) throws TokenExpiredException, InvalidClaimException {
for (Map.Entry entry : claims.entrySet()) {
switch (entry.getKey()) {
case PublicClaims.AUDIENCE:
assertValidAudienceClaim(jwt.getAudience(), (List) entry.getValue());
break;
case PublicClaims.EXPIRES_AT:
assertValidDateClaim(jwt.getExpiresAt(), (Long) entry.getValue(), true);
break;
case PublicClaims.ISSUED_AT:
assertValidDateClaim(jwt.getIssuedAt(), (Long) entry.getValue(), false);
break;
case PublicClaims.NOT_BEFORE:
assertValidDateClaim(jwt.getNotBefore(), (Long) entry.getValue(), false);
break;
case PublicClaims.ISSUER:
assertValidIssuerClaim(jwt.getIssuer(), (List) entry.getValue());
break;
case PublicClaims.JWT_ID:
assertValidStringClaim(entry.getKey(), jwt.getId(), (String) entry.getValue());
break;
case PublicClaims.SUBJECT:
assertValidStringClaim(entry.getKey(), jwt.getSubject(), (String) entry.getValue());
break;
default:
assertValidClaim(jwt.getClaim(entry.getKey()), entry.getKey(), entry.getValue());
break;
}
}
}
private void assertValidClaim(Claim claim, String claimName, Object value) {
boolean isValid = false;
if (value instanceof String) {
isValid = value.equals(claim.asString());
} else if (value instanceof Integer) {
isValid = value.equals(claim.asInt());
} else if (value instanceof Long) {
isValid = value.equals(claim.asLong());
} else if (value instanceof Boolean) {
isValid = value.equals(claim.asBoolean());
} else if (value instanceof Double) {
isValid = value.equals(claim.asDouble());
} else if (value instanceof Date) {
isValid = value.equals(claim.asDate());
} else if (value instanceof Object[]) {
List