001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2016, Connect2id Ltd. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.jwt.proc; 019 020 021import java.util.*; 022 023import com.nimbusds.jwt.JWTClaimNames; 024import net.jcip.annotations.ThreadSafe; 025 026import com.nimbusds.jose.proc.SecurityContext; 027import com.nimbusds.jwt.JWTClaimsSet; 028import com.nimbusds.jwt.util.DateUtils; 029 030 031/** 032 * A {@link JWTClaimsSetVerifier JWT claims verifier} implementation. 033 * 034 * <p>Configurable checks: 035 * 036 * <ol> 037 * <li>Specify JWT claims that must be present and which values must match 038 * exactly, for example the expected JWT issuer ("iss") and audience 039 * ("aud"). 040 * <li>Specify JWT claims that must be present, for example expiration 041 * ("exp") and not-before ("nbf") times. If the "exp" or "nbf" claims 042 * are marked as required they will be automatically checked against 043 * the current time. 044 * <li>Specify JWT claims that are prohibited, for example to prevent 045 * cross-JWT confusion in situations when explicit JWT typing via the 046 * type ("typ") header is not used. 047 * </ol> 048 * 049 * <p>Performs the following time validity checks: 050 * 051 * <ol> 052 * <li>If an expiration time ("exp") claim is present, makes sure it is 053 * ahead of the current time, else the JWT claims set is rejected. 054 * <li>If a not-before-time ("nbf") claim is present, makes sure it is 055 * before the current time, else the JWT claims set is rejected. 056 * </ol> 057 * 058 * <p>Note, to enforce a time validity check the claim ("exp" and / or "nbf" ) 059 * must be set as required. 060 * 061 * <p>Example verifier with exact matches for "iss" and "aud", and setting the 062 * "exp", "nbf" and "jti" claims as required to be present: 063 * 064 * <pre> 065 * DefaultJWTClaimsVerifier<?> verifier = new DefaultJWTClaimsVerifier<>( 066 * new JWTClaimsSet.Builder() 067 * .issuer("https://issuer.example.com") 068 * .audience("https://client.example.com") 069 * .build(), 070 * new HashSet<>(Arrays.asList("exp", "nbf", "jti"))); 071 * 072 * verifier.verify(jwtClaimsSet, null); 073 * </pre> 074 * 075 * <p>This class may be extended to perform additional checks. 076 * 077 * <p>This class is thread-safe. 078 * 079 * @author Vladimir Dzhuvinov 080 * @version 2021-06-05 081 */ 082@ThreadSafe 083public class DefaultJWTClaimsVerifier <C extends SecurityContext> implements JWTClaimsSetVerifier<C>, ClockSkewAware { 084 085 086 /** 087 * The default maximum acceptable clock skew, in seconds (60). 088 */ 089 public static final int DEFAULT_MAX_CLOCK_SKEW_SECONDS = 60; 090 091 092 /** 093 * The maximum acceptable clock skew, in seconds. 094 */ 095 private int maxClockSkew = DEFAULT_MAX_CLOCK_SKEW_SECONDS; 096 097 098 /** 099 * The accepted audience values, {@code null} if not specified. A 100 * {@code null} value present in the set allows JWTs with no audience. 101 */ 102 private final Set<String> acceptedAudienceValues; 103 104 105 /** 106 * The JWT claims that must match exactly, empty set if none. 107 */ 108 private final JWTClaimsSet exactMatchClaims; 109 110 111 /** 112 * The names of the JWT claims that must be present, empty set if none. 113 */ 114 private final Set<String> requiredClaims; 115 116 117 /** 118 * The names of the JWT claims that must not be present, empty set if 119 * none. 120 */ 121 private final Set<String> prohibitedClaims; 122 123 124 /** 125 * Creates a new JWT claims verifier. No audience ("aud"), required and 126 * prohibited claims are specified. The expiration ("exp") and 127 * not-before ("nbf") claims will be checked only if they are present 128 * and parsed successfully. 129 * 130 * @deprecated Use a more specific constructor that at least specifies 131 * a list of required JWT claims. 132 */ 133 @Deprecated 134 public DefaultJWTClaimsVerifier() { 135 this(null, null, null, null); 136 } 137 138 139 /** 140 * Creates a new JWT claims verifier. Allows any audience ("aud") 141 * unless an exact match is specified. The expiration ("exp") and 142 * not-before ("nbf") claims will be checked only if they are present 143 * and parsed successfully; add them to the required claims if they are 144 * mandatory. 145 * 146 * @param exactMatchClaims The JWT claims that must match exactly, 147 * {@code null} if none. 148 * @param requiredClaims The names of the JWT claims that must be 149 * present, empty set or {@code null} if none. 150 */ 151 public DefaultJWTClaimsVerifier(final JWTClaimsSet exactMatchClaims, 152 final Set<String> requiredClaims) { 153 154 this(null, exactMatchClaims, requiredClaims, null); 155 } 156 157 158 /** 159 * Creates new default JWT claims verifier. The expiration ("exp") and 160 * not-before ("nbf") claims will be checked only if they are present 161 * and parsed successfully; add them to the required claims if they are 162 * mandatory. 163 * 164 * @param requiredAudience The required JWT audience, {@code null} if 165 * not specified. 166 * @param exactMatchClaims The JWT claims that must match exactly, 167 * {@code null} if none. 168 * @param requiredClaims The names of the JWT claims that must be 169 * present, empty set or {@code null} if none. 170 */ 171 public DefaultJWTClaimsVerifier(final String requiredAudience, 172 final JWTClaimsSet exactMatchClaims, 173 final Set<String> requiredClaims) { 174 175 this(requiredAudience != null ? Collections.singleton(requiredAudience) : null, 176 exactMatchClaims, 177 requiredClaims, 178 null); 179 } 180 181 182 /** 183 * Creates new default JWT claims verifier. The expiration ("exp") and 184 * not-before ("nbf") claims will be checked only if they are present 185 * and parsed successfully; add them to the required claims if they are 186 * mandatory. 187 * 188 * @param acceptedAudience The accepted JWT audience values, 189 * {@code null} if not specified. A 190 * {@code null} value in the set allows JWTs 191 * with no audience. 192 * @param exactMatchClaims The JWT claims that must match exactly, 193 * {@code null} if none. 194 * @param requiredClaims The names of the JWT claims that must be 195 * present, empty set or {@code null} if none. 196 * @param prohibitedClaims The names of the JWT claims that must not be 197 * present, empty set or {@code null} if none. 198 */ 199 public DefaultJWTClaimsVerifier(final Set<String> acceptedAudience, 200 final JWTClaimsSet exactMatchClaims, 201 final Set<String> requiredClaims, 202 final Set<String> prohibitedClaims) { 203 204 this.acceptedAudienceValues = acceptedAudience != null ? Collections.unmodifiableSet(acceptedAudience) : null; 205 206 this.exactMatchClaims = exactMatchClaims != null ? exactMatchClaims : new JWTClaimsSet.Builder().build(); 207 208 Set<String> requiredClaimsCopy = new HashSet<>(this.exactMatchClaims.getClaims().keySet()); 209 if (acceptedAudienceValues != null && ! acceptedAudienceValues.contains(null)) { 210 // check if an explicit aud is required 211 requiredClaimsCopy.add(JWTClaimNames.AUDIENCE); 212 } 213 if (requiredClaims != null) { 214 requiredClaimsCopy.addAll(requiredClaims); 215 } 216 this.requiredClaims = Collections.unmodifiableSet(requiredClaimsCopy); 217 218 this.prohibitedClaims = prohibitedClaims != null ? Collections.unmodifiableSet(prohibitedClaims) : Collections.<String>emptySet(); 219 } 220 221 222 /** 223 * Returns the accepted audience values. 224 * 225 * @return The accepted JWT audience values, {@code null} if not 226 * specified. A {@code null} value in the set allows JWTs with 227 * no audience. 228 */ 229 public Set<String> getAcceptedAudienceValues() { 230 return acceptedAudienceValues; 231 } 232 233 234 /** 235 * Returns the JWT claims that must match exactly. 236 * 237 * @return The JWT claims that must match exactly, empty set if none. 238 */ 239 public JWTClaimsSet getExactMatchClaims() { 240 return exactMatchClaims; 241 } 242 243 244 /** 245 * Returns the names of the JWT claims that must be present, including 246 * the name of those that must match exactly. 247 * 248 * @return The names of the JWT claims that must be present, empty set 249 * if none. 250 */ 251 public Set<String> getRequiredClaims() { 252 return requiredClaims; 253 } 254 255 256 /** 257 * Returns the names of the JWT claims that must not be present. 258 * 259 * @return The names of the JWT claims that must not be present, empty 260 * set if none. 261 */ 262 public Set<String> getProhibitedClaims() { 263 return prohibitedClaims; 264 } 265 266 267 @Override 268 public int getMaxClockSkew() { 269 return maxClockSkew; 270 } 271 272 273 @Override 274 public void setMaxClockSkew(final int maxClockSkewSeconds) { 275 maxClockSkew = maxClockSkewSeconds; 276 } 277 278 279 @Override 280 public void verify(final JWTClaimsSet claimsSet, final C context) 281 throws BadJWTException { 282 283 // Check audience 284 if (acceptedAudienceValues != null) { 285 List<String> audList = claimsSet.getAudience(); 286 if (audList != null && ! audList.isEmpty()) { 287 boolean audMatch = false; 288 for (String aud : audList) { 289 if (acceptedAudienceValues.contains(aud)) { 290 audMatch = true; 291 break; 292 } 293 } 294 if (! audMatch) { 295 throw new BadJWTException("JWT audience rejected: " + audList); 296 } 297 } else if (! acceptedAudienceValues.contains(null)) { 298 throw new BadJWTException("JWT missing required audience"); 299 } 300 } 301 302 // Check if all required claims are present 303 if (! claimsSet.getClaims().keySet().containsAll(requiredClaims)) { 304 Set<String> missingClaims = new HashSet<>(requiredClaims); 305 missingClaims.removeAll(claimsSet.getClaims().keySet()); 306 throw new BadJWTException("JWT missing required claims: " + missingClaims); 307 } 308 309 // Check if prohibited claims are present 310 Set<String> presentProhibitedClaims = new HashSet<>(); 311 for (String prohibited: prohibitedClaims) { 312 if (claimsSet.getClaims().containsKey(prohibited)) { 313 presentProhibitedClaims.add(prohibited); 314 } 315 if (! presentProhibitedClaims.isEmpty()) { 316 throw new BadJWTException("JWT has prohibited claims: " + presentProhibitedClaims); 317 } 318 } 319 320 // Check exact matches 321 for (String exactMatch: exactMatchClaims.getClaims().keySet()) { 322 Object actualClaim = claimsSet.getClaim(exactMatch); 323 Object expectedClaim = exactMatchClaims.getClaim(exactMatch); 324 if (! actualClaim.equals(expectedClaim)) { 325 throw new BadJWTException("JWT " + exactMatch + " claim has value " + actualClaim + ", must be " + expectedClaim); 326 } 327 } 328 329 // Check time window 330 final Date now = new Date(); 331 332 final Date exp = claimsSet.getExpirationTime(); 333 if (exp != null) { 334 335 if (! DateUtils.isAfter(exp, now, maxClockSkew)) { 336 throw new BadJWTException("Expired JWT"); 337 } 338 } 339 340 final Date nbf = claimsSet.getNotBeforeTime(); 341 if (nbf != null) { 342 343 if (! DateUtils.isBefore(nbf, now, maxClockSkew)) { 344 throw new BadJWTException("JWT before use time"); 345 } 346 } 347 } 348}