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&lt;?&gt; verifier = new DefaultJWTClaimsVerifier&lt;&gt;(
066 *      new JWTClaimsSet.Builder()
067 *              .issuer("https://issuer.example.com")
068 *              .audience("https://client.example.com")
069 *              .build(),
070 *      new HashSet&lt;&gt;(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}