All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.auth0.jwt.JWTVerifier Maven / Gradle / Ivy

There is a newer version: 4.4.0
Show newest version
package com.auth0.jwt;

import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.InvalidClaimException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.impl.PublicClaims;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.codec.binary.Base64;

import java.nio.charset.StandardCharsets;
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 {
    private final Algorithm algorithm;
    final Map claims;
    private final Clock clock;

    JWTVerifier(Algorithm algorithm, Map claims, Clock clock) {
        this.algorithm = algorithm;
        this.claims = Collections.unmodifiableMap(claims);
        this.clock = clock;
    }

    /**
     * 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 JWTVerifier.Verification init(Algorithm algorithm) throws IllegalArgumentException {
        return new Verification(algorithm);
    }

    /**
     * The Verification class holds the Claims required by a JWT to be valid.
     */
    public static class Verification {
        private final Algorithm algorithm;
        private final Map claims;
        private long defaultLeeway;

        Verification(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
         * @return this same Verification instance.
         */
        public Verification withIssuer(String issuer) {
            requireClaim(PublicClaims.ISSUER, issuer);
            return this;
        }

        /**
         * Require a specific Subject ("sub") claim.
         *
         * @param subject the required Subject value
         * @return this same Verification instance.
         */
        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.
         */
        public Verification withAudience(String... audience) {
            requireClaim(PublicClaims.AUDIENCE, Arrays.asList(audience));
            return this;
        }

        /**
         * Define the default window in milliseconds 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.
         */
        public Verification acceptLeeway(long leeway) throws IllegalArgumentException {
            if (leeway < 0) {
                throw new IllegalArgumentException("Leeway value can't be negative.");
            }
            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.
         */
        public Verification acceptExpiresAt(long leeway) throws IllegalArgumentException {
            if (leeway < 0) {
                throw new IllegalArgumentException("Leeway value can't be negative.");
            }
            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.
         */
        public Verification acceptNotBefore(long leeway) throws IllegalArgumentException {
            if (leeway < 0) {
                throw new IllegalArgumentException("Leeway value can't be negative.");
            }
            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.
         */
        public Verification acceptIssuedAt(long leeway) throws IllegalArgumentException {
            if (leeway < 0) {
                throw new IllegalArgumentException("Leeway value can't be negative.");
            }
            requireClaim(PublicClaims.ISSUED_AT, leeway);
            return this;
        }

        /**
         * Require a specific JWT Id ("jti") claim.
         *
         * @param jwtId the required Id value
         * @return this same Verification instance.
         */
        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. Must be an instance of Integer, Double, Boolean, Date or String class.
         * @return this same Verification instance.
         * @throws IllegalArgumentException if the name is null or the value class is not allowed.
         */
        public Verification withClaim(String name, Object value) throws IllegalArgumentException {
            final boolean validValue = value instanceof Integer || value instanceof Double ||
                    value instanceof Boolean || value instanceof Date || value instanceof String;
            if (name == null) {
                throw new IllegalArgumentException("The Custom Claim's name can't be null.");
            }
            if (!validValue) {
                throw new IllegalArgumentException("The Custom Claim's value class must be an instance of Integer, Double, Boolean, Date or String.");
            }

            requireClaim(name, value);
            return this;
        }

        /**
         * Creates a new and reusable instance of the JWTVerifier with the configuration already provided.
         *
         * @return a new JWTVerifier instance.
         */
        public JWTVerifier build() {
            return this.build(new Clock());
        }

        /**
         * 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.
         */
        JWTVerifier build(Clock clock) {
            addLeewayToDateClaims();
            return new JWTVerifier(algorithm, claims, clock);
        }

        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 (!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 JWTVerificationException if any of the required contents inside the JWT is invalid.
     */
    public DecodedJWT verify(String token) throws JWTVerificationException {
        DecodedJWT jwt = JWTDecoder.decode(token);
        verifyAlgorithm(jwt, algorithm);
        verifySignature(TokenUtils.splitToken(token));
        verifyClaims(jwt, claims);
        return jwt;
    }

    private void verifySignature(String[] parts) throws SignatureVerificationException {
        byte[] content = String.format("%s.%s", parts[0], parts[1]).getBytes(StandardCharsets.UTF_8);
        byte[] signature = Base64.decodeBase64(parts[2]);
        algorithm.verify(content, signature);
    }

    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) {
        for (Map.Entry entry : claims.entrySet()) {
            switch (entry.getKey()) {
                case PublicClaims.AUDIENCE:
                    //noinspection unchecked
                    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:
                    assertValidStringClaim(entry.getKey(), jwt.getIssuer(), (String) 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 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());
        }

        if (!isValid) {
            throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName));
        }
    }

    private void assertValidStringClaim(String claimName, String value, String expectedValue) {
        if (!expectedValue.equals(value)) {
            throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName));
        }
    }

    private void assertValidDateClaim(Date date, long leeway, boolean shouldBeFuture) {
        Date today = clock.getToday();
        today.setTime((long) Math.floor((today.getTime() / 1000) * 1000)); //truncate millis
        boolean isValid;
        String errMessage;
        if (shouldBeFuture) {
            today.setTime(today.getTime() - leeway * 1000);
            isValid = date == null || !today.after(date);
            errMessage = String.format("The Token has expired on %s.", date);
        } else {
            today.setTime(today.getTime() + leeway * 1000);
            isValid = date == null || !today.before(date);
            errMessage = String.format("The Token can't be used before %s.", date);
        }
        if (!isValid) {
            throw new InvalidClaimException(errMessage);
        }
    }

    private void assertValidAudienceClaim(List audience, List value) {
        if (audience == null || !audience.containsAll(value)) {
            throw new InvalidClaimException("The Claim 'aud' value doesn't contain the required audience.");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy