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