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

com.atlassian.connect.spring.internal.jwt.AbstractJwtReader Maven / Gradle / Ivy

The newest version!
package com.atlassian.connect.spring.internal.jwt;

import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jwt.JWTClaimsSet;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;

public abstract class AbstractJwtReader {
    /**
     * The JWT spec says that implementers "MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew".
     * Calculations of the current time for the purposes of accepting or rejecting time-based claims (e.g. "exp" and "nbf") will allow for the current time
     * being plus or minus this leeway, resulting in some time-based claims that are marginally before or after the current time being accepted instead of rejected.
     */
    private static final int TIME_CLAIM_LEEWAY_SECONDS = 30;
    private static final String UNEXPECTED_TYPE_MESSAGE_PREFIX = "Unexpected type of JSON object member with key ";
    private static final Set NUMERIC_CLAIM_NAMES = Set.of("exp", "iat", "nbf");
    private final JWSVerifier verifier;

    protected AbstractJwtReader(JWSVerifier verifier) {
        this.verifier = verifier;
    }

    protected abstract Algorithm getSupportedAlgorithm();

    /**
     * Reads and verifies the given JWT.
     * 

* NOTE: If the JWT does not include the qsh claim, verification will still succeed. * * @param jwt the serialized JWT * @param queryStringHash the expected query-string hash * @return the claims of the JWT, if it could be successfully parsed and verified * @throws JwtParseException if the JWT could not be successfully parsed * @throws JwtVerificationException if the JWT signature did not match */ public JWTClaimsSet readAndVerify(final String jwt, final String queryStringHash) throws JwtParseException, JwtVerificationException { JWSObject jwsObject = verify(jwt); JWTClaimsSet claims; try { claims = JWTClaimsSet.parse(jwsObject.getPayload().toString()); } catch (ParseException e) { // if possible, provide a hint to the add-on developer if (e.getMessage().startsWith(UNEXPECTED_TYPE_MESSAGE_PREFIX)) { String claimName = e.getMessage().replace(UNEXPECTED_TYPE_MESSAGE_PREFIX, "").replace("\"", ""); if (NUMERIC_CLAIM_NAMES.contains(claimName)) { throw new JwtInvalidClaimException(String.format("Expecting claim '%s' to be numeric but it is a string", claimName), e); } throw new JwtParseException("Perhaps a claim is of the wrong type (e.g. expecting integer but found string): " + e.getMessage(), e); } throw new JwtParseException(e); } if (claims.getIssueTime() == null || claims.getExpirationTime() == null) { throw new JwtInvalidClaimException("'exp' and 'iat' are required claims. Atlassian JWT does not allow JWTs with " + "unlimited lifetimes."); } Date now = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(now); calendar.add(Calendar.SECOND, -TIME_CLAIM_LEEWAY_SECONDS); Date nowMinusLeeway = calendar.getTime(); calendar.setTime(now); calendar.add(Calendar.SECOND, TIME_CLAIM_LEEWAY_SECONDS); Date nowPlusLeeway = calendar.getTime(); if (null != claims.getNotBeforeTime()) { // sanity check: if the token is invalid before, on and after a given time then it is always invalid and the issuer has made a mistake if (!claims.getExpirationTime().after(claims.getNotBeforeTime())) { throw new JwtInvalidClaimException(String.format("The expiration time must be after the not-before time but exp=%s and nbf=%s", claims.getExpirationTime(), claims.getNotBeforeTime())); } if (claims.getNotBeforeTime().after(nowPlusLeeway)) { throw new JwtTooEarlyException(claims.getNotBeforeTime(), now, TIME_CLAIM_LEEWAY_SECONDS); } } if (claims.getExpirationTime().before(nowMinusLeeway)) { throw new JwtExpiredException(claims.getExpirationTime(), now, TIME_CLAIM_LEEWAY_SECONDS); } if (queryStringHash != null) { Object claim = claims.getClaim(HttpRequestCanonicalizer.QUERY_STRING_HASH_CLAIM_NAME); if (claim != null && !queryStringHash.equals(claim)) { throw new JwtInvalidClaimException(String.format("Expecting claim '%s' to have value '%s' but instead it has the value '%s'", HttpRequestCanonicalizer.QUERY_STRING_HASH_CLAIM_NAME, queryStringHash, claim)); } } return claims; } private JWSObject verify(final String jwt) throws JwtParseException, JwtVerificationException { try { final JWSObject jwsObject = JWSObject.parse(jwt); Algorithm algorithm = jwsObject.getHeader().getAlgorithm(); if (!getSupportedAlgorithm().equals(algorithm)) { throw new JwtInvalidSigningAlgorithmException(String.format("Expected JWT to be signed with '%s' but it was signed with '%s' instead", getSupportedAlgorithm(), algorithm)); } if (!jwsObject.verify(verifier)) { throw new JwtSignatureMismatchException(jwt); } return jwsObject; } catch (ParseException e) { throw new JwtParseException(e); } catch (JOSEException e) { throw new JwtSignatureMismatchException(e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy