View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.security.jwt;
23  
24  import com.nimbusds.jose.JOSEException;
25  import com.nimbusds.jose.JWSAlgorithm;
26  import com.nimbusds.jose.JWSVerifier;
27  import com.nimbusds.jose.crypto.ECDSAVerifier;
28  import com.nimbusds.jose.crypto.Ed25519Verifier;
29  import com.nimbusds.jose.crypto.MACVerifier;
30  import com.nimbusds.jose.crypto.RSASSAVerifier;
31  import com.nimbusds.jose.jwk.JWK;
32  import com.nimbusds.jwt.SignedJWT;
33  
34  import org.apache.commons.lang3.StringUtils;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  import org.springframework.context.expression.MapAccessor;
38  import org.springframework.expression.Expression;
39  import org.springframework.expression.ExpressionParser;
40  import org.springframework.expression.spel.standard.SpelExpressionParser;
41  import org.springframework.expression.spel.support.StandardEvaluationContext;
42  import org.springframework.util.Assert;
43  
44  import java.text.ParseException;
45  import java.util.ArrayList;
46  import java.util.Date;
47  import java.util.List;
48  
49  /**
50   * Helper class to verify JWTs.
51   */
52  public final class JWTVerifier {
53  
54    /** Logging facility. */
55    private static final Logger logger = LoggerFactory.getLogger(JWTVerifier.class);
56  
57    private JWTVerifier() { }
58  
59    /**
60     * Verifies a given JWT string with a given JWK provider and given claim constraints.
61     *
62     * @param token The JWT string.
63     * @param retriever The JWK provider.
64     * @param claimConstraints The claim constraints.
65     * @return The decoded and verified JWT.
66     * @throws JOSEException If the JWT cannot be verified successfully.
67     * @throws java.text.ParseException If some part of the JWT cannot be parsed successfully.
68     */
69    public static SignedJWT verify(String token, JWKSetProvider retriever, List<String> claimConstraints)
70            throws JOSEException, java.text.ParseException {
71      Assert.notNull(token, "A token must be set");
72      Assert.notNull(retriever, "A JWKS retriever must be set");
73  
74      SignedJWT jwt = SignedJWT.parse(token);
75      JWSAlgorithm alg = jwt.getHeader().getAlgorithm();
76  
77      List<JWK> jwkSet = retriever.getAll();
78  
79      List<JWSVerifier> verifiers = new ArrayList<>();
80      if (alg.equals(JWSAlgorithm.RS256) || alg.equals(JWSAlgorithm.RS384) || alg.equals(JWSAlgorithm.RS512)) {
81        for (JWK jwk : jwkSet) {
82          verifiers.add(new RSASSAVerifier(jwk.toRSAKey()));
83        }
84        return verify(jwt, claimConstraints, verifiers.toArray(new JWSVerifier[0]));
85      } else if (alg.equals(JWSAlgorithm.ES256) || alg.equals(JWSAlgorithm.ES256K) || alg.equals(JWSAlgorithm.ES384)
86          || alg.equals(JWSAlgorithm.ES512)) {
87        for (JWK jwk : jwkSet) {
88          verifiers.add(new ECDSAVerifier(jwk.toECKey()));
89        }
90        return verify(jwt, claimConstraints, verifiers.toArray(new JWSVerifier[0]));
91      } else if (alg.equals(JWSAlgorithm.EdDSA) || alg.equals(JWSAlgorithm.Ed25519)) {
92        for (JWK jwk : jwkSet) {
93          verifiers.add(new Ed25519Verifier(jwk.toPublicJWK().toOctetKeyPair()));
94        }
95        return verify(jwt, claimConstraints, verifiers.toArray(new JWSVerifier[0]));
96      } else {
97        throw new IllegalArgumentException("Unsupported algorithm '" + alg + "'");
98      }
99    }
100 
101   /**
102    * Verifies a given JWT string with a secret and given claim constraints.
103    *
104    * @param token The JWT string.
105    * @param secret The secret.
106    * @param claimConstraints The claim constraints.
107    * @return The decoded and verified JWT.
108    * @throws JOSEException If the JWT cannot be verified successfully.
109    */
110   public static SignedJWT verify(String token, String secret, List<String> claimConstraints)
111           throws JOSEException, java.text.ParseException {
112     Assert.notNull(token, "A token must be set");
113     Assert.isTrue(StringUtils.isNotBlank(secret), "A secret must be set");
114 
115     SignedJWT jwt = SignedJWT.parse(token);
116     JWSAlgorithm alg = jwt.getHeader().getAlgorithm();
117 
118     if (alg.equals(JWSAlgorithm.HS256) || alg.equals(JWSAlgorithm.HS384) || alg.equals(JWSAlgorithm.HS512)) {
119       return verify(jwt, claimConstraints, new MACVerifier(secret));
120     } else {
121       throw new IllegalArgumentException("Unsupported algorithm '" + alg + "'");
122     }
123   }
124 
125   public static SignedJWT verify(SignedJWT jwt, List<String> claimConstraints, JWSVerifier... verifiers)
126           throws JOSEException {
127     Assert.notNull(jwt, "A decoded JWT must be set");
128     Assert.notNull(claimConstraints, "Claim constraints must be set");
129     Assert.notNull(verifiers, "Verifiers must be set");
130 
131     boolean verified = false;
132     Exception lastException = new JOSEException("JWT could not be verified");
133     for (JWSVerifier verifier : verifiers) {
134       try {
135         // General verification
136         if (!jwt.verify(verifier)) {
137           throw new JOSEException("JWT could not be verified");
138         }
139 
140         // Expiration date verification
141         Date expirationTime = jwt.getJWTClaimsSet().getExpirationTime();
142         if (expirationTime != null && !new Date().before(expirationTime)) {
143           throw new JOSEException("JWT is expired");
144         }
145 
146         // Claim constraints verification
147         ExpressionParser parser = new SpelExpressionParser();
148         StandardEvaluationContext ctx = new StandardEvaluationContext();
149         ctx.addPropertyAccessor(new MapAccessor());
150         for (String constraint : claimConstraints) {
151           Expression exp = parser.parseExpression(constraint);
152           if (!exp.getValue(ctx, jwt.getJWTClaimsSet().getClaims(), Boolean.class)) {
153             throw new JOSEException("The claims did not fulfill constraint '" + constraint + "'");
154           }
155         }
156 
157         // Verification was successful if no exception has been thrown
158         verified = true;
159         break;
160       } catch (JOSEException | ParseException e) {
161         // Ignore for now and try next algorithm
162         lastException = e;
163       }
164     }
165 
166     // If verification was not successful until here, throw last known exception
167     if (!verified) {
168       throw new JOSEException(lastException.getMessage());
169     }
170 
171     return jwt;
172   }
173 }