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>The {@link #currentTime()} method can be overridden to use an alternative 076 * time provider for the "exp" (expiration time) and "nbf" (not-before time) 077 * verification, or to disable "exp" and "nbf" verification entirely. 078 * 079 * <p>This class may be extended to perform additional checks. 080 * 081 * <p>This class is thread-safe. 082 * 083 * @author Vladimir Dzhuvinov 084 * @author Eugene Kuleshov 085 * @version 2021-09-28 086 */ 087@ThreadSafe 088public class DefaultJWTClaimsVerifier <C extends SecurityContext> implements JWTClaimsSetVerifier<C>, ClockSkewAware { 089 090 091 /** 092 * The default maximum acceptable clock skew, in seconds (60). 093 */ 094 public static final int DEFAULT_MAX_CLOCK_SKEW_SECONDS = 60; 095 096 097 /** 098 * The maximum acceptable clock skew, in seconds. 099 */ 100 private int maxClockSkew = DEFAULT_MAX_CLOCK_SKEW_SECONDS; 101 102 103 /** 104 * The accepted audience values, {@code null} if not specified. A 105 * {@code null} value present in the set allows JWTs with no audience. 106 */ 107 private final Set<String> acceptedAudienceValues; 108 109 110 /** 111 * The JWT claims that must match exactly, empty set if none. 112 */ 113 private final JWTClaimsSet exactMatchClaims; 114 115 116 /** 117 * The names of the JWT claims that must be present, empty set if none. 118 */ 119 private final Set<String> requiredClaims; 120 121 122 /** 123 * The names of the JWT claims that must not be present, empty set if 124 * none. 125 */ 126 private final Set<String> prohibitedClaims; 127 128 129 /** 130 * Creates a new JWT claims verifier. No audience ("aud"), required and 131 * prohibited claims are specified. The expiration ("exp") and 132 * not-before ("nbf") claims will be checked only if they are present 133 * and parsed successfully. 134 * 135 * @deprecated Use a more specific constructor that at least specifies 136 * a list of required JWT claims. 137 */ 138 @Deprecated 139 public DefaultJWTClaimsVerifier() { 140 this(null, null, null, null); 141 } 142 143 144 /** 145 * Creates a new JWT claims verifier. Allows any audience ("aud") 146 * unless an exact match is specified. The expiration ("exp") and 147 * not-before ("nbf") claims will be checked only if they are present 148 * and parsed successfully; add them to the required claims if they are 149 * mandatory. 150 * 151 * @param exactMatchClaims The JWT claims that must match exactly, 152 * {@code null} if none. 153 * @param requiredClaims The names of the JWT claims that must be 154 * present, empty set or {@code null} if none. 155 */ 156 public DefaultJWTClaimsVerifier(final JWTClaimsSet exactMatchClaims, 157 final Set<String> requiredClaims) { 158 159 this(null, exactMatchClaims, requiredClaims, null); 160 } 161 162 163 /** 164 * Creates new default JWT claims verifier. The expiration ("exp") and 165 * not-before ("nbf") claims will be checked only if they are present 166 * and parsed successfully; add them to the required claims if they are 167 * mandatory. 168 * 169 * @param requiredAudience The required JWT audience, {@code null} if 170 * not specified. 171 * @param exactMatchClaims The JWT claims that must match exactly, 172 * {@code null} if none. 173 * @param requiredClaims The names of the JWT claims that must be 174 * present, empty set or {@code null} if none. 175 */ 176 public DefaultJWTClaimsVerifier(final String requiredAudience, 177 final JWTClaimsSet exactMatchClaims, 178 final Set<String> requiredClaims) { 179 180 this(requiredAudience != null ? Collections.singleton(requiredAudience) : null, 181 exactMatchClaims, 182 requiredClaims, 183 null); 184 } 185 186 187 /** 188 * Creates new default JWT claims verifier. The expiration ("exp") and 189 * not-before ("nbf") claims will be checked only if they are present 190 * and parsed successfully; add them to the required claims if they are 191 * mandatory. 192 * 193 * @param acceptedAudience The accepted JWT audience values, 194 * {@code null} if not specified. A 195 * {@code null} value in the set allows JWTs 196 * with no audience. 197 * @param exactMatchClaims The JWT claims that must match exactly, 198 * {@code null} if none. 199 * @param requiredClaims The names of the JWT claims that must be 200 * present, empty set or {@code null} if none. 201 * @param prohibitedClaims The names of the JWT claims that must not be 202 * present, empty set or {@code null} if none. 203 */ 204 public DefaultJWTClaimsVerifier(final Set<String> acceptedAudience, 205 final JWTClaimsSet exactMatchClaims, 206 final Set<String> requiredClaims, 207 final Set<String> prohibitedClaims) { 208 209 this.acceptedAudienceValues = acceptedAudience != null ? Collections.unmodifiableSet(acceptedAudience) : null; 210 211 this.exactMatchClaims = exactMatchClaims != null ? exactMatchClaims : new JWTClaimsSet.Builder().build(); 212 213 Set<String> requiredClaimsCopy = new HashSet<>(this.exactMatchClaims.getClaims().keySet()); 214 if (acceptedAudienceValues != null && ! acceptedAudienceValues.contains(null)) { 215 // check if an explicit aud is required 216 requiredClaimsCopy.add(JWTClaimNames.AUDIENCE); 217 } 218 if (requiredClaims != null) { 219 requiredClaimsCopy.addAll(requiredClaims); 220 } 221 this.requiredClaims = Collections.unmodifiableSet(requiredClaimsCopy); 222 223 this.prohibitedClaims = prohibitedClaims != null ? Collections.unmodifiableSet(prohibitedClaims) : Collections.<String>emptySet(); 224 } 225 226 227 /** 228 * Returns the accepted audience values. 229 * 230 * @return The accepted JWT audience values, {@code null} if not 231 * specified. A {@code null} value in the set allows JWTs with 232 * no audience. 233 */ 234 public Set<String> getAcceptedAudienceValues() { 235 return acceptedAudienceValues; 236 } 237 238 239 /** 240 * Returns the JWT claims that must match exactly. 241 * 242 * @return The JWT claims that must match exactly, empty set if none. 243 */ 244 public JWTClaimsSet getExactMatchClaims() { 245 return exactMatchClaims; 246 } 247 248 249 /** 250 * Returns the names of the JWT claims that must be present, including 251 * the name of those that must match exactly. 252 * 253 * @return The names of the JWT claims that must be present, empty set 254 * if none. 255 */ 256 public Set<String> getRequiredClaims() { 257 return requiredClaims; 258 } 259 260 261 /** 262 * Returns the names of the JWT claims that must not be present. 263 * 264 * @return The names of the JWT claims that must not be present, empty 265 * set if none. 266 */ 267 public Set<String> getProhibitedClaims() { 268 return prohibitedClaims; 269 } 270 271 272 @Override 273 public int getMaxClockSkew() { 274 return maxClockSkew; 275 } 276 277 278 @Override 279 public void setMaxClockSkew(final int maxClockSkewSeconds) { 280 maxClockSkew = maxClockSkewSeconds; 281 } 282 283 284 @Override 285 public void verify(final JWTClaimsSet claimsSet, final C context) 286 throws BadJWTException { 287 288 // Check audience 289 if (acceptedAudienceValues != null) { 290 List<String> audList = claimsSet.getAudience(); 291 if (audList != null && ! audList.isEmpty()) { 292 boolean audMatch = false; 293 for (String aud : audList) { 294 if (acceptedAudienceValues.contains(aud)) { 295 audMatch = true; 296 break; 297 } 298 } 299 if (! audMatch) { 300 throw new BadJWTException("JWT audience rejected: " + audList); 301 } 302 } else if (! acceptedAudienceValues.contains(null)) { 303 throw new BadJWTException("JWT missing required audience"); 304 } 305 } 306 307 // Check if all required claims are present 308 if (! claimsSet.getClaims().keySet().containsAll(requiredClaims)) { 309 SortedSet<String> missingClaims = new TreeSet<>(requiredClaims); 310 missingClaims.removeAll(claimsSet.getClaims().keySet()); 311 throw new BadJWTException("JWT missing required claims: " + missingClaims); 312 } 313 314 // Check if prohibited claims are present 315 SortedSet<String> presentProhibitedClaims = new TreeSet<>(); 316 for (String prohibited: prohibitedClaims) { 317 if (claimsSet.getClaims().containsKey(prohibited)) { 318 presentProhibitedClaims.add(prohibited); 319 } 320 } 321 if (! presentProhibitedClaims.isEmpty()) { 322 throw new BadJWTException("JWT has prohibited claims: " + presentProhibitedClaims); 323 } 324 325 // Check exact matches 326 for (String exactMatch: exactMatchClaims.getClaims().keySet()) { 327 Object actualClaim = claimsSet.getClaim(exactMatch); 328 Object expectedClaim = exactMatchClaims.getClaim(exactMatch); 329 if (! actualClaim.equals(expectedClaim)) { 330 throw new BadJWTException("JWT " + exactMatch + " claim has value " + actualClaim + ", must be " + expectedClaim); 331 } 332 } 333 334 // Check time window 335 final Date now = currentTime(); 336 337 if (now != null) { 338 final Date exp = claimsSet.getExpirationTime(); 339 if (exp != null) { 340 341 if (! DateUtils.isAfter(exp, now, maxClockSkew)) { 342 throw new BadJWTException("Expired JWT"); 343 } 344 } 345 346 final Date nbf = claimsSet.getNotBeforeTime(); 347 if (nbf != null) { 348 349 if (! DateUtils.isBefore(nbf, now, maxClockSkew)) { 350 throw new BadJWTException("JWT before use time"); 351 } 352 } 353 } 354 } 355 356 357 /** 358 * Returns the current time for the purpose of "exp" (expiration time) 359 * and "nbf" (not-before time) claim verification. This method can be 360 * overridden to inject an alternative time provider (e.g. for testing 361 * purposes) or to disable "exp" and "nbf" verification. 362 * 363 * @return The current time or {@code null} to disable "exp" and "nbf" 364 * claim verification entirely. 365 */ 366 protected Date currentTime() { 367 368 return new Date(); 369 } 370}